Skip to content

Commit

Permalink
[JENKINS-38018] Use docker-login wherever possible for registry crede…
Browse files Browse the repository at this point in the history
…ntials.
  • Loading branch information
jglick committed Jan 31, 2018
1 parent 0b0429d commit a135229
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 38 deletions.
Expand Up @@ -62,6 +62,8 @@
import hudson.AbortException;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.slaves.WorkspaceList;
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;

/**
* Encapsulates the endpoint of DockerHub and how to interact with it.
Expand Down Expand Up @@ -185,7 +187,7 @@ DockerRegistryToken getToken(Item context) {
}

/**
* @deprecated Call {@link #newKeyMaterialFactory(Item, VirtualChannel, Launcher, TaskListener)}
* @deprecated Call {@link #newKeyMaterialFactory(Item, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(@Nonnull AbstractBuild build) throws IOException, InterruptedException {
Expand All @@ -197,17 +199,17 @@ public KeyMaterialFactory newKeyMaterialFactory(@Nonnull AbstractBuild build) th
}

/**
* @deprecated Call {@link #newKeyMaterialFactory(Item, VirtualChannel, Launcher, TaskListener)}
* @deprecated Call {@link #newKeyMaterialFactory(Item, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(Item context, @Nonnull VirtualChannel target) throws IOException, InterruptedException {
return newKeyMaterialFactory(context, target, null, TaskListener.NULL);
}

/**
* Makes the credentials available locally and returns {@link KeyMaterialFactory} that gives you the parameters
* needed to access it.
* @deprecated Call {@link #newKeyMaterialFactory(Item, FilePath, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(@CheckForNull Item context, @Nonnull VirtualChannel target, @CheckForNull Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException {
if (credentialsId == null) {
return KeyMaterialFactory.NULL; // nothing needed to be done
Expand All @@ -219,6 +221,23 @@ public KeyMaterialFactory newKeyMaterialFactory(@CheckForNull Item context, @Non
return token.newKeyMaterialFactory(getEffectiveUrl(), target, launcher, listener);
}

/**
* Makes the credentials available locally and returns {@link KeyMaterialFactory} that gives you the parameters
* needed to access it.
* @param workspace a workspace being used for operations ({@link WorkspaceList#tempDir} will be applied)
* @param dockerExecutable as in {@link DockerTool#getExecutable}, with a 1.8+ client
*/
public KeyMaterialFactory newKeyMaterialFactory(@CheckForNull Item context, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener, @Nonnull String dockerExecutable) throws IOException, InterruptedException {
if (credentialsId == null) {
return KeyMaterialFactory.NULL; // nothing needed to be done
}
DockerRegistryToken token = getToken(context);
if (token == null) {
throw new AbortException("Could not find credentials matching " + credentialsId);
}
return token.newKeyMaterialFactory(getEffectiveUrl(), workspace, launcher, listener, dockerExecutable);
}

/**
* Decorates the repository ID namespace/name (ie. "jenkinsci/workflow-demo") with registry prefix
* (docker.acme.com:80/jenkinsci/workflow-demo).
Expand Down
Expand Up @@ -24,20 +24,26 @@
package org.jenkinsci.plugins.docker.commons.credentials;

import com.cloudbees.plugins.credentials.Credentials;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import jenkins.authentication.tokens.api.AuthenticationTokens;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;

import javax.annotation.Nonnull;
import hudson.slaves.WorkspaceList;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.authentication.tokens.api.AuthenticationTokens;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.docker.commons.impl.RegistryKeyMaterialFactory;
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;

/**
* Represents an authentication token that docker(1) understands when pushing/pulling
Expand Down Expand Up @@ -65,28 +71,48 @@ public String getToken() {
}

/**
* @deprecated Call {@link #newKeyMaterialFactory(URL, VirtualChannel, Launcher, TaskListener)}
* @deprecated use {@link #newKeyMaterialFactory(URL, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(final URL endpoint, @Nonnull VirtualChannel target) throws InterruptedException, IOException {
return newKeyMaterialFactory(endpoint, target, null, TaskListener.NULL);
}

/**
* Sets up an environment logged in to the specified Docker registry.
* @param dockerExecutable as in {@link DockerTool#getExecutable}, with a 1.8+ client
*/
public KeyMaterialFactory newKeyMaterialFactory(@Nonnull URL endpoint, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener, @Nonnull String dockerExecutable) throws InterruptedException, IOException {
try {
// see UsernamePasswordDockerRegistryTokenSource for example
String usernameColonPassword = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
int colon = usernameColonPassword.indexOf(':');
if (colon > 0) {
return new RegistryKeyMaterialFactory(usernameColonPassword.substring(0, colon), usernameColonPassword.substring(colon + 1), endpoint, launcher, listener, dockerExecutable).
contextualize(new KeyMaterialContext(WorkspaceList.tempDir(workspace)));
}
} catch (IllegalArgumentException x) {
// not Base64-encoded
}
listener.getLogger().println("Warning: authentication token does not look like a username:password; falling back to direct manipulation of Docker configuration files");
return newKeyMaterialFactory(endpoint, workspace.getChannel(), launcher, listener);
}

