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}