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.fasterxml.jackson.core.JsonProcessingException;
019import com.fasterxml.jackson.databind.DeserializationFeature;
020import com.fasterxml.jackson.dataformat.xml.XmlMapper;
021import com.google.cloud.tools.jib.api.Credential;
022import com.google.cloud.tools.jib.api.ImageReference;
023import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
024import com.google.cloud.tools.jib.api.LogEvent;
025import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory;
026import com.google.cloud.tools.jib.maven.MavenProjectProperties;
027import com.google.cloud.tools.jib.plugins.common.PropertyNames;
028import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException;
029import io.micronaut.maven.jib.JibConfiguration.AuthConfiguration;
030import io.micronaut.maven.jib.JibConfiguration.ContainerConfiguration;
031import io.micronaut.maven.jib.JibConfiguration.FromConfiguration;
032import io.micronaut.maven.jib.JibConfiguration.OutputPathsConfiguration;
033import io.micronaut.maven.jib.JibConfiguration.PlatformConfiguration;
034import io.micronaut.maven.jib.JibConfiguration.ToConfiguration;
035import java.util.function.Consumer;
036import java.util.stream.Stream;
037import org.apache.maven.model.Plugin;
038import org.apache.maven.project.MavenProject;
039import org.slf4j.Logger;
040
041import javax.inject.Inject;
042import javax.inject.Singleton;
043import java.util.ArrayList;
044import java.util.Collections;
045import java.util.LinkedHashSet;
046import java.util.List;
047import java.util.Optional;
048import java.util.Set;
049
050/**
051 * Exposes the Jib plugin configuration so that it can be read by other mojos.
052 *
053 * @author Álvaro Sánchez-Mariscal
054 * @since 1.1
055 */
056@Singleton
057public class JibConfigurationService {
058    private final Optional<JibConfiguration> configuration;
059
060    @Inject
061    public JibConfigurationService(MavenProject mavenProject) {
062        final Plugin plugin = mavenProject.getPlugin(MavenProjectProperties.PLUGIN_KEY);
063        if (plugin != null && plugin.getConfiguration() != null) {
064            final XmlMapper mapper = XmlMapper.builder()
065                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
066                    .findAndAddModules()
067                    .build();
068            try {
069                configuration = Optional.ofNullable(mapper.readValue(plugin.getConfiguration().toString(), JibConfiguration.class));
070            } catch (JsonProcessingException e) {
071                throw new IllegalArgumentException("Error parsing Jib plugin configuration", e);
072            }
073        } else {
074            configuration = Optional.empty();
075        }
076    }
077
078    /**
079     * @return the <code>to.image</code> configuration.
080     */
081    public Optional<String> getToImage() {
082        final String value = configuration.flatMap(c -> c.to().flatMap(ToConfiguration::image))
083                .orElse(null);
084        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.TO_IMAGE, value));
085    }
086
087    /**
088     * @return the <code>from.image</code> configuration.
089     */
090    public Optional<String> getFromImage() {
091        final String value = configuration.flatMap(c -> c.from().flatMap(FromConfiguration::image))
092                .orElse(null);
093        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.FROM_IMAGE, value));
094    }
095
096    /**
097     * @return the <code>from.platforms</code> configuration.
098     */
099    public Set<PlatformConfiguration> getFromPlatforms() {
100        final Set<PlatformConfiguration> platforms = configuration.flatMap(c -> c.from().map(FromConfiguration::platforms))
101                .orElse(Collections.emptySet());
102        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.FROM_PLATFORMS))
103                .map(JibConfigurationService::parsePlatforms)
104                .orElse(platforms == null ? Collections.emptySet() : platforms);
105    }
106
107    /**
108     * @return the <code>to.tags</code> configuration.
109     */
110    public Set<String> getTags() {
111        final Set<String> tags = configuration.flatMap(c -> c.to().map(ToConfiguration::tags))
112                .orElse(Collections.emptySet());
113        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.TO_TAGS))
114                .map(JibConfigurationService::parseCommaSeparatedSet)
115                .orElse(tags);
116    }
117
118    /**
119     * @return the <code>to.auth.username</code> and <code>to.auth.password</code> configuration.
120     */
121    public Optional<Credential> getToCredentials() {
122        String usernameProperty = System.getProperties().getProperty(PropertyNames.TO_AUTH_USERNAME);
123        String passwordProperty = System.getProperties().getProperty(PropertyNames.TO_AUTH_PASSWORD);
124        if (usernameProperty != null || passwordProperty != null) {
125            return Optional.of(Credential.from(usernameProperty, passwordProperty));
126        } else {
127            return configuration
128                    .flatMap(c -> c.to().flatMap(ToConfiguration::auth))
129                    .map(this::getCredentials);
130        }
131    }
132
133    /**
134     * @return the <code>from.auth.username</code> and <code>from.auth.password</code> configuration.
135     */
136    public Optional<Credential> getFromCredentials() {
137        String usernameProperty = System.getProperties().getProperty(PropertyNames.FROM_AUTH_USERNAME);
138        String passwordProperty = System.getProperties().getProperty(PropertyNames.FROM_AUTH_PASSWORD);
139        if (usernameProperty != null || passwordProperty != null) {
140            return Optional.of(Credential.from(usernameProperty, passwordProperty));
141        } else {
142            return configuration
143                    .flatMap(c -> c.from().flatMap(FromConfiguration::auth))
144                    .map(this::getCredentials);
145        }
146
147    }
148
149    /**
150     * Resolves effective credentials for a registry hosting the provided image reference.
151     * Precedence: explicit credentials -> well-known credential helpers -> Google ADC -> docker config.
152     *
153     * @param image the image reference (e.g., gcr.io/project/image:tag)
154     * @param logger the logger to use for logging events
155     * @return a Credential if one could be resolved
156     */
157    public Optional<Credential> resolveCredentialForImage(String image, Logger logger) {
158        try {
159            ImageReference imageReference = ImageReference.parse(image);
160            Consumer<LogEvent> logConsumer = logEvent -> logEvent(logEvent, logger);
161            CredentialRetrieverFactory factory = CredentialRetrieverFactory.forImage(imageReference, logConsumer);
162            return Stream.of(
163                    factory.wellKnownCredentialHelpers(),
164                    factory.googleApplicationDefaultCredentials(),
165                    factory.dockerConfig()
166                )
167                .map(retriever -> {
168                    try {
169                        return retriever.retrieve();
170                    } catch (CredentialRetrievalException e) {
171                        return Optional.<Credential>empty();
172                    }
173                })
174                .filter(Optional::isPresent)
175                .map(Optional::get)
176                .findFirst();
177        } catch (InvalidImageReferenceException e) {
178            logger.warn("Invalid image reference '{}': {}", image, e.getMessage());
179            return Optional.empty();
180        }
181    }
182
183    private Credential getCredentials(AuthConfiguration authConfiguration) {
184        return Credential.from(
185                authConfiguration.username().orElse(null),
186                authConfiguration.password().orElse(null)
187        );
188    }
189
190    /**
191     * @return the <code>container.workingDirectory</code> configuration.
192     */
193    public Optional<String> getWorkingDirectory() {
194        final String value = configuration.flatMap(c -> c.container().flatMap(ContainerConfiguration::workingDirectory))
195                .orElse(null);
196        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY, value));
197    }
198
199    /**
200     * @return the <code>container.args</code> configuration.
201     */
202    public List<String> getArgs() {
203        final List<String> args = configuration.flatMap(c -> c.container().map(ContainerConfiguration::args))
204                .orElse(Collections.emptyList());
205        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_ARGS))
206                .map(JibConfigurationService::parseCommaSeparatedList)
207                .map(List::copyOf)
208                .orElse(args == null ? Collections.emptyList() : args);
209    }
210
211    /**
212     * @return the <code>container.ports</code> configuration.
213     */
214    public Optional<String> getPorts() {
215        final Set<String> ports = configuration.flatMap(c -> c.container().map(ContainerConfiguration::ports))
216                .orElse(Collections.emptySet());
217        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_PORTS))
218                .map(s -> s.replace(",", " "))
219                .or(() -> ports == null || ports.isEmpty() ? Optional.empty() : Optional.of(String.join(" ", ports)));
220    }
221
222    /**
223     * @return the <code>container.entrypoint</code> configuration.
224     */
225    public List<String> getEntrypoint() {
226        final List<String> entrypoint = configuration.flatMap(c -> c.container().map(ContainerConfiguration::entrypoint))
227                .orElse(Collections.emptyList());
228        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_ENTRYPOINT))
229                .map(JibConfigurationService::parseCommaSeparatedList)
230                .map(List::copyOf)
231                .orElse(entrypoint == null ? Collections.emptyList() : entrypoint);
232    }
233
234    /**
235     * @return the <code>container.user</code> configuration.
236     */
237    public Optional<String> getUser() {
238        final String value = configuration.flatMap(c -> c.container().flatMap(ContainerConfiguration::user))
239                .orElse(null);
240        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_USER, value));
241    }
242
243    /**
244     * @return the <code>outputPaths.tar</code> configuration.
245     */
246    public Optional<String> getOutputPathsTar() {
247        final String value = configuration.flatMap(c -> c.outputPaths().flatMap(OutputPathsConfiguration::tar))
248                .orElse(null);
249        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.OUTPUT_PATHS_TAR, value))
250                .filter(tar -> !tar.isBlank());
251    }
252
253    private static List<String> parseCommaSeparatedList(String list) {
254        String[] parts = list.split(",");
255        var items = new ArrayList<String>(parts.length);
256        for (String part : parts) {
257            String item = part.trim();
258            if (!item.isBlank()) {
259                items.add(item);
260            }
261        }
262        return items;
263    }
264
265    private static Set<String> parseCommaSeparatedSet(String list) {
266        return new LinkedHashSet<>(parseCommaSeparatedList(list));
267    }
268
269    private static Set<PlatformConfiguration> parsePlatforms(String list) {
270        var platforms = new LinkedHashSet<PlatformConfiguration>();
271        for (String platform : parseCommaSeparatedSet(list)) {
272            if (platform.isEmpty()) {
273                continue;
274            }
275            String[] parts = platform.split("/", 2);
276            if (parts.length != 2 || parts[0].isBlank() || parts[1].isBlank()) {
277                throw new IllegalArgumentException("jib.from.platforms contains an invalid platform token: "
278                    + platform + ". Expected <os>/<architecture>.");
279            }
280            platforms.add(new PlatformConfiguration(Optional.of(parts[1]), Optional.of(parts[0])));
281        }
282        return platforms;
283    }
284
285    private void logEvent(LogEvent logEvent, Logger logger) {
286        if (logEvent.getLevel().equals(LogEvent.Level.DEBUG)) {
287            logger.debug(logEvent.getMessage());
288        } else {
289            logger.info(logEvent.getMessage());
290        }
291    }
292}