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