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 java.util.function.Consumer;
030import java.util.stream.Stream;
031import org.apache.maven.model.Plugin;
032import org.apache.maven.project.MavenProject;
033import org.slf4j.Logger;
034
035import javax.inject.Inject;
036import javax.inject.Singleton;
037import java.util.Collections;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Optional;
041import java.util.Set;
042
043import static io.micronaut.maven.jib.JibConfiguration.*;
044
045/**
046 * Exposes the Jib plugin configuration so that it can be read by other mojos.
047 *
048 * @author Álvaro Sánchez-Mariscal
049 * @since 1.1
050 */
051@Singleton
052public class JibConfigurationService {
053    private final Optional<JibConfiguration> configuration;
054
055    @Inject
056    public JibConfigurationService(MavenProject mavenProject) {
057        final Plugin plugin = mavenProject.getPlugin(MavenProjectProperties.PLUGIN_KEY);
058        if (plugin != null && plugin.getConfiguration() != null) {
059            final XmlMapper mapper = XmlMapper.builder()
060                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
061                    .findAndAddModules()
062                    .build();
063            try {
064                configuration = Optional.ofNullable(mapper.readValue(plugin.getConfiguration().toString(), JibConfiguration.class));
065            } catch (JsonProcessingException e) {
066                throw new IllegalArgumentException("Error parsing Jib plugin configuration", e);
067            }
068        } else {
069            configuration = Optional.empty();
070        }
071    }
072
073    /**
074     * @return the <code>to.image</code> configuration.
075     */
076    public Optional<String> getToImage() {
077        final String value = configuration.flatMap(c -> c.to().flatMap(ToConfiguration::image))
078                .orElse(null);
079        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.TO_IMAGE, value));
080    }
081
082    /**
083     * @return the <code>from.image</code> configuration.
084     */
085    public Optional<String> getFromImage() {
086        final String value = configuration.flatMap(c -> c.from().flatMap(FromConfiguration::image))
087                .orElse(null);
088        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.FROM_IMAGE, value));
089    }
090
091    /**
092     * @return the <code>to.tags</code> configuration.
093     */
094    public Set<String> getTags() {
095        final Set<String> tags = configuration.flatMap(c -> c.to().map(ToConfiguration::tags))
096                .orElse(Collections.emptySet());
097        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.TO_TAGS))
098                .map(JibConfigurationService::parseCommaSeparatedList)
099                .orElse(tags);
100    }
101
102    /**
103     * @return the <code>to.auth.username</code> and <code>to.auth.password</code> configuration.
104     */
105    public Optional<Credential> getToCredentials() {
106        String usernameProperty = System.getProperties().getProperty(PropertyNames.TO_AUTH_USERNAME);
107        String passwordProperty = System.getProperties().getProperty(PropertyNames.TO_AUTH_PASSWORD);
108        if (usernameProperty != null || passwordProperty != null) {
109            return Optional.of(Credential.from(usernameProperty, passwordProperty));
110        } else {
111            return configuration
112                    .flatMap(c -> c.to().flatMap(ToConfiguration::auth))
113                    .map(this::getCredentials);
114        }
115    }
116
117    /**
118     * @return the <code>from.auth.username</code> and <code>from.auth.password</code> configuration.
119     */
120    public Optional<Credential> getFromCredentials() {
121        String usernameProperty = System.getProperties().getProperty(PropertyNames.FROM_AUTH_USERNAME);
122        String passwordProperty = System.getProperties().getProperty(PropertyNames.FROM_AUTH_PASSWORD);
123        if (usernameProperty != null || passwordProperty != null) {
124            return Optional.of(Credential.from(usernameProperty, passwordProperty));
125        } else {
126            return configuration
127                    .flatMap(c -> c.from().flatMap(FromConfiguration::auth))
128                    .map(this::getCredentials);
129        }
130
131    }
132
133    /**
134     * Resolves effective credentials for a registry hosting the provided image reference.
135     * Precedence: explicit credentials -> well-known credential helpers -> Google ADC -> docker config.
136     *
137     * @param image the image reference (e.g., gcr.io/project/image:tag)
138     * @param logger the logger to use for logging events
139     * @return a Credential if one could be resolved
140     */
141    public Optional<Credential> resolveCredentialForImage(String image, Logger logger) {
142        try {
143            ImageReference imageReference = ImageReference.parse(image);
144            Consumer<LogEvent> logConsumer = logEvent -> logEvent(logEvent, logger);
145            CredentialRetrieverFactory factory = CredentialRetrieverFactory.forImage(imageReference, logConsumer);
146            return Stream.of(
147                    factory.wellKnownCredentialHelpers(),
148                    factory.googleApplicationDefaultCredentials(),
149                    factory.dockerConfig()
150                )
151                .map(retriever -> {
152                    try {
153                        return retriever.retrieve();
154                    } catch (CredentialRetrievalException e) {
155                        return Optional.<Credential>empty();
156                    }
157                })
158                .filter(Optional::isPresent)
159                .map(Optional::get)
160                .findFirst();
161        } catch (InvalidImageReferenceException e) {
162            logger.warn("Invalid image reference '{}': {}", image, e.getMessage());
163            return Optional.empty();
164        }
165    }
166
167    private Credential getCredentials(AuthConfiguration authConfiguration) {
168        return Credential.from(
169                authConfiguration.username().orElse(null),
170                authConfiguration.password().orElse(null)
171        );
172    }
173
174    /**
175     * @return the <code>container.workingDirectory</code> configuration.
176     */
177    public Optional<String> getWorkingDirectory() {
178        final String value = configuration.flatMap(c -> c.container().flatMap(ContainerConfiguration::workingDirectory))
179                .orElse(null);
180        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_WORKING_DIRECTORY, value));
181    }
182
183    /**
184     * @return the <code>container.args</code> configuration.
185     */
186    public List<String> getArgs() {
187        final List<String> args = configuration.flatMap(c -> c.container().map(ContainerConfiguration::args))
188                .orElse(Collections.emptyList());
189        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_ARGS))
190                .map(JibConfigurationService::parseCommaSeparatedList)
191                .map(List::copyOf)
192                .orElse(args);
193    }
194
195    /**
196     * @return the <code>container.ports</code> configuration.
197     */
198    public Optional<String> getPorts() {
199        final Set<String> ports = configuration.flatMap(c -> c.container().map(ContainerConfiguration::ports))
200                .orElse(Collections.emptySet());
201        return Optional.ofNullable(System.getProperties().getProperty(PropertyNames.CONTAINER_PORTS))
202                .map(s -> s.replace(",", " "))
203                .or(() -> ports.isEmpty() ? Optional.empty() : Optional.of(String.join(" ", ports)));
204    }
205
206    private static Set<String> parseCommaSeparatedList(String list) {
207        String[] parts = list.split(",");
208        var items = new HashSet<String>(parts.length);
209        for (String part : parts) {
210            items.add(part.trim());
211        }
212        return items;
213    }
214
215    private void logEvent(LogEvent logEvent, Logger logger) {
216        if (logEvent.getLevel().equals(LogEvent.Level.DEBUG)) {
217            logger.debug(logEvent.getMessage());
218        } else {
219            logger.info(logEvent.getMessage());
220        }
221    }
222}