View Javadoc
1   /*
2    * Copyright 2017-2022 original authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package io.micronaut.build;
17  
18  import io.micronaut.build.jib.JibMicronautExtension;
19  import io.micronaut.build.services.ApplicationConfigurationService;
20  import io.micronaut.build.services.DockerService;
21  import io.micronaut.build.services.JibConfigurationService;
22  import org.apache.maven.plugin.MojoExecutionException;
23  import org.apache.maven.plugins.annotations.Mojo;
24  import org.apache.maven.plugins.annotations.ResolutionScope;
25  import org.apache.maven.project.MavenProject;
26  
27  import javax.inject.Inject;
28  import java.io.File;
29  import java.io.IOException;
30  import java.nio.file.Files;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Optional;
34  import java.util.stream.Collectors;
35  
36  /**
37   * <p>Generates a <code>Dockerfile</code> depending on the <code>packaging</code> and <code>micronaut.runtime</code>
38   * properties.
39   *
40   * <pre>mvn mn:dockerfile -Dpackaging=docker-native -Dmicronaut.runtime=lambda</pre>
41   *
42   * @author Álvaro Sánchez-Mariscal
43   * @since 1.1
44   */
45  @Mojo(name = "dockerfile", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
46  public class DockerfileMojo extends AbstractDockerMojo {
47  
48      public static final String DOCKERFILE = "Dockerfile";
49      public static final String DOCKERFILE_AWS_CUSTOM_RUNTIME = "DockerfileAwsCustomRuntime";
50      public static final String DOCKERFILE_NATIVE = "DockerfileNative";
51      public static final String DOCKERFILE_CRAC = "DockerfileCrac";
52      public static final String DOCKERFILE_CRAC_CHECKPOINT = "DockerfileCracCheckpoint";
53      public static final String DOCKERFILE_CRAC_CHECKPOINT_FILE = "Dockerfile.crac.checkpoint";
54      public static final String DOCKERFILE_NATIVE_DISTROLESS = "DockerfileNativeDistroless";
55      public static final String DOCKERFILE_NATIVE_STATIC = "DockerfileNativeStatic";
56      public static final String DOCKERFILE_NATIVE_ORACLE_CLOUD = "DockerfileNativeOracleCloud";
57  
58      @Inject
59      public DockerfileMojo(MavenProject mavenProject, DockerService dockerService, JibConfigurationService jibConfigurationService,
60                            ApplicationConfigurationService applicationConfigurationService) {
61          super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService);
62      }
63  
64      @Override
65      public void execute() throws MojoExecutionException {
66          MicronautRuntime runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase());
67          Packaging packaging = Packaging.of(mavenProject.getPackaging());
68          try {
69              copyDependencies();
70              Optional<File> dockerfile;
71  
72              switch (packaging) {
73                  case DOCKER_NATIVE:
74                      dockerfile = buildDockerfileNative(runtime);
75                      break;
76  
77                  case DOCKER:
78                      dockerfile = buildDockerfile(runtime);
79                      break;
80  
81                  case DOCKER_CRAC:
82                      dockerfile = buildCracDockerfile(runtime);
83                      break;
84  
85                  default:
86                      throw new MojoExecutionException("Packaging is set to [" + packaging + "]. To generate a Dockerfile, set the packaging to either [" + Packaging.DOCKER.id() + "] or [" + Packaging.DOCKER_NATIVE.id() + "]");
87              }
88  
89              dockerfile.ifPresent(file -> getLog().info("Dockerfile written to: " + file.getAbsolutePath()));
90  
91          } catch (IOException e) {
92              throw new MojoExecutionException(e.getMessage(), e);
93          }
94      }
95  
96      private Optional<File> buildDockerfile(MicronautRuntime runtime) throws IOException {
97          File dockerfile;
98          switch (runtime.getBuildStrategy()) {
99              case ORACLE_FUNCTION:
100                 dockerfile = dockerService.loadDockerfileAsResource("DockerfileOracleCloud");
101                 processOracleFunctionDockerfile(dockerfile);
102                 break;
103             case LAMBDA:
104             case DEFAULT:
105             default:
106                 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE);
107                 processDockerfile(dockerfile);
108                 break;
109         }
110         return Optional.ofNullable(dockerfile);
111     }
112 
113     private Optional<File> buildCracDockerfile(MicronautRuntime runtime) throws IOException, MojoExecutionException {
114         File dockerfile;
115         switch (runtime.getBuildStrategy()) {
116             case ORACLE_FUNCTION:
117                 throw new MojoExecutionException("Oracle Functions are currently unsupported");
118             case LAMBDA:
119                 throw new MojoExecutionException("Lambda Functions are currently unsupported");
120             case DEFAULT:
121             default:
122                 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC_CHECKPOINT, DOCKERFILE_CRAC_CHECKPOINT_FILE);
123                 processDockerfile(dockerfile);
124                 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_CRAC);
125                 processDockerfile(dockerfile);
126                 break;
127         }
128         return Optional.ofNullable(dockerfile);
129     }
130 
131     private void processOracleFunctionDockerfile(File dockerfile) throws IOException {
132         if (dockerfile != null) {
133             List<String> allLines = Files.readAllLines(dockerfile.toPath());
134             String projectFnVersion = JibMicronautExtension.determineProjectFnVersion(System.getProperty("java.version"));
135             allLines.add(0, allLines.remove(0) + projectFnVersion);
136             String entrypoint = JibMicronautExtension.buildProjectFnEntrypoint(projectFnVersion)
137                     .stream()
138                     .map(s -> "\"" + s + "\"")
139                     .collect(Collectors.joining(", "));
140 
141             allLines.add("ENTRYPOINT [" + entrypoint + "]");
142 
143             Files.write(dockerfile.toPath(), allLines);
144         }
145     }
146 
147     private Optional<File> buildDockerfileNative(MicronautRuntime runtime) throws IOException {
148         File dockerfile;
149         switch (runtime.getBuildStrategy()) {
150             case LAMBDA:
151                 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_AWS_CUSTOM_RUNTIME);
152                 break;
153 
154             case ORACLE_FUNCTION:
155                 dockerfile = dockerService.loadDockerfileAsResource(DOCKERFILE_NATIVE_ORACLE_CLOUD);
156                 break;
157 
158             case DEFAULT:
159             default:
160                 String dockerfileName = DOCKERFILE_NATIVE;
161                 if (staticNativeImage) {
162                     getLog().info("Generating a static native image");
163                     dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_STATIC;
164                 } else if (baseImageRun.contains("distroless")) {
165                     getLog().info("Generating a mostly static native image");
166                     dockerfileName = DockerfileMojo.DOCKERFILE_NATIVE_DISTROLESS;
167                 }
168                 dockerfile = dockerService.loadDockerfileAsResource(dockerfileName);
169                 break;
170         }
171         processDockerfile(dockerfile);
172         return Optional.ofNullable(dockerfile);
173 
174     }
175 
176     private void processDockerfile(File dockerfile) throws IOException {
177         if (dockerfile != null) {
178             List<String> allLines = Files.readAllLines(dockerfile.toPath());
179             List<String> result = new ArrayList<>();
180 
181             for (String line : allLines) {
182                 if (!line.startsWith("ARG")) {
183                     if (line.contains("BASE_IMAGE_RUN")) {
184                         result.add(line.replace("${BASE_IMAGE_RUN}", baseImageRun));
185                     } else if (line.contains("BASE_IMAGE")) {
186                         result.add(line.replace("${BASE_IMAGE}", getFrom()));
187                     } else if (line.contains("EXTRA_CMD")) {
188                         if (baseImageRun.contains("alpine-glibc")) {
189                             result.add("RUN apk update && apk add libstdc++");
190                         } else {
191                             result.add("");
192                         }
193                     } else if (line.contains("GRAALVM_") || line.contains("CLASS_NAME")) {
194                         String graalVmBuildArgs = getGraalVmBuildArgs();
195                         if (baseImageRun.contains("distroless") && !graalVmBuildArgs.contains(MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG)) {
196                             graalVmBuildArgs = MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG + " " + graalVmBuildArgs;
197                         }
198 
199                         result.add(line
200                                 .replace("${GRAALVM_VERSION}", graalVmVersion())
201                                 .replace("${GRAALVM_JVM_VERSION}", graalVmJvmVersion())
202                                 .replace("${GRAALVM_ARCH}", graalVmArch())
203                                 .replace("${GRAALVM_ARGS}", graalVmBuildArgs)
204                                 .replace("${CLASS_NAME}", mainClass)
205                         );
206                     } else if (line.contains("PORT")) {
207                         result.add(line.replace("${PORT}", getPort()));
208                     } else {
209                         result.add(line);
210                     }
211                 }
212             }
213 
214             if (appArguments != null && !appArguments.isEmpty()) {
215                 getLog().info("Using application arguments: " + appArguments);
216                 result.add(getCmd());
217             }
218 
219             Files.write(dockerfile.toPath(), result);
220         }
221     }
222 }