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}