Skip to content

Commit 077d775

Browse files
feat: add improved docker image layering (#32)
* feat: add improved docker image layering * fix: mark script as input
1 parent 2dc54fa commit 077d775

File tree

5 files changed

+104
-29
lines changed

5 files changed

+104
-29
lines changed
Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,61 @@
11
# Changelog
22

3-
43
## [0.2.4]
4+
55
- Default image has changed from gcr.io/distroless/java:11 to gcr.io/distroless/java:11-debug
66

77
## [0.3.0]
8+
89
- Added support for producing multiple variants of an application with different base images
910
- Added a default variant, `slim` based off `gcr.io/distroless/java:11`
1011

1112
## [0.3.2]
13+
1214
- Removed default `slim` variant
1315

1416
## [0.3.3]
17+
1518
- Support Java 8+
1619

1720
## [0.4.0]
21+
1822
- Change default image to `hypertrace/java:11`
1923

2024
## [0.5.0]
25+
2126
- Exposed for configuration:
2227
- `maintainer`
2328
- `port`
2429
- `adminPort`
2530
- `serviceName`
2631
- `healthCheck`
2732
- `envVars`
33+
2834
## [0.7.0]
35+
2936
- Remove variants
3037
- Remove internal usage of `com.bmuschko.docker-java-application`, cleaning up ghost tasks
3138
- Use gradle application start script to run application in docker, allowing reuse and accounting
3239
for env vars like JAVA_OPTS
3340

3441
## [0.7.1]
42+
3543
- Simplify start script to use provided parameters as given
3644

3745
## [0.8.0]
46+
3847
- Add COMMIT_SHA to the generated dockerfile as a build arg defaulting to unknown
3948
- Use COMMIT_SHA build arg to set a label `commit_sha` and environment variable `COMMIT_SHA`
4049

4150
## [0.8.2]
42-
- Add support for exposing multiple docker image ports via `ports` config
51+
52+
- Add support for exposing multiple docker image ports via `ports` config
53+
54+
## [0.9.6]
55+
56+
- Improve docker layering by splitting libraries into 3 layers. From the top down:
57+
- Local libraries being built as part of the target gradle build
58+
- Organization libraries that satisfy the `orgLibrarySpec` property on the `javaApplication`
59+
spec as documented in the README, but do not satisfy the local library criteria.
60+
- External libraries, presumed to be any library that does not meet the criteria of either of
61+
the above layers.

hypertrace-gradle-docker-java-application-plugin/README.md

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
21
# Hypertrace Docker Java Application Plugin
2+
33
###### org.hypertrace.docker-java-application-plugin
44

55
### Purpose
6-
This plugin is a highly opinionated plugin that configures the target project to build docker images of the target project.
7-
It configures the default image from `org.hypertrace.docker-plugin` to use the docker file produced by
8-
`com.bmuschko.docker-java-application`. It allows configuring several values, detailed below.
6+
7+
This plugin is a highly opinionated plugin that configures the target project to build docker images
8+
of the target project.
9+
It configures the default image from `org.hypertrace.docker-plugin` to use the docker file produced
10+
by
11+
`com.bmuschko.docker-java-application`. It allows configuring several values, detailed below.
912
Additionally, the main class is taken from the application plugin - the
1013
default behavior of `com.bmuschko.docker-java-application` for identifying main classes is not used.
1114
An application can define multiple variants which allow specifying different base images to use.
@@ -19,12 +22,14 @@ An application can define multiple variants which allow specifying different bas
1922
- `port` (_DEPRECATED_ - replaced by `ports`)
2023
- Integer
2124
- No default
22-
- Exposed in dockerfile if set. If used in conjunction with `ports`, the combined list will be exposed.
25+
- Exposed in dockerfile if set. If used in conjunction with `ports`, the combined list will be
26+
exposed.
2327
- `ports`
24-
- Integer List
25-
- No default
26-
- Exposed in dockerfile if set. If used in conjunction with deprecated `port`, the combined list will be exposed.
27-
- This does not set a default for the admin port
28+
- Integer List
29+
- No default
30+
- Exposed in dockerfile if set. If used in conjunction with deprecated `port`, the combined list
31+
will be exposed.
32+
- This does not set a default for the admin port
2833
- `adminPort`
2934
- Integer
3035
- No default except for deprecated case described below
@@ -36,13 +41,19 @@ An application can define multiple variants which allow specifying different bas
3641
- Added to envVars as `SERVICE_NAME`
3742
- `healthCheck`
3843
- String
39-
- If `adminPort` is set, defaults to `-interval=2s --start-period=15s --timeout=2s CMD wget -qO http://127.0.0.1:${adminPort}/health &> /dev/null || exit`
44+
- If `adminPort` is set, defaults
45+
to `-interval=2s --start-period=15s --timeout=2s CMD wget -qO http://127.0.0.1:${adminPort}/health &> /dev/null || exit`
4046
- If `adminPort` is unset, `healthCheck` has no default
4147
- `envVars`
4248
- Map<String, String>
4349
- Defaults to `{"SERVICE_NAME": "${serviceName}"}`
4450
- Can be appended to with `put`, or overwritten with `set`
45-
51+
- `orgLibrarySpec`
52+
- `Spec<ResolvedArtifact>`
53+
- Defaults to any artifact with a group starting with `org.hypertrace`
54+
- This is used in layering the image to separate org libraries into a layer above external
55+
libraries, under the assumption these will change (and thus invalidate the layer) more
56+
frequently.
4657

4758
For further configuration, use `org.hypertrace.docker-plugin` directly and create a dockerfile.
4859

@@ -68,6 +79,7 @@ hypertraceDocker {
6879
```
6980

7081
Producing a dockerfile:
82+
7183
```Dockerfile
7284
FROM hypertrace/java:11
7385
LABEL maintainer="Hypertrace 'https://www.hypertrace.org/'"
@@ -82,6 +94,7 @@ ENV SERVICE_NAME=example-app
8294
```
8395

8496
Overriding or providing more variables:
97+
8598
```kotlin
8699
hypertraceDocker {
87100
defaultImage {
@@ -96,6 +109,7 @@ hypertraceDocker {
96109
```
97110

98111
Produces a diff of:
112+
99113
```diff
100114
-FROM hypertrace/java:11
101115
+FROM hypertrace/java:14

hypertrace-gradle-docker-java-application-plugin/src/main/java/org/hypertrace/gradle/docker/HypertraceDockerJavaApplication.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import java.util.Collections;
44
import javax.inject.Inject;
5+
import org.gradle.api.artifacts.ResolvedArtifact;
56
import org.gradle.api.model.ObjectFactory;
67
import org.gradle.api.provider.ListProperty;
78
import org.gradle.api.provider.MapProperty;
89
import org.gradle.api.provider.Property;
10+
import org.gradle.api.specs.Spec;
911

1012
public class HypertraceDockerJavaApplication {
1113

@@ -21,6 +23,7 @@ public class HypertraceDockerJavaApplication {
2123
public final ListProperty<Integer> ports;
2224
public final Property<String> healthCheck;
2325
public final MapProperty<String, String> envVars;
26+
public Spec<ResolvedArtifact> orgLibrarySpec;
2427

2528
@Inject
2629
public HypertraceDockerJavaApplication(
@@ -42,5 +45,13 @@ public HypertraceDockerJavaApplication(
4245
this.healthCheck = objectFactory.property(String.class)
4346
.convention(this.adminPort.map(adminPort -> String.format(
4447
"HEALTHCHECK --interval=2s --start-period=15s --timeout=2s CMD wget -qO- http://127.0.0.1:%d/health &> /dev/null || exit 1", adminPort)));
48+
this.orgLibrarySpec = this::isHypertraceLibrary;
49+
}
50+
51+
private boolean isHypertraceLibrary(ResolvedArtifact artifact) {
52+
return artifact.getModuleVersion()
53+
.getId()
54+
.getGroup()
55+
.startsWith("org.hypertrace");
4556
}
4657
}

hypertrace-gradle-docker-java-application-plugin/src/main/java/org/hypertrace/gradle/docker/HypertraceDockerJavaApplicationPlugin.java

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@
1212
import java.io.File;
1313
import java.net.URL;
1414
import java.nio.file.Path;
15-
import java.time.Instant;
16-
import java.time.temporal.ChronoUnit;
1715
import java.util.ArrayList;
16+
import java.util.Collection;
1817
import java.util.List;
18+
import java.util.function.Predicate;
19+
import java.util.stream.Collectors;
1920
import org.gradle.api.Plugin;
2021
import org.gradle.api.Project;
22+
import org.gradle.api.artifacts.Configuration;
23+
import org.gradle.api.artifacts.ResolvedArtifact;
2124
import org.gradle.api.file.Directory;
22-
import org.gradle.api.file.FileCollection;
2325
import org.gradle.api.plugins.ApplicationPlugin;
2426
import org.gradle.api.plugins.JavaApplication;
2527
import org.gradle.api.plugins.JavaPluginConvention;
2628
import org.gradle.api.provider.Provider;
2729
import org.gradle.api.resources.TextResource;
30+
import org.gradle.api.specs.Spec;
2831
import org.gradle.api.tasks.SourceSet;
2932
import org.gradle.api.tasks.SourceSetOutput;
3033
import org.gradle.api.tasks.Sync;
@@ -37,9 +40,9 @@ public class HypertraceDockerJavaApplicationPlugin implements Plugin<Project> {
3740
public static final String DOCKERFILE_TASK_NAME = "generateJavaApplicationDockerfile";
3841
public static final String DOCKER_START_SCRIPT_TASK_NAME = "generateJavaApplicationDockerStartScript";
3942
public static final String SYNC_BUILD_CONTEXT_TASK_NAME = "syncJavaApplicationDockerContext";
40-
private static final String DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR = "externalLibs";
41-
4243
private static final String DOCKER_BUILD_CONTEXT_LOCAL_LIBS_DIR = "localLibs";
44+
private static final String DOCKER_BUILD_CONTEXT_ORG_LIBS_DIR = "orgLibs";
45+
private static final String DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR = "externalLibs";
4346
private static final String DOCKER_BUILD_CONTEXT_CLASSES_DIR = "classes";
4447
private static final String DOCKER_BUILD_CONTEXT_RESOURCES_DIR = "resources";
4548
private static final String DOCKER_BUILD_CONTEXT_SCRIPTS_DIR = "scripts";
@@ -119,6 +122,9 @@ void createDockerfileTask(Project project, HypertraceDockerJavaApplication javaA
119122
dockerfile.copyFile(provideIfDirectoryExists(dockerfile.getDestDir()
120123
.map(dir -> dir.dir(DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR)))
121124
.map(unused -> new CopyFile(DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR, DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR)));
125+
dockerfile.copyFile(provideIfDirectoryExists(dockerfile.getDestDir()
126+
.map(dir -> dir.dir(DOCKER_BUILD_CONTEXT_ORG_LIBS_DIR)))
127+
.map(unused -> new CopyFile(DOCKER_BUILD_CONTEXT_ORG_LIBS_DIR, DOCKER_BUILD_CONTEXT_ORG_LIBS_DIR)));
122128
dockerfile.copyFile(provideIfDirectoryExists(dockerfile.getDestDir()
123129
.map(dir -> dir.dir(DOCKER_BUILD_CONTEXT_LOCAL_LIBS_DIR)))
124130
.map(unused -> new CopyFile(DOCKER_BUILD_CONTEXT_LOCAL_LIBS_DIR, DOCKER_BUILD_CONTEXT_LOCAL_LIBS_DIR)));
@@ -148,6 +154,7 @@ void createDockerfileTask(Project project, HypertraceDockerJavaApplication javaA
148154
private void createDockerStartScriptTask(Project project) {
149155
project.getTasks()
150156
.register(DOCKER_START_SCRIPT_TASK_NAME, CreateStartScripts.class, startScript -> {
157+
startScript.getInputs().file(this.getStartScriptTemplate(project));
151158
((TemplateBasedScriptGenerator) startScript.getUnixStartScriptGenerator()).setTemplate(this.getStartScriptTemplate(project));
152159
startScript.setGroup(DockerPlugin.TASK_GROUP);
153160
startScript.setDescription("Creates a startup script for use by the docker container");
@@ -178,22 +185,32 @@ private void createContextSyncTask(Project project) {
178185
.map(Dockerfile::getDestDir)
179186
.get());
180187
sync.with(project.copySpec(spec -> {
181-
// We split deps into two dirs depending on last modified time, with the assumption that any local jars will be recently built
182-
Instant time = Instant.now()
183-
.minus(30, ChronoUnit.MINUTES);
184-
spec.into(DOCKER_BUILD_CONTEXT_LOCAL_LIBS_DIR, childSpec -> childSpec.from(getRuntimeClasspath(project)
185-
.filter(file -> Instant.ofEpochMilli(file.lastModified())
186-
.isAfter(time))));
187-
spec.into(DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR, childSpec -> childSpec.from(getRuntimeClasspath(project)
188-
.filter(file -> Instant.ofEpochMilli(file.lastModified())
189-
.isBefore(time))));
188+
spec.into(DOCKER_BUILD_CONTEXT_LOCAL_LIBS_DIR, childSpec -> childSpec.from(this.buildFilteredLibraryProvider(project, this.buildLocalArtifactPredicate(project))));
189+
spec.into(DOCKER_BUILD_CONTEXT_ORG_LIBS_DIR, childSpec -> childSpec.from(this.buildFilteredLibraryProvider(project, this.buildLocalArtifactPredicate(project)
190+
.negate()
191+
.and(this.getOrgLibPredicate(project)))));
192+
spec.into(DOCKER_BUILD_CONTEXT_EXTERNAL_LIBS_DIR, childSpec -> childSpec.from(this.buildFilteredLibraryProvider(project, this.buildLocalArtifactPredicate(project)
193+
.negate()
194+
.and(this.getOrgLibPredicate(project)
195+
.negate()))));
190196
spec.into(DOCKER_BUILD_CONTEXT_CLASSES_DIR, childSpec -> childSpec.from(mainSourceSetOutput(project).getClassesDirs()));
191197
spec.into(DOCKER_BUILD_CONTEXT_RESOURCES_DIR, childSpec -> childSpec.from(mainSourceSetOutput(project).getResourcesDir()));
192198
}));
193199
});
194200
}
195201

196-
private FileCollection getRuntimeClasspath(Project project) {
202+
private Predicate<ResolvedArtifact> buildLocalArtifactPredicate(Project project) {
203+
// Either matching version or missing version indicating a composite project
204+
return artifact -> {
205+
String artifactVersion = artifact.getModuleVersion()
206+
.getId()
207+
.getVersion();
208+
return artifactVersion.equals(project.getVersion()
209+
.toString()) || artifactVersion.equals("unspecified");
210+
};
211+
}
212+
213+
private Configuration getRuntimeClasspath(Project project) {
197214
return project.getConfigurations()
198215
.getByName(RUNTIME_CLASSPATH_CONFIGURATION_NAME);
199216
}
@@ -218,4 +235,18 @@ private TextResource getStartScriptTemplate(Project project) {
218235
.getText()
219236
.fromUri(requireNonNull(resourceUrl));
220237
}
238+
239+
private Provider<Collection<File>> buildFilteredLibraryProvider(Project project, Predicate<ResolvedArtifact> filterPredicate) {
240+
return project.provider(() -> this.getRuntimeClasspath(project)
241+
.getResolvedConfiguration()
242+
.getResolvedArtifacts()
243+
.stream()
244+
.filter(filterPredicate)
245+
.map(ResolvedArtifact::getFile)
246+
.collect(Collectors.toSet()));
247+
}
248+
249+
private Predicate<ResolvedArtifact> getOrgLibPredicate(Project project) {
250+
return this.getHypertraceDockerApplicationExtension(project).orgLibrarySpec::isSatisfiedBy;
251+
}
221252
}

hypertrace-gradle-docker-java-application-plugin/src/main/resources/application-start-script.template.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ set -e
2525

2626
DEFAULT_JVM_OPTS=${defaultJvmOpts}
2727

28-
exec java \$DEFAULT_JVM_OPTS \$JAVA_OPTS -classpath '/app/resources:/app/classes:/app/localLibs/*:/app/externalLibs/*' ${mainClassName} \$@
28+
exec java \$DEFAULT_JVM_OPTS \$JAVA_OPTS -classpath '/app/resources:/app/classes:/app/localLibs/*:/app/orgLibs/*:/app/externalLibs/*' ${mainClassName} \$@

0 commit comments

Comments
 (0)