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; 047import java.util.Set; 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 public static final String DEFAULT_JAVA17_BASE_IMAGE = "eclipse-temurin:17-jre"; 058 public static final String DEFAULT_JAVA21_BASE_IMAGE = "eclipse-temurin:21-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 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 if (buildPlan.getPlatforms() == null || buildPlan.getPlatforms().isEmpty()) { 102 builder.setPlatforms(Set.of(detectPlatform())); 103 } 104 105 switch (runtime.getBuildStrategy()) { 106 case ORACLE_FUNCTION -> { 107 List<? extends LayerObject> originalLayers = buildPlan.getLayers(); 108 builder.setLayers(originalLayers.stream().map(JibMicronautExtension::remapLayer).toList()); 109 List<String> cmd = jibConfigurationService.getArgs(); 110 if (cmd.isEmpty()) { 111 cmd = Collections.singletonList("io.micronaut.oraclecloud.function.http.HttpFunction::handleRequest"); 112 } 113 builder.setWorkingDirectory(AbsoluteUnixPath.get(jibConfigurationService.getWorkingDirectory().orElse("/function"))) 114 .setEntrypoint(buildProjectFnEntrypoint()) 115 .setCmd(cmd); 116 } 117 case LAMBDA -> { 118 //TODO Leverage AWS Base images: 119 // https://docs.aws.amazon.com/lambda/latest/dg/java-image.html 120 // https://docs.aws.amazon.com/lambda/latest/dg/images-create.html 121 // https://docs.aws.amazon.com/lambda/latest/dg/images-test.html 122 List<String> entrypoint = buildPlan.getEntrypoint(); 123 Objects.requireNonNull(entrypoint).set(entrypoint.size() - 1, "io.micronaut.function.aws.runtime.MicronautLambdaRuntime"); 124 builder.setEntrypoint(entrypoint); 125 } 126 default -> { 127 //no op 128 } 129 } 130 return builder.build(); 131 } 132 133 public static List<String> buildProjectFnEntrypoint() { 134 var entrypoint = new ArrayList<String>(9); 135 entrypoint.add("java"); 136 entrypoint.add("-XX:-UsePerfData"); 137 entrypoint.add("-XX:+UseSerialGC"); 138 entrypoint.add("-Xshare:auto"); 139 entrypoint.add("-Djava.awt.headless=true"); 140 entrypoint.add("-Djava.library.path=/function/runtime/lib"); 141 entrypoint.add("-cp"); 142 entrypoint.add("/function/app/classes:/function/app/libs/*:/function/app/resources:/function/runtime/*"); 143 entrypoint.add("com.fnproject.fn.runtime.EntryPoint"); 144 return entrypoint; 145 } 146 147 public static String determineProjectFnVersion(String javaVersion) { 148 int majorVersion = Integer.parseInt(javaVersion.split("\\.")[0]); 149 if (majorVersion <= 21 && majorVersion > 17) { 150 return "21-jre"; 151 } else if (majorVersion == 17) { 152 return "17-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 -> javaVersion == 17 ? DEFAULT_JAVA17_BASE_IMAGE : DEFAULT_JAVA21_BASE_IMAGE; 163 }; 164 } 165 166 public static String getJdkVersion(MavenSession session) { 167 var releaseVersion = getPropertyValue(session, JDK_RELEASE_VERSION); 168 var targetVersion = getPropertyValue(session, JDK_TARGET_VERSION); 169 var sourceVersion = getPropertyValue(session, JDK_SOURCE_VERSION); 170 171 Optional<String> jdkVersionOpt = releaseVersion 172 .or(() -> targetVersion) 173 .or(() -> sourceVersion); 174 175 String jdkVersion = jdkVersionOpt.orElse("17"); // Default to project baseline JDK 17 176 String propertySource = releaseVersion.isPresent() ? JDK_RELEASE_VERSION : 177 targetVersion.isPresent() ? JDK_TARGET_VERSION : 178 sourceVersion.isPresent() ? JDK_SOURCE_VERSION : "default (17)"; 179 180 LOG.info("Using JDK version {} from {}", jdkVersion, propertySource); 181 return jdkVersion; 182 } 183 184 private static Optional<String> getPropertyValue(MavenSession session, String propertName) { 185 MojoExecution mojoExecution = new MojoExecution(new MojoDescriptor()); 186 var evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); 187 try { 188 return Optional.ofNullable((String) evaluator.evaluate("${" + propertName + "}", String.class)); 189 } catch (ExpressionEvaluationException e) { 190 return Optional.empty(); 191 } 192 } 193 194 static LayerObject remapLayer(LayerObject layerObject) { 195 var originalLayer = (FileEntriesLayer) layerObject; 196 FileEntriesLayer.Builder builder = FileEntriesLayer.builder().setName(originalLayer.getName()); 197 for (FileEntry originalEntry : originalLayer.getEntries()) { 198 builder.addEntry(remapEntry(originalEntry, layerObject.getName())); 199 } 200 201 return builder.build(); 202 } 203 204 static FileEntry remapEntry(FileEntry originalEntry, String layerName) { 205 List<String> pathComponents = UnixPathParser.parse(originalEntry.getExtractionPath().toString()); 206 AbsoluteUnixPath newPath; 207 if (layerName.contains("dependencies")) { 208 newPath = AbsoluteUnixPath.get("/function/app/libs/" + pathComponents.get(pathComponents.size() - 1)); 209 } else { 210 //classes or resources 211 newPath = AbsoluteUnixPath.get("/function" + originalEntry.getExtractionPath()); 212 } 213 214 return new FileEntry(originalEntry.getSourceFile(), newPath, originalEntry.getPermissions(), 215 originalEntry.getModificationTime(), originalEntry.getOwnership()); 216 } 217 218 private Platform detectPlatform() { 219 String arch = System.getProperty("os.arch").equals("aarch64") ? "arm64" : "amd64"; 220 return new Platform(arch, "linux"); 221 } 222 223}