Skip to content

Commit

Permalink
integrate revamped authentication support from docker-client
Browse files Browse the repository at this point in the history
This largely mirrors changes done in dockerfile-maven to integrate the
new authentication support from docker-client.

The plugin now loads credentials from three places, in order:

1. Any credentials stored in ~/.dockercfg or ~/.docker/config.json

2. (Optional) Google Cloud credentials, either loaded through the
DOCKER_GOOGLE_CREDENTIALS environment variable or Google's automatic
resolution process for credentials

3. Last precedence is given to any explicitly configured credentials in
the plugin's configuration.

The `useConfigFile` parameter is no longer used as authentication info
from the docker configuration file is always activated.
  • Loading branch information
mattnworb committed Jun 7, 2017
1 parent f6921cb commit 6506d74
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 99 deletions.
58 changes: 30 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ A Maven plugin for building and pushing Docker images.
* [Use a Dockerfile](#use-a-dockerfile)
* [Usage](#usage)
* [Bind Docker commands to Maven phases](#bind-docker-commands-to-maven-phases)
* [Authenticating with private registries](#authenticating-with-private-registries)
* [Using with Private Registries](#using-with-private-registries)
* [Authentication](#authentication)
* [Using encrypted passwords for authentication](#using-encrypted-passwords-for-authentication)
* [Testing](#testing)
* [Releasing](#releasing)
* [Known Issues](#known-issues)

Expand Down Expand Up @@ -259,7 +262,9 @@ Then when pushing the image with either `docker:build -DpushImage` or
`docker:push`, the docker daemon will push to `registry.example.com`.

Alternatively, if you wish to use a short name in `docker:build` you can use
`docker:tag -DpushImage` to tag the just-built image with the full registry hostname and push it. It's important to use the `pushImage` flag as using `docker:push` independently will attempt to push the original image.
`docker:tag -DpushImage` to tag the just-built image with the full registry
hostname and push it. It's important to use the `pushImage` flag as using
`docker:push` independently will attempt to push the original image.

For example:

Expand Down Expand Up @@ -294,10 +299,28 @@ For example:
</plugin>
```

#### Authenticating with Private Registries
### Authentication

To push to a private Docker image registry that requires authentication, you can put your
credentials in your Maven's global `settings.xml` file as part of the `<servers></servers>` block.
Since version 1.0.0, the docker-maven-plugin will automatically use any
authentication present in the docker-cli configuration file at `~/.dockercfg`
or `~/.docker/config.json`, without the need to configure anything (in earlier
versions of the plugin this behavior had to be enabled with
`<useConfigFile>true</useConfigFile>`, but now it is always active).

Additionally the plugin will enable support for Google Container Registry if it
is able to successfully load [Google's "Application Default Credentials"][ADC].
The plugin will also load Google credentials from the file pointed to by the
environment variable `DOCKER_GOOGLE_CREDENTIALS` if it is defined. Since GCR
authentication requires retrieving short-lived access codes for the given
credentials, support for this registry is baked into the underlying
docker-client rather than having to first populate the docker config file
before running the plugin.

[ADC]: https://developers.google.com/identity/protocols/application-default-credentials

Lastly, authentication credentials can be explicitly configured in your pom.xml
and in your Maven installation's `settings.xml` file as part of the
`<servers></servers>` block.

<servers>
<server>
Expand All @@ -312,7 +335,6 @@ credentials in your Maven's global `settings.xml` file as part of the `<servers>

Now use the server id in your project `pom.xml`.


<plugin>
<plugin>
<groupId>com.spotify</groupId>
Expand All @@ -326,8 +348,8 @@ Now use the server id in your project `pom.xml`.
</plugin>
</plugins>

`<registryUrl></registryUrl>` is optional and defaults to `https://index.docker.io/v1/` in the
Spotify docker-client dependency.
The plugin gives priority to any credentials in the docker-cli config file
before explicitly configured credentials.

#### Using encrypted passwords for authentication

Expand All @@ -342,26 +364,6 @@ Only passwords enclosed in curly braces will be considered as encrypted.
</server>
</servers>

#### Using docker config file for authentication

Another option to authenticate with private repositories is using dockers ~/.docker/config.json.
This makes it also possible to use in cooperation with cloud providers like AWS or Google Cloud which store the user's
credentials in this file, too.

<plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>VERSION GOES HERE</version>
<configuration>
[...]
<useConfigFile>true</useConfigFile>
</configuration>
</plugin>
</plugins>

**Hint:** The build will fail, if the config file doesn't exist.

## Testing

Make sure Docker daemon is running and that you can do `docker ps`. Then run `mvn clean test`.
Expand Down
9 changes: 7 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.15-SNAPSHOT</version>
<version>1.0.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>docker-maven-plugin</name>
<description>A maven plugin for docker</description>
Expand Down Expand Up @@ -125,9 +125,14 @@
<dependency>
<groupId>com.spotify</groupId>
<artifactId>docker-client</artifactId>
<version>8.5.0</version>
<version>8.7.1</version>
<classifier>shaded</classifier>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
Expand Down
186 changes: 139 additions & 47 deletions src/main/java/com/spotify/docker/AbstractDockerMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,32 @@

package com.spotify.docker;

import static com.google.common.base.Strings.isNullOrEmpty;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.spotify.docker.client.DefaultDockerClient;
import com.spotify.docker.client.DockerCertificates;
import com.spotify.docker.client.DockerCertificatesStore;
import com.spotify.docker.client.DockerClient;
import com.spotify.docker.client.auth.ConfigFileRegistryAuthSupplier;
import com.spotify.docker.client.auth.MultiRegistryAuthSupplier;
import com.spotify.docker.client.auth.NoOpRegistryAuthSupplier;
import com.spotify.docker.client.auth.RegistryAuthSupplier;
import com.spotify.docker.client.auth.gcr.ContainerRegistryAuthSupplier;
import com.spotify.docker.client.exceptions.DockerCertificateException;
import com.spotify.docker.client.messages.RegistryAuth;

import com.google.common.base.Optional;

import com.spotify.docker.client.messages.RegistryConfigs;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
Expand All @@ -42,11 +59,6 @@
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;

import java.io.IOException;
import java.nio.file.Paths;

import static com.google.common.base.Strings.isNullOrEmpty;

abstract class AbstractDockerMojo extends AbstractMojo {

@Component(role = MavenSession.class)
Expand Down Expand Up @@ -83,9 +95,6 @@ abstract class AbstractDockerMojo extends AbstractMojo {
@Parameter(property = "registryUrl")
private String registryUrl;

@Parameter(property = "useConfigFile")
private Boolean useConfigFile;

/**
* Number of retries for failing pushes, defaults to 5.
*/
Expand Down Expand Up @@ -148,24 +157,25 @@ protected DefaultDockerClient.Builder getBuilder() throws DockerCertificateExcep
.readTimeoutMillis(0);
}

protected DockerClient buildDockerClient()
throws DockerCertificateException, SecDispatcherException, MojoExecutionException {
protected DockerClient buildDockerClient() throws MojoExecutionException {

final DefaultDockerClient.Builder builder = getBuilder();
final DefaultDockerClient.Builder builder;
try {
builder = getBuilder();

final String dockerHost = rawDockerHost();
if (!isNullOrEmpty(dockerHost)) {
builder.uri(dockerHost);
}
final Optional<DockerCertificatesStore> certs = dockerCertificates();
if (certs.isPresent()) {
builder.dockerCertificates(certs.get());
final String dockerHost = rawDockerHost();
if (!isNullOrEmpty(dockerHost)) {
builder.uri(dockerHost);
}
final Optional<DockerCertificatesStore> certs = dockerCertificates();
if (certs.isPresent()) {
builder.dockerCertificates(certs.get());
}
} catch (DockerCertificateException ex) {
throw new MojoExecutionException("Cannot build DockerClient due to certificate problem", ex);
}

final RegistryAuth registryAuth = registryAuth();
if (registryAuth != null) {
builder.registryAuth(registryAuth);
}
builder.registryAuthSupplier(authSupplier());

return builder.build();
}
Expand Down Expand Up @@ -238,11 +248,8 @@ private boolean incompleteAuthSettings(final String username, final String passw

/**
* Builds the registryAuth object from server details.
* @return registryAuth
* @throws MojoExecutionException
* @throws SecDispatcherException
*/
protected RegistryAuth registryAuth() throws MojoExecutionException, SecDispatcherException {
protected RegistryAuth registryAuth() throws MojoExecutionException {
if (settings != null) {
final Server server = settings.getServer(serverId);
if (server != null) {
Expand All @@ -251,7 +258,11 @@ protected RegistryAuth registryAuth() throws MojoExecutionException, SecDispatch
final String username = server.getUsername();
String password = server.getPassword();
if (secDispatcher != null) {
password = secDispatcher.decrypt(password);
try {
password = secDispatcher.decrypt(password);
} catch (SecDispatcherException ex) {
throw new MojoExecutionException("Cannot decrypt password from settings", ex);
}
}
final String email = getEmail(server);

Expand All @@ -270,32 +281,113 @@ protected RegistryAuth registryAuth() throws MojoExecutionException, SecDispatch
if (!isNullOrEmpty(password)) {
registryAuthBuilder.password(password);
}
// registryUrl is optional.
// Spotify's docker-client defaults to 'https://index.docker.io/v1/'.
if (!isNullOrEmpty(registryUrl)) {
registryAuthBuilder.serverAddress(registryUrl);
}

return registryAuthBuilder.build();
} else if (useConfigFile != null && useConfigFile){
}
}
return null;
}

final RegistryAuth.Builder registryAuthBuilder;
try {
if (!isNullOrEmpty(registryUrl)) {
registryAuthBuilder = RegistryAuth.fromDockerConfig(registryUrl);
} else {
registryAuthBuilder = RegistryAuth.fromDockerConfig();
}
} catch (IOException ex){
throw new MojoExecutionException(
"Docker config file could not be read",
ex
);
private RegistryAuthSupplier authSupplier() throws MojoExecutionException {

final List<RegistryAuthSupplier> suppliers = new ArrayList<>();

// prioritize the docker config file
suppliers.add(new ConfigFileRegistryAuthSupplier());

// then Google Container Registry support
final RegistryAuthSupplier googleRegistrySupplier = googleContainerRegistryAuthSupplier();
if (googleRegistrySupplier != null) {
suppliers.add(googleRegistrySupplier);
}

// lastly, use any explicitly configured RegistryAuth as a catch-all
final RegistryAuth registryAuth = registryAuth();
if (registryAuth != null) {
final RegistryConfigs configsForBuild = RegistryConfigs.create(ImmutableMap.of(
serverIdFor(registryAuth), registryAuth
));
suppliers.add(new NoOpRegistryAuthSupplier(registryAuth, configsForBuild));
}

getLog().info("Using authentication suppliers: " +
Lists.transform(suppliers, new SupplierToClassNameFunction()));

return new MultiRegistryAuthSupplier(suppliers);
}

private String serverIdFor(RegistryAuth registryAuth) {
if (serverId != null) {
return serverId;
}
if (registryAuth.serverAddress() != null) {
return registryAuth.serverAddress();
}
return "index.docker.io";
}

/**
* Attempt to load a GCR compatible RegistryAuthSupplier based on a few conditions:
* <ol>
* <li>First check to see if the environemnt variable DOCKER_GOOGLE_CREDENTIALS is set and points
* to a readable file</li>
* <li>Otherwise check if the Google Application Default Credentials can be loaded</li>
* </ol>
* Note that we use a special environment variable of our own in addition to any environment
* variable that the ADC loading uses (GOOGLE_APPLICATION_CREDENTIALS) in case there is a need for
* the user to use the latter env var for some other purpose in their build.
*
* @return a GCR RegistryAuthSupplier, or null
* @throws MojoExecutionException if an IOException occurs while loading the explicitly-requested
* credentials
*/
private RegistryAuthSupplier googleContainerRegistryAuthSupplier() throws MojoExecutionException {
GoogleCredentials credentials = null;

final String googleCredentialsPath = System.getenv("DOCKER_GOOGLE_CREDENTIALS");
if (googleCredentialsPath != null) {
final File file = new File(googleCredentialsPath);
if (file.exists()) {
try {
try (FileInputStream inputStream = new FileInputStream(file)) {
credentials = GoogleCredentials.fromStream(inputStream);
getLog().info("Using Google credentials from file: " + file.getAbsolutePath());
}
} catch (IOException ex) {
throw new MojoExecutionException("Cannot load credentials referenced by "
+ "DOCKER_GOOGLE_CREDENTIALS environment variable", ex);
}
}
}

return registryAuthBuilder.build();
// use the ADC last
if (credentials == null) {
try {
credentials = GoogleCredentials.getApplicationDefault();
getLog().info("Using Google application default credentials");
} catch (IOException ex) {
// No GCP default credentials available
getLog().debug("Failed to load Google application default credentials", ex);
}
}
return null;

if (credentials == null) {
return null;
}

return ContainerRegistryAuthSupplier.forCredentials(credentials).build();
}

private static class SupplierToClassNameFunction
implements Function<RegistryAuthSupplier, String> {

@Override
@Nonnull
public String apply(@Nonnull final RegistryAuthSupplier input) {
return input.getClass().getSimpleName();
}
}
}
Loading

0 comments on commit 6506d74

Please sign in to comment.