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 private static final int SUPPORTED_JDK_VERSION = 25; 057 public static final String DEFAULT_JAVA25_BASE_IMAGE = "eclipse-temurin:" + SUPPORTED_JDK_VERSION + "-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 var originalLayers = buildPlan.getLayers(); 110 builder.setLayers(originalLayers.stream().map(JibMicronautExtension::remapLayer).toList()); 111 var 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 var 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) { 152 return "25-jre"; 153 } else { 154 return LATEST_TAG; 155 } 156 } 157 158 public static String determineBaseImage(String jdkVersion, DockerBuildStrategy buildStrategy) { 159 int javaVersion = Integer.parseInt(jdkVersion); 160 return switch (buildStrategy) { 161 case LAMBDA -> "public.ecr.aws/lambda/java:" + javaVersion; 162 default -> { 163 if (javaVersion > SUPPORTED_JDK_VERSION) { 164 throw new IllegalArgumentException("Unsupported JDK version for Docker base image: " + javaVersion 165 + ". Maximum supported version is " + SUPPORTED_JDK_VERSION + "."); 166 } 167 yield DEFAULT_JAVA25_BASE_IMAGE; 168 } 169 }; 170 } 171 172 public static String getJdkVersion(MavenSession session) { 173 var releaseVersion = getPropertyValue(session, JDK_RELEASE_VERSION); 174 var targetVersion = getPropertyValue(session, JDK_TARGET_VERSION); 175 var sourceVersion = getPropertyValue(session, JDK_SOURCE_VERSION); 176 177 Optional<String> jdkVersionOpt = releaseVersion 178 .or(() -> targetVersion) 179 .or(() -> sourceVersion); 180 181 String jdkVersion = jdkVersionOpt.orElse(String.valueOf(SUPPORTED_JDK_VERSION)); 182 String propertySource = releaseVersion.isPresent() ? JDK_RELEASE_VERSION : 183 targetVersion.isPresent() ? JDK_TARGET_VERSION : 184 sourceVersion.isPresent() ? JDK_SOURCE_VERSION : "default (" + SUPPORTED_JDK_VERSION + ")"; 185 186 LOG.info("Using JDK version {} from {}", jdkVersion, propertySource); 187 return jdkVersion; 188 } 189 190 private static Optional<String> getPropertyValue(MavenSession session, String propertName) { 191 MojoExecution mojoExecution = new MojoExecution(new MojoDescriptor()); 192 var evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); 193 try { 194 return Optional.ofNullable((String) evaluator.evaluate("${" + propertName + "}", String.class)); 195 } catch (ExpressionEvaluationException e) { 196 return Optional.empty(); 197 } 198 } 199 200 static LayerObject remapLayer(LayerObject layerObject) { 201 var originalLayer = (FileEntriesLayer) layerObject; 202 FileEntriesLayer.Builder builder = FileEntriesLayer.builder().setName(originalLayer.getName()); 203 for (FileEntry originalEntry : originalLayer.getEntries()) { 204 builder.addEntry(remapEntry(originalEntry, layerObject.getName())); 205 } 206 207 return builder.build(); 208 } 209 210 static FileEntry remapEntry(FileEntry originalEntry, String layerName) { 211 var pathComponents = UnixPathParser.parse(originalEntry.getExtractionPath().toString()); 212 AbsoluteUnixPath newPath; 213 if (layerName.contains("dependencies")) { 214 newPath = AbsoluteUnixPath.get("/function/app/libs/" + pathComponents.get(pathComponents.size() - 1)); 215 } else { 216 //classes or resources 217 newPath = AbsoluteUnixPath.get("/function" + originalEntry.getExtractionPath()); 218 } 219 220 return new FileEntry(originalEntry.getSourceFile(), newPath, originalEntry.getPermissions(), 221 originalEntry.getModificationTime(), originalEntry.getOwnership()); 222 } 223 224 private Platform detectPlatform() { 225 String osArchitecture = System.getProperty("os.arch"); 226 String arch = "aarch64".equals(osArchitecture) || "arm64".equals(osArchitecture) ? "arm64" : "amd64"; 227 return new Platform(arch, LINUX); 228 } 229 230}