001/* 002 * Copyright 2017-2022 original authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.micronaut.maven.jib; 017 018import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; 019import com.google.cloud.tools.jib.api.buildplan.ContainerBuildPlan; 020import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; 021import com.google.cloud.tools.jib.api.buildplan.FileEntry; 022import com.google.cloud.tools.jib.api.buildplan.LayerObject; 023import com.google.cloud.tools.jib.api.buildplan.Platform; 024import com.google.cloud.tools.jib.api.buildplan.Port; 025import com.google.cloud.tools.jib.buildplan.UnixPathParser; 026import com.google.cloud.tools.jib.maven.extension.JibMavenPluginExtension; 027import com.google.cloud.tools.jib.maven.extension.MavenData; 028import com.google.cloud.tools.jib.plugins.extension.ExtensionLogger; 029import com.google.cloud.tools.jib.plugins.extension.JibPluginExtensionException; 030import io.micronaut.core.util.StringUtils; 031import io.micronaut.maven.core.DockerBuildStrategy; 032import io.micronaut.maven.core.MicronautRuntime; 033import io.micronaut.maven.services.ApplicationConfigurationService; 034import org.apache.maven.execution.MavenSession; 035import org.apache.maven.plugin.MojoExecution; 036import org.apache.maven.plugin.PluginParameterExpressionEvaluator; 037import org.apache.maven.plugin.descriptor.MojoDescriptor; 038import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042import java.util.ArrayList; 043import java.util.Collections; 044import java.util.List; 045import java.util.Map; 046import java.util.Objects; 047import java.util.Optional; 048 049/** 050 * Jib extension to support building Docker images. 051 * 052 * @author Álvaro Sánchez-Mariscal 053 * @since 1.1 054 */ 055public class JibMicronautExtension implements JibMavenPluginExtension<Void> { 056 057 private static final int SUPPORTED_JDK_VERSION = 25; 058 public static final String DEFAULT_JAVA25_BASE_IMAGE = "eclipse-temurin:" + SUPPORTED_JDK_VERSION + "-jre"; 059 private static final String LATEST_TAG = "latest"; 060 private static final String JDK_TARGET_VERSION = "maven.compiler.target"; 061 private static final String JDK_RELEASE_VERSION = "maven.compiler.release"; 062 private static final String JDK_SOURCE_VERSION = "maven.compiler.source"; 063 private static final Logger LOG = LoggerFactory.getLogger(JibMicronautExtension.class); 064 private static final String LINUX = "linux"; 065 066 @Override 067 public Optional<Class<Void>> getExtraConfigType() { 068 return Optional.empty(); 069 } 070 071 @Override 072 public ContainerBuildPlan extendContainerBuildPlan(ContainerBuildPlan buildPlan, Map<String, String> properties, 073 Optional<Void> extraConfig, MavenData mavenData, 074 ExtensionLogger logger) throws JibPluginExtensionException { 075 076 ContainerBuildPlan.Builder builder = buildPlan.toBuilder(); 077 MicronautRuntime runtime = MicronautRuntime.valueOf(mavenData.getMavenProject().getProperties().getProperty(MicronautRuntime.PROPERTY, "none").toUpperCase()); 078 079 var jibConfigurationService = new JibConfigurationService(mavenData.getMavenProject()); 080 081 String baseImage = buildPlan.getBaseImage(); 082 if (StringUtils.isEmpty(buildPlan.getBaseImage())) { 083 baseImage = determineBaseImage(getJdkVersion(mavenData.getMavenSession()), runtime.getBuildStrategy()); 084 builder.setBaseImage(baseImage); 085 } 086 logger.log(ExtensionLogger.LogLevel.LIFECYCLE, "Using base image: " + baseImage); 087 088 if (buildPlan.getExposedPorts() == null || buildPlan.getExposedPorts().isEmpty()) { 089 var applicationConfigurationService = new ApplicationConfigurationService(mavenData.getMavenProject()); 090 try { 091 int port = Integer.parseInt(applicationConfigurationService.getServerPort()); 092 if (port > 0) { 093 logger.log(ExtensionLogger.LogLevel.LIFECYCLE, "Exposing port: " + port); 094 builder.addExposedPort(Port.tcp(port)); 095 } 096 } catch (NumberFormatException e) { 097 // ignore, can't automatically expose port 098 logger.log(ExtensionLogger.LogLevel.LIFECYCLE, "Dynamically resolved port present. Ensure the port is correctly exposed in the <container> configuration. See https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#example for an example."); 099 } 100 } 101 102 configurePlatforms(buildPlan, baseImage, builder, runtime, mavenData, logger); 103 104 switch (runtime.getBuildStrategy()) { 105 case ORACLE_FUNCTION -> { 106 var originalLayers = buildPlan.getLayers(); 107 builder.setLayers(originalLayers.stream().map(JibMicronautExtension::remapLayer).toList()); 108 var cmd = jibConfigurationService.getArgs(); 109 if (cmd.isEmpty()) { 110 cmd = Collections.singletonList("io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest"); 111 } 112 builder.setWorkingDirectory(AbsoluteUnixPath.get(jibConfigurationService.getWorkingDirectory().orElse("/function"))) 113 .setEntrypoint(buildProjectFnEntrypoint()) 114 .setCmd(cmd); 115 } 116 case LAMBDA -> { 117 //TODO Leverage AWS Base images: 118 // https://docs.aws.amazon.com/lambda/latest/dg/java-image.html 119 // https://docs.aws.amazon.com/lambda/latest/dg/images-create.html 120 // https://docs.aws.amazon.com/lambda/latest/dg/images-test.html 121 var entrypoint = buildPlan.getEntrypoint(); 122 Objects.requireNonNull(entrypoint).set(entrypoint.size() - 1, "io.micronaut.function.aws.runtime.MicronautLambdaRuntime"); 123 builder.setEntrypoint(entrypoint); 124 } 125 default -> { 126 //no op 127 } 128 } 129 return builder.build(); 130 } 131 132 /** 133 * Builds for the Docker daemon's platform only when the {@code docker} goal trains a JDK AOT cache, and adds the 134 * detected platform otherwise. 135 */ 136 private void configurePlatforms(ContainerBuildPlan buildPlan, String baseImage, ContainerBuildPlan.Builder builder, 137 MicronautRuntime runtime, MavenData mavenData, ExtensionLogger logger) 138 throws JibPluginExtensionException { 139 Optional<Platform> jdkAotCachePlatform = JdkAotCachePlan.platform(mavenData); 140 if (jdkAotCachePlatform.isPresent()) { 141 if (runtime.getBuildStrategy() != DockerBuildStrategy.DEFAULT) { 142 throw new JibPluginExtensionException(JibMicronautExtension.class, 143 "micronaut.docker.jdkAotCache only supports the default runtime, not " + runtime.getBuildStrategy()); 144 } 145 JdkAotCachePlan.apply(buildPlan, baseImage, builder, jdkAotCachePlatform.get(), mavenData, logger); 146 return; 147 } 148 var detectedPlatform = detectPlatform(); 149 if (buildPlan.getPlatforms() == null || buildPlan.getPlatforms().isEmpty() || !buildPlan.getPlatforms().contains(detectedPlatform)) { 150 LOG.info("Adding Detected platform: {}/{}", LINUX, detectedPlatform.getArchitecture()); 151 builder.addPlatform(detectedPlatform.getArchitecture(), LINUX); 152 } 153 } 154 155 public static List<String> buildProjectFnEntrypoint() { 156 var entrypoint = new ArrayList<String>(9); 157 entrypoint.add("java"); 158 entrypoint.add("-XX:-UsePerfData"); 159 entrypoint.add("-XX:+UseSerialGC"); 160 entrypoint.add("-Xshare:auto"); 161 entrypoint.add("-Djava.awt.headless=true"); 162 entrypoint.add("-Djava.library.path=/function/runtime/lib"); 163 entrypoint.add("-cp"); 164 entrypoint.add("/function/app/classes:/function/app/libs/release/*:/function/app/libs/snapshot/*:/function/app/resources:/function/runtime/*"); 165 entrypoint.add("com.fnproject.fn.runtime.EntryPoint"); 166 return entrypoint; 167 } 168 169 public static String determineProjectFnVersion(String javaVersion) { 170 int majorVersion = Integer.parseInt(javaVersion.split("\\.")[0]); 171 if (majorVersion >= 25) { 172 return "25-jre"; 173 } else { 174 return LATEST_TAG; 175 } 176 } 177 178 public static String determineBaseImage(String jdkVersion, DockerBuildStrategy buildStrategy) { 179 int javaVersion = Integer.parseInt(jdkVersion); 180 return switch (buildStrategy) { 181 case LAMBDA -> "public.ecr.aws/lambda/java:" + javaVersion; 182 default -> { 183 if (javaVersion > SUPPORTED_JDK_VERSION) { 184 throw new IllegalArgumentException("Unsupported JDK version for Docker base image: " + javaVersion 185 + ". Maximum supported version is " + SUPPORTED_JDK_VERSION + "."); 186 } 187 yield DEFAULT_JAVA25_BASE_IMAGE; 188 } 189 }; 190 } 191 192 public static String getJdkVersion(MavenSession session) { 193 var releaseVersion = getPropertyValue(session, JDK_RELEASE_VERSION); 194 var targetVersion = getPropertyValue(session, JDK_TARGET_VERSION); 195 var sourceVersion = getPropertyValue(session, JDK_SOURCE_VERSION); 196 197 Optional<String> jdkVersionOpt = releaseVersion 198 .or(() -> targetVersion) 199 .or(() -> sourceVersion); 200 201 String jdkVersion = jdkVersionOpt.orElse(String.valueOf(SUPPORTED_JDK_VERSION)); 202 String propertySource = releaseVersion.isPresent() ? JDK_RELEASE_VERSION : 203 targetVersion.isPresent() ? JDK_TARGET_VERSION : 204 sourceVersion.isPresent() ? JDK_SOURCE_VERSION : "default (" + SUPPORTED_JDK_VERSION + ")"; 205 206 LOG.info("Using JDK version {} from {}", jdkVersion, propertySource); 207 return jdkVersion; 208 } 209 210 private static Optional<String> getPropertyValue(MavenSession session, String propertName) { 211 MojoExecution mojoExecution = new MojoExecution(new MojoDescriptor()); 212 var evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); 213 try { 214 return Optional.ofNullable((String) evaluator.evaluate("${" + propertName + "}", String.class)); 215 } catch (ExpressionEvaluationException e) { 216 return Optional.empty(); 217 } 218 } 219 220 static LayerObject remapLayer(LayerObject layerObject) { 221 var originalLayer = (FileEntriesLayer) layerObject; 222 FileEntriesLayer.Builder builder = FileEntriesLayer.builder().setName(originalLayer.getName()); 223 for (FileEntry originalEntry : originalLayer.getEntries()) { 224 builder.addEntry(remapEntry(originalEntry, layerObject.getName())); 225 } 226 227 return builder.build(); 228 } 229 230 static FileEntry remapEntry(FileEntry originalEntry, String layerName) { 231 var pathComponents = UnixPathParser.parse(originalEntry.getExtractionPath().toString()); 232 AbsoluteUnixPath newPath; 233 if ("dependencies".equals(layerName)) { 234 newPath = AbsoluteUnixPath.get("/function/app/libs/release/" + pathComponents.get(pathComponents.size() - 1)); 235 } else if ("snapshot-dependencies".equals(layerName)) { 236 newPath = AbsoluteUnixPath.get("/function/app/libs/snapshot/" + pathComponents.get(pathComponents.size() - 1)); 237 } else { 238 //classes or resources 239 newPath = AbsoluteUnixPath.get("/function" + originalEntry.getExtractionPath()); 240 } 241 242 return new FileEntry(originalEntry.getSourceFile(), newPath, originalEntry.getPermissions(), 243 originalEntry.getModificationTime(), originalEntry.getOwnership()); 244 } 245 246 private Platform detectPlatform() { 247 String osArchitecture = System.getProperty("os.arch"); 248 String arch = "aarch64".equals(osArchitecture) || "arm64".equals(osArchitecture) ? "arm64" : "amd64"; 249 return new Platform(arch, LINUX); 250 } 251 252}