Skip to content

Commit

Permalink
Create multi-layer docker files (#903)
Browse files Browse the repository at this point in the history
* Create multi-layer docker files

This commit improves our docker file generation in order to use
multiple layers for dependencies. Instead of having a single libs
directory, we now have 3 layers which are copied to the `libs`
directory:

- one for the transitive dependencies which are not snapshot
- one for the snapshot dependencies
- one for the project dependencies

In addition to the layer which contains the application jar.
Moreover, the dockerfile creation now makes use of the `--link`
option for COPY, which makes it even more efficient.

Fixes #735

* Use `--link` for all COPY operations

* Fix CRaC docker files

* Disable `--link` by default

As this is causing some flakiness on CI, it could probably cause flakiness
on user builds too.

* Do not use COPY for empty layers

* Restore --link by default

Since it wasn't at fault when running builds on CI.

* Restore lenient(false)
  • Loading branch information
melix authored Dec 11, 2023
1 parent e0c2df2 commit 9644e11
Show file tree
Hide file tree
Showing 23 changed files with 526 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,12 @@ private void registerDockerImage(Project project, TaskProvider<Jar> optimizedJar
});
} else {
dockerImages.create("optimized", image -> {
MicronautDockerPlugin.createDependencyLayers(image, project.getConfigurations().getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME));
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.APP);
layer.getRuntimeKind().set(runtime == OptimizerIO.TargetRuntime.JIT ? RuntimeKind.JIT : RuntimeKind.NATIVE);
layer.getFiles().from(optimizedRunnerJar);
});
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.LIBS);
layer.getFiles().from(layer.getFiles().from(project.getConfigurations().getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME))
);
});
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import com.bmuschko.gradle.docker.tasks.image.Dockerfile;
import io.micronaut.gradle.docker.DockerBuildStrategy;
import io.micronaut.gradle.docker.MicronautDockerfile;
import io.micronaut.gradle.docker.model.Layer;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
Expand All @@ -16,6 +19,7 @@
import org.gradle.api.tasks.TaskAction;
import org.gradle.jvm.toolchain.JavaLanguageVersion;

import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -24,6 +28,7 @@

import static io.micronaut.gradle.crac.MicronautCRaCPlugin.ARM_ARCH;
import static io.micronaut.gradle.crac.MicronautCRaCPlugin.X86_64_ARCH;
import static io.micronaut.gradle.docker.MicronautDockerfile.applyStandardTransforms;

