Skip to content

Commit

Permalink
Merge pull request #3 from jenkinsci/workflow-JENKINS-26051
Browse files Browse the repository at this point in the history
[FIXED JENKINS-26051] withCredentials step
  • Loading branch information
jglick committed Jan 20, 2015
2 parents 91e5d8e + 615c102 commit 15908db
Show file tree
Hide file tree
Showing 12 changed files with 489 additions and 67 deletions.
35 changes: 35 additions & 0 deletions pom.xml
Expand Up @@ -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>
Expand Down Expand Up @@ -55,6 +58,38 @@
<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>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>${workflow.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>${workflow.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<!-- For some reason this is otherwise missing and causes many verbose errors: -->
<dependency>
<groupId>org.jenkins-ci.modules</groupId>
<artifactId>sshd</artifactId>
<version>1.6</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
60 changes: 45 additions & 15 deletions src/main/java/org/jenkinsci/plugins/credentialsbinding/Binding.java
Expand Up @@ -36,7 +36,6 @@
import hudson.model.Run;
import hudson.model.TaskListener;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.kohsuke.stapler.DataBoundConstructor;

Expand All @@ -59,7 +58,25 @@ public String getVariable() {
return variable;
}

/** Callback for processing during a build. */
/** Result of {@link #bindSingle}. */
@SuppressWarnings("PublicConstructorInNonPublicClass") // intended to be called from a subclass of Binding
protected static final class SingleEnvironment {

final String value;
final Unbinder unbinder;

public SingleEnvironment(String value) {
this(value, new NullUnbinder());
}

public SingleEnvironment(String value, Unbinder unbinder) {
this.value = value;
this.unbinder = unbinder;
}

}

@Deprecated
public interface Environment {

/** Produces the value of the environment variable. */
Expand All @@ -72,29 +89,42 @@ public interface Environment {

@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.unbinder.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);
Environment e = bind((AbstractBuild) build, launcher, (BuildListener) listener);
return new SingleEnvironment(e.value(), new UnbinderWrapper(e));
} else {
throw new AbstractMethodError("you must override bindSingle");
}
}
private static class UnbinderWrapper implements Unbinder {
private static final long serialVersionUID = 1; // only really serialize if what it wraps is, too
private final Environment e;
UnbinderWrapper(Environment e) {
this.e = e;
}
@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);
return new MultiEnvironment() {
public Map<String,String> values() {
return Collections.singletonMap(variable, single.value());
}
public void unbind() throws IOException, InterruptedException {
single.unbind();
}
};
SingleEnvironment single = bindSingle(build, workspace, launcher, listener);
return new MultiEnvironment(Collections.singletonMap(variable, single.value), single.unbinder);
}

@Override public final Set<String> variables() {
Expand Down
Expand Up @@ -34,6 +34,9 @@
import hudson.model.TaskListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
Expand All @@ -60,21 +63,47 @@ public final String getCredentialsId() {
return credentialsId;
}

/** Callback for processing during a build. */
public interface MultiEnvironment {
/** Result of {@link #bind}. */
public static final class MultiEnvironment implements Serializable {

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

public MultiEnvironment(Map<String,String> values) {
this(values, new NullUnbinder());
}

public MultiEnvironment(Map<String,String> values, Unbinder unbinder) {
this.values = new HashMap<String,String>(values);
this.unbinder = unbinder;
}

public Map<String,String> getValues() {
return Collections.unmodifiableMap(values);
}

public Unbinder getUnbinder() {
return unbinder;
}

}

/** Callback run at the end of a build. */
public interface Unbinder extends Serializable {
/** Performs any needed cleanup. */
void unbind() throws IOException, InterruptedException;
void unbind(@Nonnull Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException;
}

/** No-op callback. */
protected static final class NullUnbinder implements Unbinder {
private static final long serialVersionUID = 1;
@Override public void unbind(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {}
}

/** Sets up bindings for a build. */
public abstract MultiEnvironment bind(@Nonnull Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException;

/** Defines keys expected to be set in {@link MultiEnvironment#values}, particularly any that might be sensitive. */
/** Defines keys expected to be set in {@link MultiEnvironment#getValues}, particularly any that might be sensitive. */
public abstract Set<String> variables();

/**
Expand Down
@@ -0,0 +1,149 @@
/*
* 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.
*/
@SuppressWarnings("rawtypes") // TODO DescribableHelper does not yet seem to handle List<? extends MultiBinding<?>> or even List<MultiBinding<?>>
public final class BindingStep extends AbstractStepImpl {

private final List<MultiBinding> bindings;

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

public List<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.Unbinder> unbinders = new ArrayList<MultiBinding.Unbinder>();
for (MultiBinding<?> binding : step.bindings) {
MultiBinding.MultiEnvironment environment = binding.bind(run, workspace, launcher, listener);
unbinders.add(environment.getUnbinder());
overrides.putAll(environment.getValues());
}
getContext().newBodyInvoker().withContext(overrides).withCallback(new Callback(unbinders)).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.Unbinder> unbinders;

Callback(List<MultiBinding.Unbinder> unbinders) {
this.unbinders = unbinders;
}

private void cleanup(StepContext context) {
for (MultiBinding.Unbinder unbinder : unbinders) {
try {
unbinder.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";
}

@Override public boolean takesImplicitBlockArgument() {
return true;
}

}

}

0 comments on commit 15908db

Please sign in to comment.