diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStep.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStep.java index 18c4b5e3..29779132 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStep.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStep.java @@ -1,10 +1,16 @@ 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; @@ -12,11 +18,9 @@ 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. @@ -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 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. */ @@ -74,8 +82,28 @@ public String getSubmitter() { return submitter; } + public Map getSubmittersApprovals() { + return submittersApprovals; + } + @DataBoundSetter public void setSubmitter(String submitter) { this.submitter = Util.fixEmptyAndTrim(submitter); + this.submittersApprovals = initSubmittersApprovals(this.submitter); + } + + private Map initSubmittersApprovals(String submitters){ + if(submitters == null){ + return null; + } + String submitters_list = submitters.replaceAll("[,&|()]", " ").trim(); + if(submitters_list.isEmpty()){ + return null; + } + Map initApprovals = Maps.newHashMap(); + for (String u : submitters_list.split("\\s+")){ + initApprovals.put(u, false); + } + return initApprovals; } public String getSubmitterParameter() { return submitterParameter; } diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java index 13ab3137..ded52ba7 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java @@ -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; @@ -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 @@ -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()), @@ -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 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 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. */ @@ -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 submitters = Sets.newHashSet(submitter.split(",")); - if (submitters.contains(a.getName())) + } + final Set submitters = input.getSubmittersApprovals().keySet(); + if(submitters.contains(a.getName())){ return true; + } for (GrantedAuthority ga : a.getAuthorities()) { if (submitters.contains(ga.getAuthority())) return true; diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-submitter.html b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-submitter.html index 2d5415c6..f64a430e 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-submitter.html +++ b/src/main/resources/org/jenkinsci/plugins/workflow/support/steps/input/InputStep/help-submitter.html @@ -1,4 +1,8 @@
- User IDs and/or external 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 external group names of person or people permitted to respond to the input, e.g.: +
"alice & bob" - means need both of alice and bob approve for proceeding +
"alice | bob" - means need one of alice and bob approve for proceeding +
"alice,bob" same as the above "alice | bob" for proceeding +
"(alice | bob) & tom" - means need alice and tom approve or bob and tom approve for proceeding +
So "&" means logical AND, "|" means logical OR, "," same as "|" and you can use "()" to group them.
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest.java index b16b2465..7ad0a691 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepTest.java @@ -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; @@ -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 */ @@ -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()); @@ -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 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); + } + + }