diff --git a/pom.xml b/pom.xml index 5665c6e..29b78ea 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,11 @@ 3.0.1 provided + + org.jenkins-ci.plugins + credentials + 2.1.16 + diff --git a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/AbstractDockerLauncher.java b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/AbstractDockerLauncher.java index a09bb78..74b620d 100644 --- a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/AbstractDockerLauncher.java +++ b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/AbstractDockerLauncher.java @@ -26,6 +26,7 @@ import com.gpuopenanalytics.jenkins.remotedocker.job.DockerConfiguration; import hudson.EnvVars; +import hudson.FilePath; import hudson.Launcher; import hudson.Proc; import hudson.util.ArgumentListBuilder; @@ -45,6 +46,7 @@ public abstract class AbstractDockerLauncher extends Launcher.DecoratedLauncher private DockerState dockerState; private DockerVersion version; + private FilePath loginTempDir; protected AbstractDockerLauncher(@Nonnull Launcher launcher) { super(launcher); @@ -167,6 +169,9 @@ public Launcher.ProcStarter executeCommand(ArgumentListBuilder args) { if (!"docker".equals(args.toList().get(0))) { args.prepend("docker"); } + if (loginTempDir != null) { + args.prepend("env", "HOME=" + loginTempDir.getRemote()); + } return getInner().launch() //TODO I think we should pass something here //.envs() @@ -223,6 +228,7 @@ private DockerVersion parseVersion() throws IOException, InterruptedException { */ void configure(DockerState dockerState) { this.dockerState = dockerState; + configureTempDir(dockerState.getLoginTempDir()); } /** @@ -233,4 +239,8 @@ void configure(DockerState dockerState) { protected DockerState getDockerState() { return dockerState; } + + public void configureTempDir(FilePath loginTempDir) { + this.loginTempDir = loginTempDir; + } } diff --git a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/DockerState.java b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/DockerState.java index fd82da2..9051c86 100644 --- a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/DockerState.java +++ b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/DockerState.java @@ -24,6 +24,7 @@ package com.gpuopenanalytics.jenkins.remotedocker; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; import com.google.common.collect.ImmutableList; import com.gpuopenanalytics.jenkins.remotedocker.job.DockerConfiguration; import com.gpuopenanalytics.jenkins.remotedocker.job.SideDockerConfiguration; @@ -34,6 +35,7 @@ import hudson.slaves.WorkspaceList; import hudson.util.ArgumentListBuilder; import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -58,17 +60,20 @@ public class DockerState implements Serializable { private ImmutableList containerIds; private String networkId; private boolean removeContainers; + private FilePath loginTempDir; public DockerState(boolean debug, String mainContainerId, Collection containerIds, Optional network, - boolean removeContainers) { + boolean removeContainers, + FilePath loginTempDir) { this.debug = debug; this.mainContainerId = mainContainerId; this.containerIds = ImmutableList.copyOf(containerIds); this.networkId = network.map(DockerNetwork::getId).orElse(null); this.removeContainers = removeContainers; + this.loginTempDir = loginTempDir; } private int execute(Launcher launcher, @@ -84,13 +89,13 @@ private int execute(Launcher launcher, return status; } - public void tearDown(Launcher launcher) throws IOException, InterruptedException { + public void tearDown(AbstractDockerLauncher launcher) throws IOException, InterruptedException { if (removeContainers) { TaskListener listener = launcher.getListener(); for (String containerId : containerIds) { ArgumentListBuilder args = new ArgumentListBuilder() .add("docker", "rm", "-f", containerId); - int status = execute(launcher, args); + int status = execute(launcher.getInner(), args); if (status != 0) { listener.error("Failed to remove container %s", containerId); @@ -99,12 +104,69 @@ public void tearDown(Launcher launcher) throws IOException, InterruptedException if (networkId != null) { ArgumentListBuilder args = new ArgumentListBuilder() .add("docker", "network", "rm", networkId); - int status = execute(launcher, args); + int status = execute(launcher.getInner(), args); if (status != 0) { listener.error("Failed to remove network %s", networkId); } } } + logout(launcher.getInner()); + } + + private void logout(Launcher launcher) throws IOException, InterruptedException { + if (loginTempDir != null) { + ArgumentListBuilder args = new ArgumentListBuilder("env", + "HOME=" + loginTempDir + .getRemote(), + "docker", + "logout"); + int status = execute(launcher, args); + if (status != 0) { + launcher.getListener().error("Failed to docker logout"); + } + } + } + + /** + * Attempt to docker login. Returns the temporary directory to + * use for HOME to store docker credentials. + * + * @param buildWrapper + * @param launcher + * @param workspace + * @return + * @throws IOException + * @throws InterruptedException + */ + private static FilePath login(RemoteDockerBuildWrapper buildWrapper, + AbstractDockerLauncher launcher, + FilePath workspace) throws IOException, InterruptedException { + if (buildWrapper.getCredentialsId() != null) { + UsernamePasswordCredentials creds = buildWrapper.getCredentials(); + FilePath tempDir = WorkspaceList.tempDir(workspace); + launcher.configureTempDir(tempDir); + ArgumentListBuilder args = new ArgumentListBuilder("docker", + "login"); + args.add("-u", creds.getUsername()); + args.add("-p"); + args.addMasked(creds.getPassword()); + if (!StringUtils.isEmpty(buildWrapper.getDockerRegistryUrl())) { + args.add(buildWrapper.getDockerRegistryUrl()); + } + Launcher.ProcStarter cmd = launcher.executeCommand(args); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + cmd.stdout(launcher.getListener()); + cmd.stderr(baos); + int status = cmd.join(); + if (status != 0) { + launcher.getListener().error("Failed to docker login"); + launcher.getListener().error(baos.toString()); + throw new RuntimeException( + "Could not docker login. Are your credentials correct?"); + } + return tempDir; + } + return null; } /** @@ -116,6 +178,8 @@ public void tearDown(Launcher launcher) throws IOException, InterruptedException public static DockerState launchContainers(RemoteDockerBuildWrapper buildWrapper, AbstractDockerLauncher launcher, FilePath workspace) throws IOException, InterruptedException { + FilePath loginTempDir = login(buildWrapper, launcher, workspace); + Optional network = Optional.empty(); if (!buildWrapper.getSideDockerConfigurations().isEmpty()) { //There are side container, so create a network @@ -124,14 +188,16 @@ public static DockerState launchContainers(RemoteDockerBuildWrapper buildWrapper List containerIds = new ArrayList<>(); //Launch side containers first for (SideDockerConfiguration side : buildWrapper.getSideDockerConfigurations()) { - String id = launchContainer(buildWrapper, side, false, launcher, workspace, + String id = launchContainer(buildWrapper, side, false, launcher, + workspace, network); containerIds.add(id); } //Launch main container DockerConfiguration main = buildWrapper.getDockerConfiguration(); - String mainId = launchContainer(buildWrapper, main, true, launcher, workspace, + String mainId = launchContainer(buildWrapper, main, true, launcher, + workspace, network); containerIds.add(mainId); Collections.reverse(containerIds); @@ -139,7 +205,8 @@ public static DockerState launchContainers(RemoteDockerBuildWrapper buildWrapper DockerState dockerState = new DockerState(buildWrapper.isDebug(), mainId, containerIds, network, - buildWrapper.isRemoveContainers()); + buildWrapper.isRemoveContainers(), + loginTempDir); launcher.configure(dockerState); return dockerState; } @@ -159,8 +226,8 @@ private static ArgumentListBuilder getlaunchArgs(RemoteDockerBuildWrapper buildW Optional network) throws IOException, InterruptedException { String workspacePath = workspace.getRemote(); String workspaceTarget = Optional.ofNullable( - buildWrapper.getWorkspaceOverride()) - .orElse(workspacePath); + buildWrapper.getWorkspaceOverride()) + .orElse(workspacePath); //Fully resolve the source workspace String workspaceSrc = Paths.get(workspacePath) .toAbsolutePath() @@ -212,7 +279,8 @@ private static String launchContainer(RemoteDockerBuildWrapper buildWrapper, AbstractDockerLauncher launcher, FilePath workspace, Optional network) throws IOException, InterruptedException { - ArgumentListBuilder args = getlaunchArgs(buildWrapper, config, isMain, launcher, + ArgumentListBuilder args = getlaunchArgs(buildWrapper, config, isMain, + launcher, workspace, network); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int status = launcher.executeCommand(args) @@ -231,7 +299,8 @@ private static String launchContainer(RemoteDockerBuildWrapper buildWrapper, containerId, ImmutableList.of(containerId), Optional.empty(), - false); + false, + null); launcher.configure(tempState); config.postCreate(launcher); return containerId; @@ -258,4 +327,7 @@ public Optional getNetworkId() { return Optional.ofNullable(networkId); } + public FilePath getLoginTempDir() { + return loginTempDir; + } } diff --git a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper.java b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper.java index e94f950..39218ac 100644 --- a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper.java +++ b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper.java @@ -24,6 +24,12 @@ package com.gpuopenanalytics.jenkins.remotedocker; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import com.gpuopenanalytics.jenkins.remotedocker.job.AbstractDockerConfiguration; import com.gpuopenanalytics.jenkins.remotedocker.job.AbstractDockerConfigurationDescriptor; import com.gpuopenanalytics.jenkins.remotedocker.job.DockerImageConfiguration; @@ -34,17 +40,23 @@ import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Descriptor; +import hudson.model.Item; import hudson.model.Run; +import hudson.security.ACL; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; +import hudson.util.ListBoxModel; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -59,6 +71,9 @@ public class RemoteDockerBuildWrapper extends BuildWrapper { private static final String WORKSPACE_OVERRIDE_FIELD = "workspaceOverride"; private static final String WORKSPACE_OVERRIDE_OPTIONAL_FIELD = "workspaceOverrideOptional"; + private static final String DOCKER_REGISTRY_URL_FIELD = "dockerRegistryUrl"; + private static final String CREDENTIALS_ID_FIELD = "credentialsId"; + private static final String DOCKER_LOGIN_OPTIONAL_FIELD = "dockerLoginOptional"; private boolean debug; private String workspaceOverride; @@ -66,11 +81,16 @@ public class RemoteDockerBuildWrapper extends BuildWrapper { private AbstractDockerConfiguration dockerConfiguration; private List sideDockerConfigurations; + private String dockerRegistryUrl; + private String credentialsId; + @DataBoundConstructor public RemoteDockerBuildWrapper(boolean debug, String workspaceOverride, AbstractDockerConfiguration dockerConfiguration, - List sideDockerConfigurations) { + List sideDockerConfigurations, + String dockerRegistryUrl, + String credentialsId) { this.debug = debug; this.workspaceOverride = StringUtils.isNotEmpty( workspaceOverride) ? workspaceOverride : null; @@ -78,6 +98,8 @@ public RemoteDockerBuildWrapper(boolean debug, this.sideDockerConfigurations = Optional.ofNullable( sideDockerConfigurations) .orElse(Collections.emptyList()); + this.dockerRegistryUrl = dockerRegistryUrl; + this.credentialsId = credentialsId; } public boolean isDebug() { @@ -105,6 +127,27 @@ public List getSideDockerConfigurations() { return sideDockerConfigurations; } + public UsernamePasswordCredentials getCredentials() { + List allCredentials = CredentialsProvider + .lookupCredentials(UsernamePasswordCredentials.class, + Jenkins.get(), + ACL.SYSTEM, Collections.emptyList()); + UsernamePasswordCredentials credentials = CredentialsMatchers.firstOrNull( + allCredentials, + CredentialsMatchers.allOf( + CredentialsMatchers + .withId(credentialsId))); + return credentials; + } + + public String getDockerRegistryUrl() { + return dockerRegistryUrl; + } + + public String getCredentialsId() { + return credentialsId; + } + private void validate() throws Descriptor.FormException { if (StringUtils.isNotEmpty(workspaceOverride) && !workspaceOverride.startsWith("/")) { @@ -152,7 +195,7 @@ public DockerEnvironment(DockerLauncher launcher, @Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { - dockerState.tearDown(launcher.getInner()); + dockerState.tearDown(launcher); return true; } } @@ -180,6 +223,11 @@ public BuildWrapper newInstance(StaplerRequest req, //If the box is unchecked, override whatever value might have been entered formData.remove(WORKSPACE_OVERRIDE_FIELD); } + if (!formData.getBoolean(DOCKER_LOGIN_OPTIONAL_FIELD)) { + //If the box is unchecked, delete registry & credentials + formData.remove(DOCKER_REGISTRY_URL_FIELD); + formData.remove(CREDENTIALS_ID_FIELD); + } RemoteDockerBuildWrapper wrapper = (RemoteDockerBuildWrapper) super.newInstance( req, formData); wrapper.validate(); @@ -198,6 +246,21 @@ public Descriptor getDefaultDockerConfigurationDescriptor() { return Jenkins.get().getDescriptorOrDie( DockerImageConfiguration.class); } + + public ListBoxModel doFillCredentialsIdItems( + @AncestorInPath Jenkins context, + @QueryParameter String credentialsId) { + if (context == null || !context.hasPermission(Item.CONFIGURE)) { + return new StandardListBoxModel(); + } + + List domainRequirements = new ArrayList<>(); + return new StandardListBoxModel() + .includeEmptyValue() + .includeAs(ACL.SYSTEM, Jenkins.get(), + UsernamePasswordCredentialsImpl.class) + .includeCurrentValue(credentialsId); + } } } diff --git a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/DockerLauncherDecorator.java b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/DockerLauncherDecorator.java index 8566ee8..4cf2a1b 100644 --- a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/DockerLauncherDecorator.java +++ b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/DockerLauncherDecorator.java @@ -24,6 +24,7 @@ package com.gpuopenanalytics.jenkins.remotedocker.pipeline; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; import com.gpuopenanalytics.jenkins.remotedocker.AbstractDockerLauncher; import com.gpuopenanalytics.jenkins.remotedocker.DockerState; import com.gpuopenanalytics.jenkins.remotedocker.job.DockerConfiguration; @@ -34,6 +35,7 @@ import hudson.model.Node; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.io.Serializable; @@ -47,17 +49,20 @@ public class DockerLauncherDecorator extends LauncherDecorator implements Serial private DockerConfiguration dockerConfiguration; private EnvVars environment; private String workspaceOverride; + private UsernamePasswordCredentials credentials; public DockerLauncherDecorator(boolean debug, DockerState dockerState, DockerConfiguration dockerConfiguration, EnvVars environment, - String workspaceOverride) { + String workspaceOverride, + @Nullable UsernamePasswordCredentials credentials) { this.debug = debug; this.dockerState = dockerState; this.dockerConfiguration = dockerConfiguration; this.environment = environment; this.workspaceOverride = workspaceOverride; + this.credentials=credentials; } @Nonnull diff --git a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStep.java b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStep.java index d579b93..4d9dde4 100644 --- a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStep.java +++ b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStep.java @@ -57,12 +57,16 @@ public class RemoteDockerStep extends Step { private List sideContainers; private String workspaceOverride; private Boolean removeContainers = true; + private String registryUrl; + private String credentialsId; @DataBoundConstructor public RemoteDockerStep(boolean debug, AbstractDockerConfiguration main, List sideContainers, - String workspaceOverride) { + String workspaceOverride, + String registryUrl, + String credentialsId) { this.debug = debug; this.main = main; this.sideContainers = ImmutableList.copyOf( @@ -70,6 +74,8 @@ public RemoteDockerStep(boolean debug, .orElse(Collections.emptyList())); this.workspaceOverride = StringUtils.isNotEmpty( workspaceOverride) ? workspaceOverride : null; + this.registryUrl = registryUrl; + this.credentialsId = credentialsId; } @Override @@ -102,6 +108,14 @@ public String getWorkspaceOverride() { return workspaceOverride; } + public String getRegistryUrl() { + return registryUrl; + } + + public String getCredentialsId() { + return credentialsId; + } + @Extension public static final class DescriptorImpl extends StepDescriptor { diff --git a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStepExecution.java b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStepExecution.java index cc2dd56..754d6b5 100644 --- a/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStepExecution.java +++ b/src/main/java/com/gpuopenanalytics/jenkins/remotedocker/pipeline/RemoteDockerStepExecution.java @@ -24,6 +24,8 @@ package com.gpuopenanalytics.jenkins.remotedocker.pipeline; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.gpuopenanalytics.jenkins.remotedocker.AbstractDockerLauncher; import com.gpuopenanalytics.jenkins.remotedocker.DockerState; import com.gpuopenanalytics.jenkins.remotedocker.RemoteDockerBuildWrapper; import com.gpuopenanalytics.jenkins.remotedocker.SimpleDockerLauncher; @@ -32,6 +34,7 @@ import hudson.FilePath; import hudson.Launcher; import hudson.LauncherDecorator; +import hudson.Proc; import org.jenkinsci.plugins.workflow.steps.BodyExecution; import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; import org.jenkinsci.plugins.workflow.steps.BodyInvoker; @@ -68,12 +71,16 @@ public boolean start() throws Exception { remoteDockerStep.isDebug(), remoteDockerStep.getWorkspaceOverride(), remoteDockerStep.getMain(), - remoteDockerStep.getSideContainers()); + remoteDockerStep.getSideContainers(), + remoteDockerStep.getRegistryUrl(), + remoteDockerStep.getCredentialsId()); buildWrapper.setRemoveContainers(remoteDockerStep.isRemoveContainers()); Launcher launcher = getContext().get(Launcher.class); FilePath workspace = getContext().get(FilePath.class); EnvVars environment = getContext().get(EnvVars.class); + UsernamePasswordCredentials credentials = getContext().get( + UsernamePasswordCredentials.class); SimpleDockerLauncher simpleDockerLauncher = new SimpleDockerLauncher( launcher, buildWrapper.isDebug(), environment, buildWrapper); @@ -87,7 +94,8 @@ public boolean start() throws Exception { dockerState, remoteDockerStep.getMain(), environment, - remoteDockerStep.getWorkspaceOverride()); + remoteDockerStep.getWorkspaceOverride(), + credentials); LauncherDecorator launcherDecorator = BodyInvoker.mergeLauncherDecorators( getContext().get(LauncherDecorator.class), @@ -108,7 +116,7 @@ public void stop(@Nonnull Throwable cause) throws Exception { } if (dockerState != null) { Launcher launcher = getContext().get(Launcher.class); - dockerState.tearDown(launcher); + dockerState.tearDown(createLauncher(launcher, dockerState)); } } @@ -124,6 +132,27 @@ public String getStatus() { return null; } + private static AbstractDockerLauncher createLauncher(Launcher launcher, + DockerState dockerState) { + return new AbstractDockerLauncher(launcher, dockerState) { + @Override + public Proc dockerExec(ProcStarter starter, + boolean addRunArgs) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public EnvVars getEnvironment() { + return new EnvVars(); + } + + @Override + public boolean isDebug() { + return dockerState.isDebug(); + } + }; + } + /** * Callback to execute at the end of the step's body. Basically, just * shutdown the containers @@ -140,7 +169,7 @@ public Callback(DockerState dockerState) { public void onSuccess(StepContext context, Object result) { try { Launcher launcher = context.get(Launcher.class); - dockerState.tearDown(launcher); + dockerState.tearDown(createLauncher(launcher, dockerState)); context.onSuccess(result); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); @@ -151,7 +180,7 @@ public void onSuccess(StepContext context, Object result) { public void onFailure(StepContext context, Throwable t) { try { Launcher launcher = context.get(Launcher.class); - dockerState.tearDown(launcher); + dockerState.tearDown(createLauncher(launcher, dockerState)); context.onFailure(t); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); diff --git a/src/main/resources/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper/config.jelly b/src/main/resources/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper/config.jelly index 7445953..e23e9c4 100644 --- a/src/main/resources/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper/config.jelly +++ b/src/main/resources/com/gpuopenanalytics/jenkins/remotedocker/RemoteDockerBuildWrapper/config.jelly @@ -24,7 +24,8 @@ --> + xmlns:f="/lib/form" + xmlns:c="/lib/credentials"> - + + + + + + + + +