diff --git a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildDockerStepsIntegrationTest.java b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildDockerStepsIntegrationTest.java index c4cce6e656..01e39b31a9 100644 --- a/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildDockerStepsIntegrationTest.java +++ b/jib-core/src/integration-test/java/com/google/cloud/tools/jib/builder/BuildDockerStepsIntegrationTest.java @@ -19,10 +19,8 @@ import com.google.cloud.tools.jib.Command; import com.google.cloud.tools.jib.cache.Caches; import com.google.cloud.tools.jib.image.ImageReference; -import com.google.cloud.tools.jib.registry.LocalRegistry; import java.nio.file.Path; import org.junit.Assert; -import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -30,8 +28,6 @@ /** Integration tests for {@link BuildDockerSteps}. */ public class BuildDockerStepsIntegrationTest { - @ClassRule public static LocalRegistry localRegistry = new LocalRegistry(5000); - private static final TestBuildLogger logger = new TestBuildLogger(); @Rule public TemporaryFolder temporaryCacheDirectory = new TemporaryFolder(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildContainerConfigurationStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildContainerConfigurationStep.java index bbcde14340..ec0013259a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildContainerConfigurationStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildContainerConfigurationStep.java @@ -60,19 +60,19 @@ class BuildContainerConfigurationStep implements Callable @Override public ListenableFuture call() throws ExecutionException, InterruptedException { // TODO: This might need to belong in BuildImageSteps. - List> afterBaseImageLayerFuturesFutureDependencies = new ArrayList<>(); - afterBaseImageLayerFuturesFutureDependencies.addAll( + List> afterImageLayerFuturesFutureDependencies = new ArrayList<>(); + afterImageLayerFuturesFutureDependencies.addAll( NonBlockingFutures.get(pullBaseImageLayerFuturesFuture)); - afterBaseImageLayerFuturesFutureDependencies.addAll(buildApplicationLayerFutures); - return Futures.whenAllSucceed(afterBaseImageLayerFuturesFutureDependencies) - .call(this::afterBaseImageLayerFuturesFuture, listeningExecutorService); + afterImageLayerFuturesFutureDependencies.addAll(buildApplicationLayerFutures); + return Futures.whenAllSucceed(afterImageLayerFuturesFutureDependencies) + .call(this::afterImageLayerFuturesFuture, listeningExecutorService); } /** * Depends on {@code pushAuthorizationFuture}, {@code pullBaseImageLayerFuturesFuture.get()}, and * {@code buildApplicationLayerFutures}. */ - private Blob afterBaseImageLayerFuturesFuture() + private Blob afterImageLayerFuturesFuture() throws ExecutionException, InterruptedException, LayerPropertyNotFoundException { try (Timer ignored = new Timer(buildConfiguration.getBuildLogger(), DESCRIPTION)) { // Constructs the image. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildDockerSteps.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildDockerSteps.java index 5daf925d91..4c1e3c2b51 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildDockerSteps.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildDockerSteps.java @@ -47,7 +47,7 @@ public class BuildDockerSteps { private final SourceFilesConfiguration sourceFilesConfiguration; private final Caches.Initializer cachesInitializer; - BuildDockerSteps( + public BuildDockerSteps( BuildConfiguration buildConfiguration, SourceFilesConfiguration sourceFilesConfiguration, Caches.Initializer cachesInitializer) { diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildTarballAndLoadDockerStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildTarballAndLoadDockerStep.java index 712901c9e3..574f7c0af4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildTarballAndLoadDockerStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/BuildTarballAndLoadDockerStep.java @@ -112,9 +112,12 @@ private Void afterPushBaseImageLayerFuturesFuture() } for (Future cachedLayerFuture : buildApplicationLayerFutures) { Path layerFile = NonBlockingFutures.get(cachedLayerFuture).getContentFile(); - layerFiles.add(layerFile.getFileName().toString()); - tarStreamBuilder.addEntry( - new TarArchiveEntry(layerFile.toFile(), layerFile.getFileName().toString())); + // TODO: Consolidate with build configuration step so we don't have to rebuild the image + if (!layerFiles.contains(layerFile.getFileName().toString())) { + layerFiles.add(layerFile.getFileName().toString()); + tarStreamBuilder.addEntry( + new TarArchiveEntry(layerFile.toFile(), layerFile.getFileName().toString())); + } } // Add config to tarball diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/PushImageStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/PushImageStep.java index 22e91377dc..a108733715 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/PushImageStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/PushImageStep.java @@ -106,7 +106,7 @@ private Void afterPushBaseImageLayerFuturesFuture() buildConfiguration.getTargetImageRegistry(), buildConfiguration.getTargetImageRepository()); - // TODO: Consolidate with BuildAndPushContainerConfigurationStep. + // TODO: Consolidate with BuildContainerConfigurationStep. // Constructs the image. Image image = new Image(); for (Future cachedLayerFuture : diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildDockerStepsRunner.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildDockerStepsRunner.java new file mode 100644 index 0000000000..333719e467 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/BuildDockerStepsRunner.java @@ -0,0 +1,179 @@ +/* + * 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.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.BuildDockerSteps; +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +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.registry.RegistryAuthenticationFailedException; +import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException; +import java.io.IOException; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; +import org.apache.http.conn.HttpHostConnectException; + +/** + * Runs {@link BuildDockerSteps} and builds helpful error messages. + * + *

TODO: Consolidate with {@link BuildImageStepsRunner}. + */ +public class BuildDockerStepsRunner { + + /** + * Sets up a new {@link BuildDockerStepsRunner}. Creates the directory for the cache, if needed. + * + * @param useOnlyProjectCache if {@code true}, sets the base layers cache directory to be the same + * as the application layers cache directory + * @throws CacheDirectoryCreationException if the {@code cacheDirectory} could not be created + */ + public static BuildDockerStepsRunner newRunner( + BuildConfiguration buildConfiguration, + SourceFilesConfiguration sourceFilesConfiguration, + Path cacheDirectory, + boolean useOnlyProjectCache) + throws CacheDirectoryCreationException { + if (!Files.exists(cacheDirectory)) { + try { + Files.createDirectory(cacheDirectory); + + } catch (IOException ex) { + throw new CacheDirectoryCreationException(cacheDirectory, ex); + } + } + Caches.Initializer cachesInitializer = Caches.newInitializer(cacheDirectory); + if (useOnlyProjectCache) { + cachesInitializer.setBaseCacheDirectory(cacheDirectory); + } + + return new BuildDockerStepsRunner( + buildConfiguration, sourceFilesConfiguration, cachesInitializer); + } + + private final Supplier buildDockerStepsSupplier; + + private BuildDockerStepsRunner( + BuildConfiguration buildConfiguration, + SourceFilesConfiguration sourceFilesConfiguration, + Caches.Initializer cachesInitializer) { + buildDockerStepsSupplier = + () -> new BuildDockerSteps(buildConfiguration, sourceFilesConfiguration, cachesInitializer); + } + + /** + * Runs the {@link BuildDockerSteps}. + * + * @param helpfulSuggestions suggestions to use in help messages for exceptions + */ + public void buildDocker(HelpfulSuggestions helpfulSuggestions) + throws BuildImageStepsExecutionException { + BuildDockerSteps buildDockerSteps = buildDockerStepsSupplier.get(); + + try { + buildDockerSteps.run(); + + } catch (CacheMetadataCorruptedException cacheMetadataCorruptedException) { + // TODO: Have this be different for Maven and Gradle. + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forCacheMetadataCorrupted(), cacheMetadataCorruptedException); + + } catch (ExecutionException executionException) { + BuildConfiguration buildConfiguration = buildDockerSteps.getBuildConfiguration(); + + if (executionException.getCause() instanceof HttpHostConnectException) { + // Failed to connect to registry. + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forHttpHostConnect(), executionException.getCause()); + + } else if (executionException.getCause() instanceof RegistryUnauthorizedException) { + handleRegistryUnauthorizedException( + (RegistryUnauthorizedException) executionException.getCause(), + buildConfiguration, + helpfulSuggestions); + + } else if (executionException.getCause() instanceof RegistryAuthenticationFailedException + && executionException.getCause().getCause() instanceof HttpResponseException) { + handleRegistryUnauthorizedException( + new RegistryUnauthorizedException( + buildConfiguration.getTargetImageRegistry(), + buildConfiguration.getTargetImageRepository(), + (HttpResponseException) executionException.getCause().getCause()), + buildConfiguration, + helpfulSuggestions); + + } else if (executionException.getCause() instanceof UnknownHostException) { + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forUnknownHost(), executionException.getCause()); + + } else { + throw new BuildImageStepsExecutionException( + helpfulSuggestions.none(), executionException.getCause()); + } + + } catch (InterruptedException | IOException ex) { + // TODO: Add more suggestions for various build failures. + throw new BuildImageStepsExecutionException(helpfulSuggestions.none(), ex); + + } catch (CacheDirectoryNotOwnedException ex) { + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forCacheDirectoryNotOwned(ex.getCacheDirectory()), ex); + } + } + + private void handleRegistryUnauthorizedException( + RegistryUnauthorizedException registryUnauthorizedException, + BuildConfiguration buildConfiguration, + HelpfulSuggestions helpfulSuggestions) + throws BuildImageStepsExecutionException { + if (registryUnauthorizedException.getHttpResponseException().getStatusCode() + == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { + // No permissions for registry/repository. + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forHttpStatusCodeForbidden( + registryUnauthorizedException.getImageReference()), + registryUnauthorizedException); + + } else { + boolean isRegistryForBase = + registryUnauthorizedException + .getRegistry() + .equals(buildConfiguration.getBaseImageRegistry()); + boolean areBaseImageCredentialsConfigured = + buildConfiguration.getBaseImageCredentialHelperName() != null + || buildConfiguration.getKnownBaseRegistryCredentials() != null; + if (isRegistryForBase && !areBaseImageCredentialsConfigured) { + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forNoCredentialHelpersDefinedForBaseImage( + registryUnauthorizedException.getRegistry()), + registryUnauthorizedException); + } + + // Credential helper probably was not configured correctly or did not have the necessary + // credentials. + throw new BuildImageStepsExecutionException( + helpfulSuggestions.forCredentialsNotCorrect(registryUnauthorizedException.getRegistry()), + registryUnauthorizedException); + } + } +} 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 6880ce2642..6555cfbb93 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 @@ -49,6 +49,21 @@ private static String buildAndRun(TestProject testProject, String imageReference return new Command("docker", "run", imageReference).run(); } + private static String buildToDockerDaemonAndRun(TestProject testProject, String imageReference) + throws IOException, InterruptedException { + BuildResult buildResult = testProject.build("build", "jibBuildDocker"); + + BuildTask jibBuildDockerTask = buildResult.task(":jibBuildDocker"); + + Assert.assertNotNull(jibBuildDockerTask); + Assert.assertEquals(TaskOutcome.SUCCESS, jibBuildDockerTask.getOutcome()); + Assert.assertThat( + buildResult.getOutput(), + CoreMatchers.containsString("Built image to Docker daemon as " + imageReference)); + + return new Command("docker", "run", imageReference).run(); + } + @Test public void testBuild_empty() throws IOException, InterruptedException { Assert.assertEquals( @@ -62,6 +77,22 @@ public void testBuild_simple() throws IOException, InterruptedException { buildAndRun(simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); } + @Test + public void testDockerDaemon_empty() throws IOException, InterruptedException { + Assert.assertEquals( + "", + buildToDockerDaemonAndRun( + emptyTestProject, "gcr.io/jib-integration-testing/emptyimage:gradle")); + } + + @Test + public void testDockerDaemon_simple() throws IOException, InterruptedException { + Assert.assertEquals( + "Hello, world\n", + buildToDockerDaemonAndRun( + simpleTestProject, "gcr.io/jib-integration-testing/simpleimage:gradle")); + } + @Test public void testDockerContext() throws IOException, InterruptedException { BuildResult buildResult = simpleTestProject.build("build", "jibDockerContext", "--info"); 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 8e6eb13ed4..b7e4b1c736 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,18 +16,121 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.builder.BuildConfiguration; +import com.google.cloud.tools.jib.frontend.BuildDockerStepsRunner; +import com.google.cloud.tools.jib.frontend.BuildImageStepsExecutionException; +import com.google.cloud.tools.jib.frontend.CacheDirectoryCreationException; +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.credentials.RegistryCredentials; +import com.google.common.base.Preconditions; +import java.nio.file.Path; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.TaskAction; /** Builds a container image and exports to the default Docker daemon. */ public class BuildDockerTask extends DefaultTask { + /** + * Directory name for the cache. The directory will be relative to the build output directory. + * + *

TODO: Move to ProjectProperties. + */ + private static final String CACHE_DIRECTORY_NAME = "jib-cache"; + + /** + * Converts an {@link ImageConfiguration} to an {@link Authorization}. + * + *

TODO: Move to ImageConfiguration. + */ + @Nullable + private static Authorization getImageAuthorization(ImageConfiguration imageConfiguration) { + if (imageConfiguration.getAuth().getUsername() == null + || imageConfiguration.getAuth().getPassword() == null) { + return null; + } + + return Authorizations.withBasicCredentials( + imageConfiguration.getAuth().getUsername(), imageConfiguration.getAuth().getPassword()); + } + @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.}. + */ + @Nested + @Nullable + public JibExtension getJib() { + return jibExtension; + } + + /** TODO: Refactor with {@link BuildImageTask} for less duplicate code. */ @TaskAction - public void buildDocker() { - getLogger().warn("Doing gradle jibBuildDocker!"); + public void buildDocker() throws InvalidImageReferenceException { + // Asserts required @Input parameters are not null. + Preconditions.checkNotNull(jibExtension); + + ImageReference baseImageReference = ImageReference.parse(jibExtension.getBaseImage()); + ImageReference targetImageReference = ImageReference.parse(jibExtension.getTargetImage()); + + if (baseImageReference.usesDefaultTag()) { + getLogger() + .warn( + "Base image '" + + baseImageReference + + "' does not use a specific image digest - build may not be reproducible"); + } + + ProjectProperties projectProperties = new ProjectProperties(getProject(), getLogger()); + String mainClass = projectProperties.getMainClass(jibExtension.getMainClass()); + + RegistryCredentials knownBaseRegistryCredentials = null; + Authorization fromAuthorization = getImageAuthorization(jibExtension.getFrom()); + if (fromAuthorization != null) { + knownBaseRegistryCredentials = new RegistryCredentials("jib.from.auth", fromAuthorization); + } + + BuildConfiguration buildConfiguration = + BuildConfiguration.builder(new GradleBuildLogger(getLogger())) + .setBaseImage(baseImageReference) + .setBaseImageCredentialHelperName(jibExtension.getFrom().getCredHelper()) + .setKnownBaseRegistryCredentials(knownBaseRegistryCredentials) + .setTargetImage(targetImageReference) + .setMainClass(mainClass) + .setJvmFlags(jibExtension.getJvmFlags()) + .build(); + + // Uses a directory in the Gradle build cache as the Jib cache. + Path cacheDirectory = getProject().getBuildDir().toPath().resolve(CACHE_DIRECTORY_NAME); + try { + BuildDockerStepsRunner buildDockerStepsRunner = + BuildDockerStepsRunner.newRunner( + buildConfiguration, + projectProperties.getSourceFilesConfiguration(), + cacheDirectory, + jibExtension.getUseOnlyProjectCache()); + + getLogger().lifecycle("Building to docker daemon as " + targetImageReference); + getLogger().lifecycle(""); + getLogger().lifecycle(""); + + buildDockerStepsRunner.buildDocker( + HelpfulSuggestionsProvider.get("Build to Docker daemon failed")); + + getLogger().lifecycle(""); + getLogger().lifecycle("Built image to Docker daemon as " + targetImageReference); + getLogger().lifecycle(""); + + } catch (CacheDirectoryCreationException | BuildImageStepsExecutionException ex) { + throw new GradleException(ex.getMessage(), ex.getCause()); + } } void setJibExtension(JibExtension jibExtension) {