17
17
package org .springframework .boot .loader .thin ;
18
18
19
19
import java .io .File ;
20
+ import java .io .FileOutputStream ;
20
21
import java .net .URL ;
21
22
import java .security .AccessControlException ;
22
23
import java .util .ArrayList ;
35
36
import org .springframework .core .env .MutablePropertySources ;
36
37
import org .springframework .core .env .SimpleCommandLinePropertySource ;
37
38
import org .springframework .core .env .StandardEnvironment ;
39
+ import org .springframework .util .FileCopyUtils ;
38
40
import org .springframework .util .StringUtils ;
39
41
40
42
import ch .qos .logback .classic .Level ;
@@ -131,8 +133,15 @@ public class ThinJarLauncher extends ExecutableArchiveLauncher {
131
133
*/
132
134
public static final String THIN_PARENT_BOOT = "thin.parent.boot" ;
133
135
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
+
134
142
private StandardEnvironment environment = new StandardEnvironment ();
135
143
private boolean debug ;
144
+ private String root = "" ;
136
145
137
146
public static void main (String [] args ) throws Exception {
138
147
LogUtils .setLogLevel (Level .OFF );
@@ -147,7 +156,7 @@ protected ThinJarLauncher(String[] args) throws Exception {
147
156
protected void launch (String [] args ) throws Exception {
148
157
addCommandLineProperties (args );
149
158
args = removeThinArgs (args );
150
- String root = environment .resolvePlaceholders ("${" + THIN_ROOT + ":}" );
159
+ root = environment .resolvePlaceholders ("${" + THIN_ROOT + ":}" );
151
160
boolean classpath = !"false" .equals (
152
161
environment .resolvePlaceholders ("${" + THIN_CLASSPATH + ":false}" ));
153
162
boolean compute = !"false"
@@ -181,6 +190,13 @@ protected void launch(String[] args) throws Exception {
181
190
return ;
182
191
}
183
192
log .info ("Version: " + getVersion ());
193
+ if (!"false" .equals (
194
+ environment .resolvePlaceholders ("${" + THIN_DOCKER + ":false}" ))) {
195
+
196
+ createDockerBuild ();
197
+
198
+ return ;
199
+ }
184
200
if (!"false" .equals (
185
201
environment .resolvePlaceholders ("${" + THIN_DRYRUN + ":false}" ))) {
186
202
List <Archive > archives = getClassPathArchives ();
@@ -245,19 +261,115 @@ private String classpath(List<Archive> archives) throws Exception {
245
261
builder .append (separator );
246
262
}
247
263
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 ) {
248
313
String uri = archive .getUrl ().toURI ().toString ();
249
314
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
+ }
254
321
}
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 );
257
331
}
258
- builder .append (new File (uri ).getCanonicalPath ());
259
332
}
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 ();
261
373
}
262
374
263
375
private void addCommandLineProperties (String [] args ) {
@@ -339,7 +451,10 @@ protected List<Dependency> getDependencies() throws Exception {
339
451
private PathResolver getResolver () {
340
452
String locations = environment
341
453
.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
+ }
343
458
String offline = environment .resolvePlaceholders ("${" + THIN_OFFLINE + ":false}" );
344
459
PathResolver resolver = new PathResolver (DependencyResolver .instance ());
345
460
if (StringUtils .hasText (locations )) {
0 commit comments