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 io.micronaut.core.util.StringUtils; 030import io.micronaut.maven.core.DockerBuildStrategy; 031import io.micronaut.maven.core.MicronautRuntime; 032import io.micronaut.maven.services.ApplicationConfigurationService; 033import org.apache.maven.execution.MavenSession; 034import org.apache.maven.plugin.MojoExecution; 035import org.apache.maven.plugin.PluginParameterExpressionEvaluator; 036import org.apache.maven.plugin.descriptor.MojoDescriptor; 037import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.List; 044import java.util.Map; 045import java.util.Objects; 046import java.util.Optional; 047 048/** 049 * Jib extension to support building Docker images. 050 * 051 * @author Álvaro Sánchez-Mariscal 052 * @since 1.1 053 */ 054public class JibMicronautExtension implements JibMavenPluginExtension<Void> { 055 056 public static final String DEFAULT_JAVA21_BASE_IMAGE = "eclipse-temurin:21-jre"; 057 public static final String DEFAULT_JAVA25_BASE_IMAGE = "eclipse-temurin:25-jre"; 058 private static final String LATEST_TAG = "latest"; 059 private static final String JDK_TARGET_VERSION = "maven.compiler.target"; 060 private static final String JDK_RELEASE_VERSION = "maven.compiler.release"; 061 private static final String JDK_SOURCE_VERSION = "maven.compiler.source"; 062 private static final Logger LOG = LoggerFactory.getLogger(JibMicronautExtension.class); 063 private static final String LINUX = "linux"; 064 065 @Override 066 public Optional<Class<Void>> getExtraConfigType() { 067 return Optional.empty(); 068 } 069 070 @Override 071 public ContainerBuildPlan extendContainerBuildPlan(ContainerBuildPlan buildPlan, Map<String, String> properties, 072 Optional<Void> extraConfig, MavenData mavenData, 073 ExtensionLogger logger) { 074 075 ContainerBuildPlan.Builder builder = buildPlan.toBuilder(); 076 MicronautRuntime runtime = MicronautRuntime.valueOf(mavenData.getMavenProject().getProperties().getProperty(MicronautRuntime.PROPERTY, "none").toUpperCase()); 077 078 var jibConfigurationService = new JibConfigurationService(mavenData.getMavenProject()); 079 080 String baseImage = buildPlan.getBaseImage(); 081 if (StringUtils.isEmpty(buildPlan.getBaseImage())) { 082 baseImage = determineBaseImage(getJdkVersion(mavenData.getMavenSession()), runtime.getBuildStrategy()); 083 builder.setBaseImage(baseImage); 084 } 085 logger.log(ExtensionLogger.LogLevel.LIFECYCLE, "Using base image: " + baseImage); 086 087 if (buildPlan.getExposedPorts() == null || buildPlan.getExposedPorts().isEmpty()) { 088 var applicationConfigurationService = new ApplicationConfigurationService(mavenData.getMavenProject()); 089 try { 090 int port = Integer.parseInt(applicationConfigurationService.getServerPort()); 091 if (port > 0) { 092 logger.log(ExtensionLogger.LogLevel.LIFECYCLE, "Exposing port: " + port); 093 builder.addExposedPort(Port.tcp(port)); 094 } 095 } catch (NumberFormatException e) { 096 // ignore, can't automatically expose port 097 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."); 098 } 099 } 100 101 var detectedPlatform = detectPlatform(); 102 if (buildPlan.getPlatforms() == null || buildPlan.getPlatforms().isEmpty() || !buildPlan.getPlatforms().contains(detectedPlatform)) { 103 LOG.info("Adding Detected platform: {}/{}", LINUX, detectedPlatform.getArchitecture()); 104 builder.addPlatform(detectedPlatform.getArchitecture(), LINUX); 105 } 106 107 switch (runtime.getBuildStrategy()) { 108 case ORACLE_FUNCTION -> { 109 List<? extends LayerObject> originalLayers = buildPlan.getLayers(); 110 builder.setLayers(originalLayers.stream().map(JibMicronautExtension::remapLayer).toList()); 111 List<String> cmd = jibConfigurationService.getArgs(); 112 if (cmd.isEmpty()) { 113 cmd = Collections.singletonList("io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest"); 114 } 115 builder.setWorkingDirectory(AbsoluteUnixPath.get(jibConfigurationService.getWorkingDirectory().orElse("/function"))) 116 .setEntrypoint(buildProjectFnEntrypoint()) 117 .setCmd(cmd); 118 } 119 case LAMBDA -> { 120 //TODO Leverage AWS Base images: 121 // https://docs.aws.amazon.com/lambda/latest/dg/java-image.html 122 // https://docs.aws.amazon.com/lambda/latest/dg/images-create.html 123 // https://docs.aws.amazon.com/lambda/latest/dg/images-test.html 124 List<String> entrypoint = buildPlan.getEntrypoint(); 125 Objects.requireNonNull(entrypoint).set(entrypoint.size() - 1, "io.micronaut.function.aws.runtime.MicronautLambdaRuntime"); 126 builder.setEntrypoint(entrypoint); 127 } 128 default -> { 129 //no op 130 } 131 } 132 return builder.build(); 133 } 134 135 public static List<String> buildProjectFnEntrypoint() { 136 var entrypoint = new ArrayList<String>(9); 137 entrypoint.add("java"); 138 entrypoint.add("-XX:-UsePerfData"); 139 entrypoint.add("-XX:+UseSerialGC"); 140 entrypoint.add("-Xshare:auto"); 141 entrypoint.add("-Djava.awt.headless=true"); 142 entrypoint.add("-Djava.library.path=/function/runtime/lib"); 143 entrypoint.add("-cp"); 144 entrypoint.add("/function/app/classes:/function/app/libs/*:/function/app/resources:/function/runtime/*"); 145 entrypoint.add("com.fnproject.fn.runtime.EntryPoint"); 146 return entrypoint; 147 } 148 149 public static String determineProjectFnVersion(String javaVersion) { 150 int majorVersion = Integer.parseInt(javaVersion.split("\\.")[0]); 151 if (majorVersion <= 25 && majorVersion > 21) { 152 return "25-jre"; 153 } else if (majorVersion == 21) { 154 return "21-jre"; 155 } else { 156 return LATEST_TAG; 157 } 158 } 159 160 public static String determineBaseImage(String jdkVersion, DockerBuildStrategy buildStrategy) { 161 int javaVersion = Integer.parseInt(jdkVersion); 162 return switch (buildStrategy) { 163 case LAMBDA -> "public.ecr.aws/lambda/java:" + javaVersion; 164 default -> javaVersion == 21 ? DEFAULT_JAVA21_BASE_IMAGE : DEFAULT_JAVA25_BASE_IMAGE; 165 }; 166 } 167 168 public static String getJdkVersion(MavenSession session) { 169 var releaseVersion = getPropertyValue(session, JDK_RELEASE_VERSION); 170 var targetVersion = getPropertyValue(session, JDK_TARGET_VERSION); 171 var sourceVersion = getPropertyValue(session, JDK_SOURCE_VERSION); 172 173 Optional<String> jdkVersionOpt = releaseVersion 174 .or(() -> targetVersion) 175 .or(() -> sourceVersion); 176 177 String jdkVersion = jdkVersionOpt.orElse("21"); // Default to project baseline JDK 21 178 String propertySource = releaseVersion.isPresent() ? JDK_RELEASE_VERSION : 179 targetVersion.isPresent() ? JDK_TARGET_VERSION : 180 sourceVersion.isPresent() ? JDK_SOURCE_VERSION : "default (21)"; 181 182 LOG.info("Using JDK version {} from {}", jdkVersion, propertySource); 183 return jdkVersion; 184 } 185 186 private static Optional<String> getPropertyValue(MavenSession session, String propertName) { 187 MojoExecution mojoExecution = new MojoExecution(new MojoDescriptor()); 188 var evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); 189 try { 190 return Optional.ofNullable((String) evaluator.evaluate("${" + propertName + "}", String.class)); 191 } catch (ExpressionEvaluationException e) { 192 return Optional.empty(); 193 } 194 } 195 196 static LayerObject remapLayer(LayerObject layerObject) { 197 var originalLayer = (FileEntriesLayer) layerObject; 198 FileEntriesLayer.Builder builder = FileEntriesLayer.builder().setName(originalLayer.getName()); 199 for (FileEntry originalEntry : originalLayer.getEntries()) { 200 builder.addEntry(remapEntry(originalEntry, layerObject.getName())); 201 } 202 203 return builder.build(); 204 } 205 206 static FileEntry remapEntry(FileEntry originalEntry, String layerName) { 207 List<String> pathComponents = UnixPathParser.parse(originalEntry.getExtractionPath().toString()); 208 AbsoluteUnixPath newPath; 209 if (layerName.contains("dependencies")) { 210 newPath = AbsoluteUnixPath.get("/function/app/libs/" + pathComponents.get(pathComponents.size() - 1)); 211 } else { 212 //classes or resources 213 newPath = AbsoluteUnixPath.get("/function" + originalEntry.getExtractionPath()); 214 } 215 216 return new FileEntry(originalEntry.getSourceFile(), newPath, originalEntry.getPermissions(), 217 originalEntry.getModificationTime(), originalEntry.getOwnership()); 218 } 219 220 private Platform detectPlatform() { 221 String arch = System.getProperty("os.arch").equals("aarch64") ? "arm64" : "amd64"; 222 return new Platform(arch, LINUX); 223 } 224 225}