Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Submitters aggregation for proceeding #14

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package org.jenkinsci.plugins.workflow.support.steps.input;

import com.google.common.collect.Sets;
import hudson.Extension;
import hudson.Util;
import hudson.model.ParameterDefinition;
import jenkins.model.Jenkins;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
* {@link Step} that pauses for human input.
Expand All @@ -32,16 +36,20 @@ public class InputStep extends AbstractStepImpl implements Serializable {
private String id;

/**
* Optional user/group name who can approve this.
* Optional user/group name who can approve this
*/
private String submitter;

/**
* Optional user/group name who did approval (true) or not (false).
*/
private Map<String, Boolean> submittersApprovals;

/**
* Optional parameter name to stored the user who responded to the input.
*/
private String submitterParameter;


/**
* Either a single {@link ParameterDefinition} or a list of them.
*/
Expand Down Expand Up @@ -74,8 +82,28 @@ public String getSubmitter() {
return submitter;
}

public Map<String, Boolean> getSubmittersApprovals() {
return submittersApprovals;
}

@DataBoundSetter public void setSubmitter(String submitter) {
this.submitter = Util.fixEmptyAndTrim(submitter);
this.submittersApprovals = initSubmittersApprovals(this.submitter);
}

private Map<String, Boolean> initSubmittersApprovals(String submitters){
if(submitters == null){
return null;
}
String submitters_list = submitters.replaceAll("[,&|()]", " ").trim();
if(submitters_list.isEmpty()){
return null;
}
Map<String, Boolean> initApprovals = Maps.newHashMap();
for (String u : submitters_list.split("\\s+")){
initApprovals.put(u, false);
}
return initApprovals;
}

public String getSubmitterParameter() { return submitterParameter; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.jenkinsci.plugins.workflow.support.steps.input;

import com.google.common.collect.Sets;
import com.google.inject.Inject;
import hudson.FilePath;
import hudson.Util;
import hudson.console.HyperlinkNote;
Expand All @@ -18,29 +16,34 @@
import hudson.security.ACL;
import hudson.util.HttpResponses;
import jenkins.model.Jenkins;
import jenkins.util.Timer;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.support.actions.PauseAction;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;

import javax.servlet.ServletException;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.util.Timer;

import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.jenkinsci.plugins.workflow.support.actions.PauseAction;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;

import com.google.inject.Inject;

/**
* @author Kohsuke Kawaguchi
Expand Down Expand Up @@ -71,7 +74,7 @@ public boolean start() throws Exception {
node.addAction(new PauseAction("Input"));

String baseUrl = '/' + run.getUrl() + getPauseAction().getUrlName() + '/';
if (input.getParameters().isEmpty()) {
if (input.getParameters().isEmpty() && (input.getSubmitterParameter() == null || input.getSubmitterParameter().isEmpty())) {
String thisUrl = baseUrl + Util.rawEncode(getId()) + '/';
listener.getLogger().printf("%s%n%s or %s%n", input.getMessage(),
POSTHyperlinkNote.encodeTo(thisUrl + "proceedEmpty", input.getOk()),
Expand Down Expand Up @@ -169,20 +172,46 @@ public HttpResponse doProceed(StaplerRequest request) throws IOException, Servle
*/
public HttpResponse proceed(Object v) {
User user = User.current();
if (user != null){
Map<String, Boolean> approvalsMap = this.input.getSubmittersApprovals();
if (user != null) {
run.addAction(new ApproverAction(user.getId()));
listener.getLogger().println("Approved by " + hudson.console.ModelHyperlinkNote.encodeTo(user));
listener.getLogger()
.println("Approved by " + hudson.console.ModelHyperlinkNote.encodeTo(user));
if (approvalsMap != null && !approvalsMap.get(user.getId())) {
approvalsMap.put(user.getId(), true);
} else {
listener.getLogger()
.println(hudson.console.ModelHyperlinkNote.encodeTo(user) + " have already approved");
}
}
if (user == null || approvalsMap == null || evalApprovals()) {
outcome = new Outcome(v, null);
postSettlement();
getContext().onSuccess(v);
} else {
listener.getLogger()
.println("Still wait for others approval for proceeding. The submitters configured is " + this.input.getSubmitter());
}

outcome = new Outcome(v, null);
postSettlement();
getContext().onSuccess(v);

// TODO: record this decision to FlowNode

return HttpResponses.ok();
}

private boolean evalApprovals() {
Map<String, Boolean> approvals = this.input.getSubmittersApprovals();
StringBuffer exprGroovy = new StringBuffer("");
for (Entry entry : approvals.entrySet()) {
exprGroovy.append("boolean ")
.append(entry.getKey())
.append(" = ")
.append(entry.getValue())
.append(" \n");
}
exprGroovy.append(this.input.getSubmitter()
.replaceAll(",", "|"));
return (Boolean) groovy.util.Eval.me(exprGroovy.toString());
}

/**
* Used from the Proceed hyperlink when no parameters are defined.
*/
Expand Down Expand Up @@ -264,12 +293,13 @@ private boolean canSubmit() {
* Checks if the given user can settle this input.
*/
private boolean canSettle(Authentication a) {
String submitter = input.getSubmitter();
if (submitter==null)
if(input.getSubmittersApprovals() == null){
return true;
final Set<String> submitters = Sets.newHashSet(submitter.split(","));
if (submitters.contains(a.getName()))
}
final Set<String> submitters = input.getSubmittersApprovals().keySet();
if(submitters.contains(a.getName())){
return true;
}
for (GrantedAuthority ga : a.getAuthorities()) {
if (submitters.contains(ga.getAuthority()))
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<div>
User IDs and/or <em>external</em> group names of person or people permitted to respond to the input, separated by ','.
If you configure "alice, bob", will match with "alice" but not with "bob". You need to remove all the white spaces.
User IDs and/or <em>external</em> group names of person or people permitted to respond to the input, e.g.:
<br> "alice & bob" - means need both of alice and bob approve for proceeding
<br> "alice | bob" - means need one of alice and bob approve for proceeding
<br> "alice,bob" same as the above "alice | bob" for proceeding
<br> "(alice | bob) & tom" - means need alice and tom approve or bob and tom approve for proceeding
<br><b> So "&" means logical AND, "|" means logical OR, "," same as "|" and you can use "()" to group them.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,15 @@

package org.jenkinsci.plugins.workflow.support.steps.input;

import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.model.BooleanParameterDefinition;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.queue.QueueTaskFuture;
import jenkins.model.Jenkins;


import java.util.Arrays;
import java.util.List;

import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
Expand All @@ -46,10 +43,12 @@
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;

import java.util.Arrays;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

/**
* @author Kohsuke Kawaguchi
*/
Expand Down Expand Up @@ -193,6 +192,8 @@ public void test_submitter_parameter() throws Exception {
// submit the input, and run workflow to the completion
JenkinsRule.WebClient wc = j.createWebClient();
wc.login("alice");
HtmlPage console_page = wc.getPage(b, "console");
assertFalse(console_page.asXml().contains("proceedEmpty"));
HtmlPage p = wc.getPage(b, a.getUrlName());
j.submit(p.getFormByName(is.getId()), "proceed");
assertEquals(0, a.getExecutions().size());
Expand Down Expand Up @@ -279,4 +280,61 @@ private void runAndAbort(JenkinsRule.WebClient webClient, WorkflowJob foo, Strin
j.assertBuildStatus(Result.ABORTED, p.scheduleBuild2(0).get());
}

@Test public void test_submitters_approvals_aggregation() throws Exception {
//set up dummy security real
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
// job setup
WorkflowJob foo = j.jenkins.createProject(WorkflowJob.class, "foo");
foo.setDefinition(new CpsFlowDefinition(StringUtils.join(Arrays.asList(
"def x = input message:'Do you want chocolate?', id:'Icecream', ok: 'Purchase icecream', submitter:'alice & bob', submitterParameter: 'approval';",
"echo(\"after: ${x}\");",
"x = input message:'Do you want chocolate?', id:'Icecream', ok: 'Purchase icecream', submitter:'(john | kate) & tom', submitterParameter: 'approval';",
"echo(\"after: ${x}\");"
),"\n"),true));

// get the build going, and wait until workflow pauses
QueueTaskFuture<WorkflowRun> q = foo.scheduleBuild2(0);
WorkflowRun b = q.getStartCondition().get();
j.waitForMessage("input", b);

// make sure we are pausing at the right state that reflects what we wrote in the program
InputAction a = b.getAction(InputAction.class);
assertEquals(1, a.getExecutions().size());

InputStepExecution is = a.getExecution("Icecream");
assertEquals("Do you want chocolate?", is.getInput().getMessage());
assertEquals("alice & bob", is.getInput().getSubmitter());

// submit the input, and run workflow to the completion
JenkinsRule.WebClient wc = j.createWebClient();
wc.login("alice");
HtmlPage p = wc.getPage(b, a.getUrlName());
j.submit(p.getFormByName(is.getId()), "proceed");

j.assertLogContains("Approved by alice", b);
j.assertLogContains("Still wait for others approval for proceeding. The submitters configured is alice & bob", b);

wc.login("bob");
p = wc.getPage(b, a.getUrlName());
j.submit(p.getFormByName(is.getId()), "proceed");
j.assertLogContains("Approved by bob", b);

j.assertLogContains("after: bob", b);

wc.login("kate");
p = wc.getPage(b, a.getUrlName());
j.submit(p.getFormByName(is.getId()), "proceed");
j.assertLogContains("Approved by kate", b);
j.assertLogContains("Still wait for others approval for proceeding. The submitters configured is (john | kate) & tom", b);

wc.login("tom");
p = wc.getPage(b, a.getUrlName());
j.submit(p.getFormByName(is.getId()), "proceed");
j.assertLogContains("Approved by kate", b);

q.get();
j.assertLogContains("after: tom", b);
}


}