Skip to content

Commit a740b97

Browse files
committed
dsyer/spring-boot-thin-launcher#25 Generate Docker build
- generate .m2/repository containing all dependencies based on their GAV coordinates - add application jar to .m2/ - create Dockerfile with seperate ADD commands for all dependencies and the application jar, classpath ENV, and CMD
1 parent 7804338 commit a740b97

File tree

2 files changed

+136
-10
lines changed

2 files changed

+136
-10
lines changed

Diff for: README.md

+11
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ You can set a variety of options on the command line with system properties (`-D
248248
| Option | Default | Description |
249249
|--------|---------|-------------|
250250
| `thin.main` | Start-Class in MANIFEST.MF| The main class to launch (for a Spring Boot app, usually the one with `@SpringBootApplication`)|
251+
| `thin.docker` | false | Prepare a Docker build by creating a Dockerfile in the local directory and including all dependencies as separate Docker layers. |
251252
| `thin.dryrun` | false | Only resolve and download the dependencies. Don't run any main class. N.B. any value other than "false" (even empty) is true. |
252253
| `thin.offline` | false | Switch to "offline" mode. All dependencies must be avalailable locally (e.g. via a previous dry run) or there will be an exception. |
253254
| `thin.classpath` | false | Only print the classpath. Don't run and main class. N.B. any value other than "false" (even empty) is true. |
@@ -292,6 +293,16 @@ ENTRYPOINT [ "sh", "-c", "java -Djava.security.egd=file:/dev/./urandom -jar app.
292293
EXPOSE 8080
293294
```
294295

296+
### How to Create a complete Docker build
297+
298+
Creates Dockerfile and `.m2/` directory with all runtime dependencies in current directory.
299+
Dockerfile will contain all dependencies and application jar as separate layers (will improve build and shipment)
300+
301+
```
302+
$ java -jar app.jar --thin.docker
303+
$ docker build -t myname/myapp:myversion .
304+
```
305+
295306
## Building
296307

297308
To build this project locally, use the maven wrapper in the top level

Diff for: launcher/src/main/java/org/springframework/boot/loader/thin/ThinJarLauncher.java

+125-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.loader.thin;
1818

1919
import java.io.File;
20+
import java.io.FileOutputStream;
2021
import java.net.URL;
2122
import java.security.AccessControlException;
2223
import java.util.ArrayList;
@@ -35,6 +36,7 @@
3536
import org.springframework.core.env.MutablePropertySources;
3637
import org.springframework.core.env.SimpleCommandLinePropertySource;
3738
import org.springframework.core.env.StandardEnvironment;
39+
import org.springframework.util.FileCopyUtils;
3840
import org.springframework.util.StringUtils;
3941

4042
import ch.qos.logback.classic.Level;
@@ -131,8 +133,15 @@ public class ThinJarLauncher extends ExecutableArchiveLauncher {
131133
*/
132134
public static final String THIN_PARENT_BOOT = "thin.parent.boot";
133135

136+
/**
137+
* Flag to say that a Docker build should be created, i.e., .m2 with repository and appropriate Dockerfile.
138+
* Default false.
139+
*/
140+
public static final String THIN_DOCKER = "thin.docker";
141+
134142
private StandardEnvironment environment = new StandardEnvironment();
135143
private boolean debug;
144+
private String root = "";
136145

137146
public static void main(String[] args) throws Exception {
138147
LogUtils.setLogLevel(Level.OFF);
@@ -147,7 +156,7 @@ protected ThinJarLauncher(String[] args) throws Exception {
147156
protected void launch(String[] args) throws Exception {
148157
addCommandLineProperties(args);
149158
args = removeThinArgs(args);
150-
String root = environment.resolvePlaceholders("${" + THIN_ROOT + ":}");
159+
root = environment.resolvePlaceholders("${" + THIN_ROOT + ":}");
151160
boolean classpath = !"false".equals(
152161
environment.resolvePlaceholders("${" + THIN_CLASSPATH + ":false}"));
153162
boolean compute = !"false"
@@ -181,6 +190,13 @@ protected void launch(String[] args) throws Exception {
181190
return;
182191
}
183192
log.info("Version: " + getVersion());
193+
if (!"false".equals(
194+
environment.resolvePlaceholders("${" + THIN_DOCKER + ":false}"))) {
195+
196+
createDockerBuild();
197+
198+
return;
199+
}
184200
if (!"false".equals(
185201
environment.resolvePlaceholders("${" + THIN_DRYRUN + ":false}"))) {
186202
List<Archive> archives = getClassPathArchives();
@@ -245,19 +261,115 @@ private String classpath(List<Archive> archives) throws Exception {
245261
builder.append(separator);
246262
}
247263
log.info("Archive: {}", archive);
264+
String uri = archive.getUrl().toURI().toString();
265+
uri = cutFileAndInternalJarRootFromUri(uri);
266+
builder.append(new File(uri).getCanonicalPath());
267+
}
268+
return builder.toString();
269+
}
270+
271+
private String cutFileAndInternalJarRootFromUri(String uri) {
272+
if (uri.startsWith("jar:")) {
273+
uri = uri.substring("jar:".length());
274+
}
275+
if (uri.startsWith("file:")) {
276+
uri = uri.substring("file:".length());
277+
}
278+
if (uri.endsWith("!/")) {
279+
uri = uri.substring(0, uri.length() - "!/".length());
280+
}
281+
return uri;
282+
}
283+
284+
// TODO: Add some more options, e.g.,
285+
// thin.docker.file for the path to the docker file
286+
// thin.docker.base for the base image,
287+
// ...
288+
private void createDockerBuild() throws Exception {
289+
// FIXME Find a better name for the root since it not (only) contains a Maven kind of repository
290+
final String dockerM2 = ".m2";
291+
if (StringUtils.hasText(root)) {
292+
log.warn ("Overriding current 'thin.root' ('{}') by Docker Maven root '{}' for Dockerfile", root, dockerM2);
293+
} else {
294+
log.info ("Using '{}' as Docker Maven root", dockerM2);
295+
}
296+
root = dockerM2;
297+
298+
List<Archive> archives = getClassPathArchives();
299+
300+
StringBuilder builder = new StringBuilder();
301+
302+
// TODO: Make the base image configurable
303+
builder.append ("FROM openjdk:8-alpine\n" +
304+
"\n" +
305+
"# The dependencies\n");
306+
307+
File rootDir = new File (root);
308+
int rootDirPathlen = rootDir.getAbsolutePath().length();
309+
String mainJar = null;
310+
String separator = System.getProperty("path.separator");
311+
StringBuilder classpath = new StringBuilder();
312+
for (Archive archive : archives) {
248313
String uri = archive.getUrl().toURI().toString();
249314
if (uri.startsWith("jar:")) {
250-
uri = uri.substring("jar:".length());
251-
}
252-
if (uri.startsWith("file:")) {
253-
uri = uri.substring("file:".length());
315+
if (null != mainJar) {
316+
log.warn("Cannot ADD another main JAR '{}' to Dockerfile (already have '{}')", uri, mainJar);
317+
} else {
318+
uri = cutFileAndInternalJarRootFromUri(uri);
319+
mainJar = uri;
320+
}
254321
}
255-
if (uri.endsWith("!/")) {
256-
uri = uri.substring(0, uri.length() - "!/".length());
322+
else {
323+
uri = cutFileAndInternalJarRootFromUri(uri);
324+
uri = uri.substring(rootDirPathlen);
325+
String fullUri = root + uri;
326+
builder.append ("ADD " + fullUri + " /" + fullUri + "\n");
327+
if (classpath.length() != 0) {
328+
classpath.append(separator);
329+
}
330+
classpath.append("/" + fullUri);
257331
}
258-
builder.append(new File(uri).getCanonicalPath());
259332
}
260-
return builder.toString();
333+
334+
if (null == mainJar) {
335+
throw new RuntimeException ("There is no main jar defined");
336+
}
337+
338+
String mainJarBasename = new File (mainJar).getName();
339+
String mainJarTarget = root + "/" + mainJarBasename;
340+
341+
// This is a hack to get the Jar file into the Docker build (can we do it better?)
342+
log.debug ("Copying application Jar '{}' to '{}'", mainJar, mainJarTarget);
343+
File mainJarIn = new File(mainJar);
344+
File mainJarOut = new File (mainJarTarget);
345+
FileCopyUtils.copy(mainJarIn, mainJarOut);
346+
347+
builder.append ("\n" +
348+
"EXPOSE 8080" +
349+
"\n" +
350+
"ENV CLASSPATH=/" +mainJarTarget + separator + classpath.toString() +
351+
"\n\n" +
352+
// Let the Application be the latest Docker layer
353+
"# The Spring Boot Application\n" +
354+
"ADD " + mainJarTarget + " /" + mainJarTarget + "\n" +
355+
"\n" +
356+
// TODO: Set Spring/Thin profile(s) as provided by THIN_PROFILE property
357+
// "CMD [ \"sh\", \"-c\", \"java -Djava.security.egd=file:/dev/./urandom ${JAVA_OPTS} -jar "
358+
// + mainJarBasename + " --thin.root=/" + root + "\" ${MAIN_ARGS} ]\n"
359+
"\n"+
360+
"CMD [ \"sh\", \"-c\", \"java -Djava.security.egd=file:/dev/./urandom ${JAVA_OPTS} " +
361+
getMainClass() + " ${MAIN_ARGS}\" ]\n"
362+
);
363+
File dockerfile = new File ("Dockerfile");
364+
if (dockerfile.exists()) {
365+
log.warn ("Overriding existing Dockerfile");
366+
dockerfile.delete();
367+
} else {
368+
log.info ("Creating Dockerfile");
369+
}
370+
FileOutputStream dockerstream = new FileOutputStream(dockerfile);
371+
dockerstream.write(builder.toString().getBytes());
372+
dockerstream.close();
261373
}
262374

263375
private void addCommandLineProperties(String[] args) {
@@ -339,7 +451,10 @@ protected List<Dependency> getDependencies() throws Exception {
339451
private PathResolver getResolver() {
340452
String locations = environment
341453
.resolvePlaceholders("${" + ThinJarLauncher.THIN_LOCATION + ":}");
342-
String root = environment.resolvePlaceholders("${" + THIN_ROOT + ":}");
454+
if (!StringUtils.hasText(root)) {
455+
// root might already be set, e.g., by overriding it for Docker build generator
456+
root = environment.resolvePlaceholders("${" + THIN_ROOT + ":}");
457+
}
343458
String offline = environment.resolvePlaceholders("${" + THIN_OFFLINE + ":false}");
344459
PathResolver resolver = new PathResolver(DependencyResolver.instance());
345460
if (StringUtils.hasText(locations)) {

0 commit comments

Comments
 (0)