@CacheableTask
public abstract class CRaCCheckpointDockerfile extends Dockerfile {
Expand Down Expand Up @@ -59,6 +64,25 @@ public abstract class CRaCCheckpointDockerfile extends Dockerfile {
@Input
public abstract Property<JavaLanguageVersion> getJavaVersion();

/**
* The layers to copy to the image.
* @return the layers
*/
@Input
public abstract ListProperty<Layer> getLayers();

/**
* If true, the COPY command will use --link option when copying files from the build context.
* Defaults to false.
* @return The use copy link property
*/
@Input
@Optional
public abstract Property<Boolean> getUseCopyLink();

@Inject
protected abstract ObjectFactory getObjects();

@SuppressWarnings("java:S5993") // Gradle API
public CRaCCheckpointDockerfile() {
setGroup(BasePlugin.BUILD_GROUP);
Expand Down Expand Up @@ -86,9 +110,11 @@ public void create() throws IOException {
}
}
super.create();
applyStandardTransforms(getUseCopyLink(), getObjects(), this);
getProject().getLogger().lifecycle("Checkpoint Dockerfile written to: {}", getDestFile().get().getAsFile().getAbsolutePath());
}


@SuppressWarnings("java:S5738") // Using deprecated method still, until it's removal in 4.0.0
private void setupInstructions(List<Instruction> additionalInstructions) {
DockerBuildStrategy strategy = this.getBuildStrategy().getOrElse(DockerBuildStrategy.DEFAULT);
Expand Down Expand Up @@ -177,10 +203,7 @@ static void setupResources(CRaCCheckpointDockerfile task) {
" && rm \"$name\"");

task.instruction("# Copy layers");
task.copyFile("layers/libs", workDir + "/libs");
task.copyFile("layers/classes", workDir + "/classes");
task.copyFile("layers/resources", workDir + "/resources");
task.copyFile("layers/application.jar", workDir + "/application.jar");
MicronautDockerfile.setupResources(task, task.getLayers().get(), workDir);

task.instruction("# Add build scripts");
task.copyFile("scripts/checkpoint.sh", workDir + "/checkpoint.sh");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,7 @@ private void setupResources() {
copyFile("--from=" + createCheckpointImageName(getProject()) + " /azul-crac-jdk", "/azul-crac-jdk");
instruction("# Copy layers");
copyFile("cr", workDir + "/cr");
copyFile("layers/libs", workDir + "/libs");
copyFile("layers/classes", workDir + "/classes");
copyFile("layers/resources", workDir + "/resources");
copyFile("layers/application.jar", workDir + "/application.jar");
MicronautDockerfile.setupResources(this, getLayers().get(), workDir);
copyFile("scripts/run.sh", workDir + "/run.sh");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ private CheckpointTasksOfNote configureCheckpointDockerBuild(Project project,
task.getArch().set(configuration.getArch());
task.getJavaVersion().set(configuration.getJavaVersion());
task.setupDockerfileInstructions();
task.getLayers().convention(buildLayersTask.flatMap(BuildLayersTask::getLayers));
});

TaskProvider<DockerBuildImage> dockerBuildTask = tasks.register(adaptTaskName("checkpointBuildImage", imageName), DockerBuildImage.class, task -> {
Expand Down
1 change: 1 addition & 0 deletions docker-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ dependencies {
compileOnly libs.graalvmPlugin

testImplementation testFixtures(project(":micronaut-minimal-plugin"))
testImplementation libs.mockserver.netty
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
import java.util.List;
import java.util.Optional;

abstract class DockerfileEditor {
public abstract class DockerfileEditor {
private DockerfileEditor() {

}

static void apply(ObjectFactory objects, Dockerfile task, List<Action<? super Editor>> actions) {
public static void apply(ObjectFactory objects, Dockerfile task, List<Action<? super Editor>> actions) {
try {
Path dockerFile = task.getDestFile().get().getAsFile().toPath();
List<String> lines = Files.readAllLines(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFile;
import org.gradle.api.plugins.BasePlugin;
Expand Down Expand Up @@ -63,19 +65,54 @@ public void apply(Project project) {
dockerImages.all(image -> createDockerImage(project, image));
TaskProvider<Jar> runnerJar = createMainRunnerJar(project, tasks);
dockerImages.create("main", image -> {
createDependencyLayers(image, project.getConfigurations().getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME));
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.APP);
layer.getFiles().from(runnerJar);
});
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.LIBS);
layer.getFiles().from(project.getConfigurations().getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME));
});
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.EXPANDED_RESOURCES);
layer.getFiles().from(project.getExtensions().getByType(SourceSetContainer.class)
.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getResourcesDir());
});
});
}

public static void createDependencyLayers(MicronautDockerImage image, Configuration configuration) {
var projectLibs = configuration.getIncoming()
.artifactView(view -> {
view.lenient(true);
view.componentFilter(ProjectComponentIdentifier.class::isInstance);
}).getFiles();
var snapshotLibs = configuration.getIncoming()
.artifactView(view -> {
view.lenient(true);
view.componentFilter(component -> {
if (component instanceof ModuleComponentIdentifier module) {
return module.getVersion().endsWith("-SNAPSHOT");
}
return !(component instanceof ProjectComponentIdentifier);
});
}).getFiles();
var allOtherLibs = configuration.getIncoming()
.artifactView(view -> {
view.lenient(true);
view.componentFilter(component -> {
if (component instanceof ModuleComponentIdentifier module) {
return !module.getVersion().endsWith("-SNAPSHOT");
}
return !(component instanceof ProjectComponentIdentifier);
});
}).getFiles();
// First, all dependencies that are not snapshots
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.LIBS);
layer.getFiles().from(allOtherLibs);
});
// Then all snapshots
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.SNAPSHOT_LIBS);
layer.getFiles().from(snapshotLibs);
});
// Finally, all project dependencies
image.addLayer(layer -> {
layer.getLayerKind().set(LayerKind.PROJECT_LIBS);
layer.getFiles().from(projectLibs);
});
}

Expand Down Expand Up @@ -149,10 +186,10 @@ private TaskProvider<Jar> createMainRunnerJar(Project project, TaskContainer tas
jar.dependsOn(tasks.findByName("classes"));
jar.getArchiveClassifier().set("runner");
SourceSetContainer sourceSets = project
.getExtensions().getByType(SourceSetContainer.class);
.getExtensions().getByType(SourceSetContainer.class);

SourceSet mainSourceSet = sourceSets
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);

