Skip to content
Permalink
Browse files
[JENKINS-26051] First draft of withCredentials step.
  • Loading branch information
jglick committed Jan 12, 2015
1 parent 91e5d8e commit f23dd15d1098d1d2873f6f8c1c97a9cee82c9c53
14 pom.xml
@@ -17,6 +17,9 @@
<description>Allows credentials to be bound to environment variables for use from miscellaneous build steps.
</description>
<url>http://wiki.jenkins-ci.org/display/JENKINS/Credentials+Binding+Plugin</url>
<properties>
<workflow.version>1.1</workflow.version>
</properties>
<licenses>
<license>
<name>MIT</name>
@@ -55,6 +58,17 @@
<artifactId>plain-credentials</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>${workflow.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-aggregator</artifactId>
<version>${workflow.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
@@ -35,6 +35,7 @@

import hudson.model.Run;
import hudson.model.TaskListener;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@@ -60,6 +61,17 @@ public String getVariable() {
}

/** Callback for processing during a build. */
public interface SingleEnvironment extends Serializable {

/** Produces the value of the environment variable. */
String value();

/** Performs any needed cleanup. */
void unbind(@Nonnull Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException;

}

@Deprecated
public interface Environment {

/** Produces the value of the environment variable. */
@@ -72,27 +84,48 @@ public String getVariable() {

@Deprecated
@SuppressWarnings("rawtypes")
public Environment bind(@Nonnull AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
return bindSingle(build, build.getWorkspace(), launcher, listener);
public Environment bind(@Nonnull final AbstractBuild build, final Launcher launcher, final BuildListener listener) throws IOException, InterruptedException {
final SingleEnvironment e = bindSingle(build, build.getWorkspace(), launcher, listener);
return new Environment() {
@Override public String value() {
return e.value();
}
@Override public void unbind() throws IOException, InterruptedException {
e.unbind(build, build.getWorkspace(), launcher, listener);
}
};
}

/** Sets up bindings for a build. */
public /* abstract */Environment bindSingle(@Nonnull Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
public /* abstract */SingleEnvironment bindSingle(@Nonnull Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
if (Util.isOverridden(Binding.class, getClass(), "bind", AbstractBuild.class, Launcher.class, BuildListener.class) && build instanceof AbstractBuild && listener instanceof BuildListener) {
return bind((AbstractBuild) build, launcher, (BuildListener) listener);
return new EnvironmentWrapper(bind((AbstractBuild) build, launcher, (BuildListener) listener));
} else {
throw new AbstractMethodError("you must override bindSingle");
}
}
private static class EnvironmentWrapper implements SingleEnvironment {
private static final long serialVersionUID = 1; // only really serialize if what it wraps is, too
private final Environment e;
EnvironmentWrapper(Environment e) {
this.e = e;
}
@Override public String value() {
return e.value();
}
@Override public void unbind(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
e.unbind();
}
}

@Override public final MultiEnvironment bind(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
final Environment single = bindSingle(build, workspace, launcher, listener);
final SingleEnvironment single = bindSingle(build, workspace, launcher, listener);
return new MultiEnvironment() {
public Map<String,String> values() {
@Override public Map<String,String> values() {
return Collections.singletonMap(variable, single.value());
}
public void unbind() throws IOException, InterruptedException {
single.unbind();
@Override public void unbind(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
single.unbind(build, workspace, launcher, listener);
}
};
}
@@ -34,6 +34,7 @@
import hudson.model.TaskListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
@@ -60,14 +61,15 @@ public final String getCredentialsId() {
return credentialsId;
}

// TODO perhaps this should be split into a struct with a values map, accessible immediately, and an unbinder, which needs to hold serializable state?
/** Callback for processing during a build. */
public interface MultiEnvironment {
public interface MultiEnvironment extends Serializable {

/** Produces the value of the environment variables. */
Map<String,String> values();

/** Performs any needed cleanup. */
void unbind() throws IOException, InterruptedException;
void unbind(@Nonnull Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException;

}

@@ -0,0 +1,144 @@
/*
* The MIT License
*
* Copyright 2015 Jesse Glick.
*
* 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.credentialsbinding.impl;

import com.google.inject.Inject;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Run;
import hudson.model.TaskListener;
import java.util.ArrayList;
import java.util.List;
import org.jenkinsci.plugins.credentialsbinding.MultiBinding;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.kohsuke.stapler.DataBoundConstructor;

/**
* Workflow step to bind credentials.
*/
public final class BindingStep extends AbstractStepImpl {

private final List<? extends MultiBinding<?>> bindings;

@DataBoundConstructor public BindingStep(List<? extends MultiBinding<?>> bindings) {
this.bindings = bindings;
}

public List<? extends MultiBinding<?>> getBindings() {
return bindings;
}

public static final class Execution extends AbstractStepExecutionImpl {

private static final long serialVersionUID = 1;

@Inject(optional=true) private transient BindingStep step;
@StepContextParameter private transient Run<?,?> run;
@StepContextParameter private transient FilePath workspace;
@StepContextParameter private transient Launcher launcher;
@StepContextParameter private transient TaskListener listener;
// TODO ideally we would like to just create a fresh EnvVars with only our own bindings.
// But DefaultStepContext has no notion of merging multiple EnvVars instances across nested scopes.
// So we need to pick up the bindings created by ExecutorStepExecution and append to them.
// This has the unfortunate effect of “freezing” any custom values set via EnvActionImpl.setProperty for the duration of this step,
// because these will also be present in the inherited EnvVars.
@StepContextParameter private transient EnvVars env;

@Override public boolean start() throws Exception {
EnvVars overrides = new EnvVars(env);
List<MultiBinding.MultiEnvironment> environments = new ArrayList<MultiBinding.MultiEnvironment>();
for (MultiBinding<?> binding : step.bindings) {
MultiBinding.MultiEnvironment environment = binding.bind(run, workspace, launcher, listener);
environments.add(environment);
overrides.putAll(environment.values());
}
getContext().newBodyInvoker().withContext(overrides).withCallback(new Callback(environments)).start();
return false;
}

@Override public void stop(Throwable cause) throws Exception {
// should be no need to do anything special (but verify in JENKINS-26148)
}

// TODO in case [Workflow]Run gets some equivalent to getSensitiveBuildVariables, this should be implemented here somehow

}

private static final class Callback extends BodyExecutionCallback {

private static final long serialVersionUID = 1;

private final List<MultiBinding.MultiEnvironment> environments;

Callback(List<MultiBinding.MultiEnvironment> environments) {
this.environments = environments;
}

private void cleanup(StepContext context) {
for (MultiBinding.MultiEnvironment environment : environments) {
try {
environment.unbind(context.get(Run.class), context.get(FilePath.class), context.get(Launcher.class), context.get(TaskListener.class));
} catch (Exception x) {
context.onFailure(x);
}
}
}

@Override public void onSuccess(StepContext context, Object result) {
cleanup(context);
context.onSuccess(result);
}

@Override public void onFailure(StepContext context, Throwable t) {
cleanup(context);
context.onFailure(t);
}

}

@Extension public static final class DescriptorImpl extends AbstractStepDescriptorImpl {

public DescriptorImpl() {
super(Execution.class);
}

@Override public String getFunctionName() {
return "withCredentials";
}

@Override public String getDisplayName() {
return "Bind credentials to variables";
}

}

}
@@ -49,25 +49,44 @@
return FileCredentials.class;
}

@Override public Environment bindSingle(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
@Override public SingleEnvironment bindSingle(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
FileCredentials credentials = getCredentials(build);
Computer computer = workspace.toComputer();
Node node = computer == null ? null : computer.getNode();
FilePath root = node == null ? workspace : node.getRootPath();
FilePath secrets = root.child("secretFiles");
final FilePath dir = secrets.child(UUID.randomUUID().toString());
FilePath secrets = secretsDir(workspace);
String dirName = UUID.randomUUID().toString();
final FilePath dir = secrets.child(dirName);
dir.mkdirs();
secrets.chmod(/*0700*/448);
final FilePath secret = dir.child(credentials.getFileName());
FilePath secret = dir.child(credentials.getFileName());
copy(secret, credentials);
return new Environment() {
@Override public String value() {
return secret.getRemote();
}
@Override public void unbind() throws IOException, InterruptedException {
dir.deleteRecursive();
}
};
return new EnvironmentImpl(dirName, secret.getRemote());
}

private static class EnvironmentImpl implements SingleEnvironment {

private static final long serialVersionUID = 1;

private final String dirName, credentialsFileName;

EnvironmentImpl(String dirName, String credentialsFileName) {
this.dirName = dirName;
this.credentialsFileName = credentialsFileName;
}

@Override public String value() {
return credentialsFileName;
}

@Override public void unbind(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
secretsDir(workspace).child(dirName).deleteRecursive();
}

}

private static FilePath secretsDir(FilePath workspace) {
Computer computer = workspace.toComputer();
Node node = computer == null ? null : computer.getNode();
FilePath root = node == null ? workspace : node.getRootPath();
return root.child("secretFiles");
}

protected void copy(FilePath secret, FileCredentials credentials) throws IOException, InterruptedException {
@@ -52,7 +52,7 @@
return bindings;
}

@Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
@Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
final List<MultiBinding.MultiEnvironment> m = new ArrayList<MultiBinding.MultiEnvironment>();
for (MultiBinding binding : bindings) {
m.add(binding.bind(build, build.getWorkspace(), launcher, listener));
@@ -65,7 +65,7 @@
}
@Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {
for (MultiBinding.MultiEnvironment e : m) {
e.unbind();
e.unbind(build, build.getWorkspace(), launcher, listener);
}
return true;
}
@@ -46,14 +46,26 @@
return StringCredentials.class;
}

@Override public Environment bindSingle(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
final String value = getCredentials(build).getSecret().getPlainText();
return new Environment() {
@Override public String value() {
return value;
}
@Override public void unbind() throws IOException, InterruptedException {}
};
@Override public SingleEnvironment bindSingle(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
return new EnvironmentImpl(getCredentials(build).getSecret().getPlainText());
}

private static class EnvironmentImpl implements SingleEnvironment {

private static final long serialVersionUID = 1;

private final String value;

EnvironmentImpl(String value) {
this.value = value;
}

@Override public String value() {
return value;
}

@Override public void unbind(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {}

}

@Extension public static class DescriptorImpl extends BindingDescriptor<StringCredentials> {

0 comments on commit f23dd15

Please sign in to comment.