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}