/**
* Makes the credentials available locally and returns {@link KeyMaterialFactory} that gives you the parameters
* needed to access it.
*
* This is done by inserting the token into {@code ~/.dockercfg}
* @deprecated use {@link #newKeyMaterialFactory(URL, Launcher, TaskListener, String)}
*/
@Deprecated
public KeyMaterialFactory newKeyMaterialFactory(final @Nonnull URL endpoint, @Nonnull VirtualChannel target, @CheckForNull Launcher launcher, final @Nonnull TaskListener listener) throws InterruptedException, IOException {
target.call(new MasterToSlaveCallable<Void, IOException>() {
/**
* Insert the token into {@code ~/.dockercfg}
*/
@Override
public Void call() throws IOException {
// TODO: TF: Should this not be done via docker login (possibly preceded by a logout) ?

JSONObject json;
JSONObject auths;

Expand Down
Expand Up @@ -130,6 +130,7 @@ public KeyMaterialFactory newKeyMaterialFactory(@Nonnull Item context, @Nonnull
}

static FilePath dotDocker(@Nonnull VirtualChannel target) throws IOException, InterruptedException {
// TODO this is wrong, should be using WorkspaceList.tempDir
return FilePath.getHomeDirectory(target).child(".docker");
}

Expand Down
Expand Up @@ -23,16 +23,17 @@
*/
package org.jenkinsci.plugins.docker.commons.credentials;

import hudson.FilePath;
import hudson.model.AbstractBuild;
import org.jenkinsci.plugins.docker.commons.impl.CompositeKeyMaterialFactory;
import org.jenkinsci.plugins.docker.commons.impl.NullKeyMaterialFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
* Represents a locally extracted credentials information.
Expand All @@ -45,13 +46,8 @@
* @see DockerServerEndpoint#newKeyMaterialFactory(AbstractBuild)
* @see DockerRegistryEndpoint#newKeyMaterialFactory(AbstractBuild)
*/
public abstract class KeyMaterialFactory implements Serializable {
public abstract class KeyMaterialFactory {

/**
* Ensure consistent serialization.
*/
private static final long serialVersionUID = 1L;

public static final KeyMaterialFactory NULL = new NullKeyMaterialFactory();

private /* write once */ KeyMaterialContext context;
Expand Down Expand Up @@ -92,6 +88,17 @@ protected synchronized KeyMaterialContext getContext() {
*/
public abstract KeyMaterial materialize() throws IOException, InterruptedException;

/**
* Creates a read-protected directory inside {@link KeyMaterialContext#getBaseDir} suitable for storing secret files.
* Be sure to {@link FilePath#deleteRecursive} this in {@link KeyMaterial#close}.
*/
protected final FilePath createSecretsDirectory() throws IOException, InterruptedException {
FilePath dir = new FilePath(getContext().getBaseDir(), UUID.randomUUID().toString());
dir.mkdirs();
dir.chmod(0700);
return dir;
}

/**
* Merge additional {@link KeyMaterialFactory}s into one.
*/
Expand Down
Expand Up @@ -40,8 +40,6 @@
@Restricted(NoExternalUse.class)
public final class NullKeyMaterialFactory extends KeyMaterialFactory {

private static final long serialVersionUID = 1L;

@Override
public KeyMaterial materialize() throws IOException, InterruptedException {
return KeyMaterial.NULL;
Expand Down
@@ -0,0 +1,102 @@
/*
* The MIT License
*
* Copyright 2018 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.docker.commons.impl;

import hudson.AbortException;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.util.ArgumentListBuilder;
import java.io.IOException;
import java.net.URL;
import javax.annotation.Nonnull;
import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial;
import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Logs you in to a Docker registry.
*/
@Restricted(NoExternalUse.class)
public class RegistryKeyMaterialFactory extends KeyMaterialFactory {

private final @Nonnull String username;
private final @Nonnull String password;
private final @Nonnull URL endpoint;
private final @Nonnull Launcher launcher;
private final @Nonnull TaskListener listener;
private final @Nonnull String dockerExecutable;

public RegistryKeyMaterialFactory(@Nonnull String username, @Nonnull String password, @Nonnull URL endpoint, @Nonnull Launcher launcher, @Nonnull TaskListener listener, @Nonnull String dockerExecutable) {
this.username = username;
this.password = password;
this.endpoint = endpoint;
this.launcher = launcher;
this.listener = listener;
this.dockerExecutable = dockerExecutable;
}

@Override
public KeyMaterial materialize() throws IOException, InterruptedException {
FilePath dockerConfig = createSecretsDirectory();
try {
if (launcher.launch().cmds(new ArgumentListBuilder(dockerExecutable, "login", "-u", username, "-p").add(password, true).add(endpoint)).envs("DOCKER_CONFIG=" + dockerConfig).stdout(listener).join() != 0) {
throw new AbortException("docker login failed");
}
} catch (IOException | InterruptedException x) {
try {
dockerConfig.deleteRecursive();
} catch (Exception x2) {
x.addSuppressed(x2);
}
throw x;
}
return new RegistryKeyMaterial(dockerConfig, new EnvVars("DOCKER_CONFIG", dockerConfig.getRemote()));
}

private static class RegistryKeyMaterial extends KeyMaterial {

private final FilePath dockerConfig;

RegistryKeyMaterial(FilePath dockerConfig, EnvVars envVars) {
super(envVars);
this.dockerConfig = dockerConfig;
}

@Override
public void close() throws IOException {
try {
dockerConfig.deleteRecursive();
} catch (InterruptedException x) {
// TODO would better have been thrown from KeyMaterial.close to begin with
throw new IOException(x);
}
}

}

}
Expand Up @@ -39,10 +39,6 @@
*/
@Restricted(NoExternalUse.class)
public class ServerHostKeyMaterialFactory extends KeyMaterialFactory{
/**
* Standardize serialization
*/
private static final long serialVersionUID = 1L;

/**
* The host.
Expand Down
Expand Up @@ -33,7 +33,6 @@

import javax.annotation.CheckForNull;
import java.io.IOException;
import java.util.UUID;

/**
* {@link org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory} for talking to docker daemon.
Expand Down Expand Up @@ -80,11 +79,7 @@ public KeyMaterial materialize() throws IOException, InterruptedException {
EnvVars e = new EnvVars();

if (key != null && cert != null && ca != null) {
final FilePath tempCredsDir = new FilePath(getContext().getBaseDir(), UUID.randomUUID().toString());
tempCredsDir.mkdirs();

// protect this information from prying eyes
tempCredsDir.chmod(0700);
FilePath tempCredsDir = createSecretsDirectory();

// these file names are defined by convention by docker
copyInto(tempCredsDir, "key.pem", key);
Expand Down
Expand Up @@ -75,13 +75,11 @@ public String getToolName() {
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// prepare the credentials to talk to this docker and make it available for docker you'll be forking
KeyMaterialFactory keyMaterialFactory = server.newKeyMaterialFactory(build).plus(registry.newKeyMaterialFactory(build));
KeyMaterial key = keyMaterialFactory.materialize();
try {
String dockerExecutable = DockerTool.getExecutable(toolName, build.getBuiltOn(), listener, build.getEnvironment(listener));
KeyMaterialFactory keyMaterialFactory = server.newKeyMaterialFactory(build).plus(registry.newKeyMaterialFactory(build.getParent(), build.getWorkspace(), launcher, listener, dockerExecutable));
try (KeyMaterial key = keyMaterialFactory.materialize()) {
// fork docker with appropriate environment to interact with this docker daemon
return launcher.launch().cmds(DockerTool.getExecutable(toolName, build.getBuiltOn(), listener, build.getEnvironment(listener)), "info").envs(key.env()).join() == 0;
} finally {
key.close();
return launcher.launch().cmds(dockerExecutable, "info").envs(key.env()).join() == 0;
}
}

Expand Down

0 comments on commit a135229

Please sign in to comment.