FileCollection dirs = mainSourceSet.getOutput().getClassesDirs();

Expand All @@ -164,7 +201,7 @@ private TaskProvider<Jar> createMainRunnerJar(Project project, TaskContainer tas
attrs.put("Class-Path", project.getProviders().provider(() -> {
List<String> classpath = new ArrayList<>();
Configuration runtimeClasspath = project.getConfigurations()
.getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME);
.getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME);

classpath.add("resources/");
classpath.add("classes/");
Expand Down Expand Up @@ -204,6 +241,7 @@ private Optional<TaskProvider<MicronautDockerfile>> configureDockerBuild(Project
task.setDescription("Builds a Docker File for image " + imageName);
task.getDestFile().set(targetDockerFile);
task.setupDockerfileInstructions();
task.getLayers().convention(buildLayersTask.flatMap(BuildLayersTask::getLayers));
});
}
TaskProvider<DockerBuildImage> dockerBuildTask = tasks.register(adaptTaskName("dockerBuild", imageName), DockerBuildImage.class, task -> {
Expand Down Expand Up @@ -246,20 +284,22 @@ private TaskProvider<NativeImageDockerfile> configureNativeDockerBuild(Project p
throw new GradleException("Unable to configure docker task for image " + imageName, e);
}
task.getDestFile().set(targetDockerFile);
task.getLayers().convention(buildLayersTask.flatMap(BuildLayersTask::getLayers));
});
} else {
dockerFileTask = tasks.register(dockerfileNativeTaskName, NativeImageDockerfile.class, task -> {
task.setGroup(BasePlugin.BUILD_GROUP);
task.setDescription("Builds a Native Docker File for image " + imageName);
task.getDestFile().set(targetDockerFile);
task.getLayers().convention(buildLayersTask.flatMap(BuildLayersTask::getLayers));
});
}
TaskProvider<PrepareDockerContext> prepareContext = tasks.register(adaptTaskName("dockerPrepareContext", imageName), PrepareDockerContext.class, context -> {
// Because docker requires all files to be found in the build context we need to
// copy the configuration file directories into the build context
context.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir("docker/native-" + imageName + "/config-dirs"));
context.getInputDirectories().from(dockerFileTask.map(t -> t.getNativeImageOptions()
.map(NativeImageOptions::getConfigurationFileDirectories).get() // drop dependency on building image
.map(NativeImageOptions::getConfigurationFileDirectories).get() // drop dependency on building image
));
});
TaskProvider<DockerBuildImage> dockerBuildTask = tasks.register(adaptTaskName("dockerBuildNative", imageName), DockerBuildImage.class, task -> {
Expand Down Expand Up @@ -290,20 +330,20 @@ private TaskProvider<NativeImageDockerfile> configureNativeDockerBuild(Project p
});
TaskProvider<DockerCopyFileFromContainer> buildLambdaZip = taskContainer.register(adaptTaskName("buildNativeLambda", imageName), DockerCopyFileFromContainer.class);
Provider<String> lambdaZip = project.getLayout()
.getBuildDirectory()
.dir("libs")
.map(dir -> dir.file(project.getName() + "-" + project.getVersion() + "-" + simpleNameOf("lambda", imageName) + ".zip").getAsFile().getAbsolutePath());
.getBuildDirectory()
.dir("libs")
.map(dir -> dir.file(project.getName() + "-" + project.getVersion() + "-" + simpleNameOf("lambda", imageName) + ".zip").getAsFile().getAbsolutePath());
TaskProvider<DockerRemoveContainer> removeContainer = taskContainer.register(adaptTaskName("destroyLambdaContainer", imageName), DockerRemoveContainer.class);
removeContainer.configure(task -> {
task.mustRunAfter(buildLambdaZip);
task.getContainerId().set(
createLambdaContainer.flatMap(DockerCreateContainer::getContainerId)
createLambdaContainer.flatMap(DockerCreateContainer::getContainerId)
);
});
buildLambdaZip.configure(task -> {
task.dependsOn(createLambdaContainer);
task.getContainerId().set(
createLambdaContainer.flatMap(DockerCreateContainer::getContainerId)
createLambdaContainer.flatMap(DockerCreateContainer::getContainerId)
);
task.getRemotePath().set("/function/function.zip");
task.getHostPath().set(lambdaZip);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.micronaut.gradle.docker;

import com.bmuschko.gradle.docker.tasks.image.Dockerfile;
import io.micronaut.gradle.docker.model.Layer;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.model.ObjectFactory;
Expand Down Expand Up @@ -39,6 +40,22 @@ public abstract class MicronautDockerfile extends Dockerfile implements DockerBu
@Input
private final Property<String> targetWorkingDirectory;

/**
* The layers to copy to the image.
* @return the layers
*/
@Input
public abstract ListProperty<Layer> getLayers();

/**
* If true, the COPY command will use --link option when copying files from the build context.
* Defaults to false.
* @return The use copy link property
*/
@Input
@Optional
public abstract Property<Boolean> getUseCopyLink();

public MicronautDockerfile() {
Project project = getProject();
setGroup(BasePlugin.BUILD_GROUP);
Expand Down Expand Up @@ -77,12 +94,21 @@ protected Provider<List<String>> getTweaks() {
@Override
public void create() throws IOException {
super.create();
applyStandardTransforms(getUseCopyLink(), getObjects(), this);
if (getDockerfileTweaks().isPresent()) {
DockerfileEditor.apply(getObjects(), this, getDockerfileTweaks().get());
}
getLogger().lifecycle("Dockerfile written to: {}", getDestFile().get().getAsFile().getAbsolutePath());
}

public static void applyStandardTransforms(Provider<Boolean> useCopyLink, ObjectFactory objects, Dockerfile task) {
if (Boolean.TRUE.equals(useCopyLink.getOrElse(true))) {
DockerfileEditor.apply(objects, task, List.of(
editor -> editor.replaceRegex("COPY (?!--link)(.*)", "COPY --link $1")
));
}
}

protected void setupInstructions(List<Instruction> additionalInstructions) {
String workDir = getTargetWorkingDirectory().get();
DockerBuildStrategy buildStrategy = this.buildStrategy.getOrElse(DockerBuildStrategy.DEFAULT);
Expand All @@ -96,11 +122,7 @@ protected void setupInstructions(List<Instruction> additionalInstructions) {
javaApplication.getMainClass().set("com.fnproject.fn.runtime.EntryPoint");
from(new Dockerfile.From(from != null ? from : "fnproject/fn-java-fdk:" + getProjectFnVersion()));
workingDir("/function");
runCommand("mkdir -p /function/app/resources");
copyFile("layers/libs/*.jar", "/function/app/");
copyFile("layers/classes", "/function/app/classes");
copyFile("layers/resources", "/function/app/resources");
copyFile("layers/application.jar", "/function/app/");
setupResources(this, getLayers().get(), "function");
String cmd = this.defaultCommand.get();
if ("none".equals(cmd)) {
super.defaultCommand("io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest");
Expand All @@ -112,7 +134,7 @@ protected void setupInstructions(List<Instruction> additionalInstructions) {
javaApplication.getMainClass().set("io.micronaut.function.aws.runtime.MicronautLambdaRuntime");
default:
from(new Dockerfile.From(from != null ? from : DEFAULT_BASE_IMAGE));
setupResources(this);
setupResources(this, getLayers().get(), null);
exposePort(exposedPorts);
getInstructions().addAll(additionalInstructions);
if (getInstructions().get().stream().noneMatch(instruction -> instruction.getKeyword().equals(EntryPointInstruction.KEYWORD))) {
Expand Down Expand Up @@ -198,15 +220,25 @@ private String getProjectFnVersion() {
return "latest";
}

static void setupResources(Dockerfile task) {
public static void setupResources(Dockerfile task, List<Layer> layers, String workDir) {
if (workDir == null) {
workDir = determineWorkingDir(task);
}
task.workingDir(workDir);
for (Layer layer : layers) {
var files = layer.getFiles();
if (!files.isEmpty()) {
var kind = layer.getLayerKind().get();
task.copyFile("layers/" + kind.sourceDirName(), workDir + "/" + kind.targetDirName());
}
}
}

private static String determineWorkingDir(Dockerfile task) {
String workDir = DEFAULT_WORKING_DIR;
if (task instanceof DockerBuildOptions dbo) {
workDir = dbo.getTargetWorkingDirectory().get();
}
task.workingDir(workDir);
task.copyFile("layers/libs", workDir + "/libs");
task.copyFile("layers/classes", workDir + "/classes");
task.copyFile("layers/resources", workDir + "/resources");
task.copyFile("layers/application.jar", workDir + "/application.jar");
return workDir;
}
}
Loading

0 comments on commit 9644e11

Please sign in to comment.