From 8dd3245b1f65ee0eb1e7a9a718c47d673f8d4eae Mon Sep 17 00:00:00 2001 From: karthikjayaraman80 <48958618+karthikjayaraman80@users.noreply.github.com> Date: Thu, 10 Sep 2020 21:33:22 -0400 Subject: [PATCH 1/4] Supporting Multiple Deployments in a single build Consider a job that deploys to multiple environments. The View only shows the details of the first environment. The change enables all the environment deployments for a single build. Sample Jenkins pipeline that deploys to multiple environments: pipeline { agent any stages { stage('Deploy to DEV') { steps { input message: 'Deploy to DEV' addDeployToDashboard(env: 'DEV', buildNumber: "$BUILD_NUMBER") } } stage('Deploy to QA') { steps { input message: 'Deploy to QA' addDeployToDashboard(env: 'QA', buildNumber: "$BUILD_NUMBER") } } stage('Deploy to PROD') { steps { input message: 'Deploy to PROD' addDeployToDashboard(env: 'PROD', buildNumber: "$BUILD_NUMBER") } } } } Signed-off-by: Karthik Jayaraman --- .../jenkinsci/plugins/environmentdashboard/DeploymentView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java index 7a26624..4859925 100644 --- a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java @@ -48,7 +48,8 @@ private List getEnvs(TopLevelItem item) { return runs .stream() - .map(run -> run.getAction(DeploymentAction.class)) + .map(run -> run.getActions(DeploymentAction.class)) + .flatMap(List::stream) .filter(Objects::nonNull) .collect(Collectors.groupingBy(DeploymentAction::getEnv)) .entrySet() From 9ec3c3a60df2e67681174370df1a7b7a617a8c85 Mon Sep 17 00:00:00 2001 From: Karthik Jayaraman Date: Fri, 11 Sep 2020 13:54:33 -0400 Subject: [PATCH 2/4] Adding user/group level permissions Signed-off-by: Karthik Jayaraman --- .../environmentdashboard/BuildAddUrl.java | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java index e33849f..899a083 100644 --- a/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java @@ -8,20 +8,37 @@ import jenkins.tasks.SimpleBuildStep; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import javax.annotation.Nonnull; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; + public class BuildAddUrl extends Builder implements SimpleBuildStep { private final String title; private final String url; - + private String users; + private String groups; + @DataBoundConstructor public BuildAddUrl(String title, String url) { this.url = url; this.title = title; } + + @DataBoundSetter + public void setUsers(String users) { + this.users = users; + } + + @DataBoundSetter + public void setGroups(String groups) { + this.groups = groups; + } public String getTitle() { return title; @@ -31,6 +48,14 @@ public String getUrl() { return url; } + public String getUsers() { + return users; + } + + public String getGroups() { + return groups; + } + @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; @@ -43,7 +68,7 @@ public void perform( @Nonnull Launcher launcher, @Nonnull TaskListener listener ) throws InterruptedException, IOException { - run.addAction(new BuildUrlAction(title, url)); + run.addAction(new BuildUrlAction(title, url, users, groups)); } @Extension @@ -64,15 +89,49 @@ public boolean isApplicable(Class t) { public static class BuildUrlAction implements Action { private final String title; private final String url; + private final ArrayList userList = new ArrayList(); + private final ArrayList groupList = new ArrayList(); - BuildUrlAction(String title, String url) { + BuildUrlAction(String title, String url, String users, String groups) { this.title = title; this.url = url; + if (users != null && !"".equals(users)) { + this.userList.addAll(Arrays.asList(users.split(","))); + } + if (groups != null && !"".equals(groups)) { + this.groupList.addAll(Arrays.asList(groups.split(","))); + } } @Override public String getIconFileName() { - return String.format("/plugin/%s/deploy.png", getClass().getPackage().getImplementationTitle()); + String iconFileName = String.format("/plugin/%s/deploy.png", getClass().getPackage().getImplementationTitle()); + + User currentUser = User.current(); + if (currentUser == null) return null; + + String currentUserId = currentUser.getId(); + List currentUserGroups = currentUser.getAuthorities(); + + if (!checkPermissions() || isUserInList(currentUserId) || isUserInGroup(currentUserGroups)) + return iconFileName; + + return null; + } + + private boolean checkPermissions() { + return userList.size() > 0 || groupList.size() > 0; + } + + private boolean isUserInList(String userId) { + return userList.contains(userId); + } + + private boolean isUserInGroup(List userGroups) { + for(String userGroup: userGroups) + if (groupList.contains(userGroup)) + return true; + return false; } @Override From 6aaca8b222df2ff02bce2d57858efc6eb3c2066d Mon Sep 17 00:00:00 2001 From: Karthik Jayaraman Date: Fri, 11 Sep 2020 13:58:12 -0400 Subject: [PATCH 3/4] Cleanup Signed-off-by: Karthik Jayaraman --- .../jenkinsci/plugins/environmentdashboard/DeploymentView.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java index 4859925..7a26624 100644 --- a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java @@ -48,8 +48,7 @@ private List getEnvs(TopLevelItem item) { return runs .stream() - .map(run -> run.getActions(DeploymentAction.class)) - .flatMap(List::stream) + .map(run -> run.getAction(DeploymentAction.class)) .filter(Objects::nonNull) .collect(Collectors.groupingBy(DeploymentAction::getEnv)) .entrySet() From dc13cf18db1456d8d8150bc3d474dd30b4f060ab Mon Sep 17 00:00:00 2001 From: Karthik Jayaraman Date: Sun, 13 Sep 2020 14:04:00 -0400 Subject: [PATCH 4/4] Using Data Tables Signed-off-by: Karthik Jayaraman --- pom.xml | 10 + .../DeploymentTableConfiguration.java | 34 +++ .../DeploymentTableModel.java | 151 ++++++++++ .../DeploymentTableRow.java | 22 ++ .../environmentdashboard/DeploymentView.java | 132 ++++---- .../DeploymentView/main.jelly | 102 ++----- src/main/webapp/js/dataTables.rowsGroup.js | 285 ++++++++++++++++++ 7 files changed, 585 insertions(+), 151 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java create mode 100644 src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java create mode 100644 src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java create mode 100644 src/main/webapp/js/dataTables.rowsGroup.js diff --git a/pom.xml b/pom.xml index 49139ec..1913af8 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,16 @@ workflow-multibranch 2.21 + + io.jenkins.plugins + data-tables-api + 1.10.20-12 + + + com.github.spotbugs + spotbugs-annotations + 4.0.0 + diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java new file mode 100644 index 0000000..b39aee7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java @@ -0,0 +1,34 @@ +package org.jenkinsci.plugins.environmentdashboard; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jenkins.plugins.datatables.TableConfiguration; + +public class DeploymentTableConfiguration extends TableConfiguration{ + + private final Map configuration = new HashMap<>(); + + DeploymentTableConfiguration() { + super(); + + this.configuration.put("rowsGroup", new int[]{0}); + this.configuration.put("pagingType", "full_numbers"); + this.configuration.put("stateSave", true); + } + + @Override + public String getConfiguration() { + try { + return new ObjectMapper().writeValueAsString(configuration); + } + catch (JsonProcessingException exception) { + throw new IllegalArgumentException( + String.format("Can't convert table configuration '%s' to JSON object", configuration), exception); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java new file mode 100644 index 0000000..c5a7265 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java @@ -0,0 +1,151 @@ +package org.jenkinsci.plugins.environmentdashboard; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.jenkinsci.plugins.environmentdashboard.Deployment.DeploymentAction; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; + +import hudson.model.Job; +import hudson.model.Run; +import hudson.model.TopLevelItem; +import io.jenkins.plugins.datatables.TableColumn; +import io.jenkins.plugins.datatables.TableConfiguration; +import io.jenkins.plugins.datatables.TableModel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +public class DeploymentTableModel extends TableModel { + private String id; + List items; + + DeploymentTableModel(String id) { + this.id = id; + } + + public TableModel populate(List items) { + this.items = items; + return this; + } + + private String getStatusHTML(String altText, String imageUrl, String href) { + // Issue with URL + href = href.substring(href.indexOf("/job")); + return new StringBuffer().append("\"")") + .toString(); + } + + private String getPopupHTML(int row, int col, String envName) { + return new StringBuffer().append("").append(envName).append("") + .toString(); + } + + @Override + public String getId() { + return id; + } + + @Override + public TableConfiguration getTableConfiguration() { + return new DeploymentTableConfiguration(); + } + + @Override + public List getColumns() { + List columns = new ArrayList(); + columns.add(new TableColumn("Job", "job")); + columns.add(new TableColumn("Environment", "environment")); + columns.add(new TableColumn("Release", "release")); + columns.add(new TableColumn("Result", "result")); + columns.add(new TableColumn("Completed", "completed")); + return columns; + } + + @Override + public List getRows() { + List rows = new ArrayList(); + + int i=0; + for (Unit unit: getUnits(items)) { + int j=0; + for (Unit.Environment env: unit.environments) { + DeploymentAction action = env.getCurrentAction(); + Run run = action.getRun(); + rows.add(new DeploymentTableRow(unit.job.getName(), + getPopupHTML(i, j, env.name), + action.getBuildNumber(), + getStatusHTML(run.getDescription(), + run.getIconColor().getImageOf("32x32"), + run.getUrl()), + run.getTimestampString() + " ago")); + ++j; + } + ++i; + } + return rows; + } + + private List getEnvs(TopLevelItem item) { + List runs = Collections.emptyList(); + if (item instanceof WorkflowMultiBranchProject) { + runs = ((WorkflowMultiBranchProject) item) + .getItems() + .stream() + .map(Job::getBuilds) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } else if (item instanceof WorkflowJob) { + runs = ((WorkflowJob) item).getBuilds(); + } + + return runs + .stream() + .map(run -> run.getActions(DeploymentAction.class)) + .flatMap(List::stream) + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(DeploymentAction::getEnv)) + .entrySet() + .stream() + .map(e -> new Unit.Environment(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + public List getUnits(List items) { + return items.stream() + .map(item -> new Unit(item, getEnvs(item))) + .filter(unit -> !unit.getEnvironments().isEmpty()) + .collect(Collectors.toList()); + } + + @Getter + @RequiredArgsConstructor + public static class Unit { + private final TopLevelItem job; + private final List environments; + + @Getter + @RequiredArgsConstructor + public static class Environment { + private final String name; + private final List actions; + + public DeploymentAction getCurrentAction() { + return actions.get(0); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java new file mode 100644 index 0000000..23f3877 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java @@ -0,0 +1,22 @@ +package org.jenkinsci.plugins.environmentdashboard; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class DeploymentTableRow { + private String job; + private String environment; + private String release; + private String result; + private String completed; + + DeploymentTableRow(String job, String environment, String release, String result, String completed) { + this.job = job; + this.environment = environment; + this.release = release; + this.result = result; + this.completed = completed; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java index 7a26624..0cc4adb 100644 --- a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java @@ -1,88 +1,37 @@ package org.jenkinsci.plugins.environmentdashboard; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.annotation.Nonnull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.bind.JavaScriptMethod; + import hudson.Extension; import hudson.Util; -import hudson.model.Job; import hudson.model.ListView; import hudson.model.TopLevelItem; import hudson.model.ViewDescriptor; import hudson.util.FormValidation; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import io.jenkins.plugins.datatables.AsyncTableContentProvider; +import io.jenkins.plugins.datatables.TableModel; import net.sf.json.JSONObject; -import org.jenkinsci.plugins.environmentdashboard.Deployment.DeploymentAction; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; -import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; +public class DeploymentView extends ListView implements AsyncTableContentProvider{ + private DeploymentTableModel model; -public class DeploymentView extends ListView { @DataBoundConstructor - public DeploymentView(String name) { + public DeploymentView(final String name) { super(name); } - private List getEnvs(TopLevelItem item) { - List runs = Collections.emptyList(); - if (item instanceof WorkflowMultiBranchProject) { - runs = ((WorkflowMultiBranchProject) item) - .getItems() - .stream() - .map(Job::getBuilds) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } else if (item instanceof WorkflowJob) { - runs = ((WorkflowJob) item).getBuilds(); - } - - return runs - .stream() - .map(run -> run.getAction(DeploymentAction.class)) - .filter(Objects::nonNull) - .collect(Collectors.groupingBy(DeploymentAction::getEnv)) - .entrySet() - .stream() - .map(e -> new Unit.Environment(e.getKey(), e.getValue())) - .collect(Collectors.toList()); - } - - public List getUnits(List items) { - return items - .stream() - .map(item -> new Unit(item, getEnvs(item))) - .filter(unit -> !unit.getEnvironments().isEmpty()) - .collect(Collectors.toList()); - } - - @Getter - @RequiredArgsConstructor - public static class Unit { - private final TopLevelItem job; - private final List environments; - - @Getter - @RequiredArgsConstructor - public static class Environment { - private final String name; - private final List actions; - - public DeploymentAction getCurrentAction() { - return actions.get(0); - } - } - } - @Extension public static class DeploymentViewDescriptor extends ViewDescriptor { public DeploymentViewDescriptor() { @@ -96,13 +45,14 @@ public String getDisplayName() { return "Deployment View"; } - // Copy-n-paste from ListView$Descriptor as sadly we cannot inherit from that class - public FormValidation doCheckIncludeRegex(@QueryParameter String value) { - String v = Util.fixEmpty(value); + // Copy-n-paste from ListView$Descriptor as sadly we cannot inherit from that + // class + public FormValidation doCheckIncludeRegex(@QueryParameter final String value) { + final String v = Util.fixEmpty(value); if (v != null) { try { Pattern.compile(v); - } catch (PatternSyntaxException pse) { + } catch (final PatternSyntaxException pse) { return FormValidation.error(pse.getMessage()); } } @@ -110,10 +60,42 @@ public FormValidation doCheckIncludeRegex(@QueryParameter String value) { } @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + public boolean configure(final StaplerRequest req, final JSONObject json) throws FormException { save(); return true; } } + + + @Override + public TableModel getTableModel(String id) { + if (model == null) { + model = new DeploymentTableModel(id); + } + return model; + } + + public TableModel getTableModel(String id, List items) { + if (model == null) { + model = new DeploymentTableModel(id); + } + return model.populate(items); + } + + @Override + @JavaScriptMethod + public String getTableRows(final String id) { + return toJsonArray(getTableModel(id).getRows()); + } + + private String toJsonArray(final List rows) { + try { + return new ObjectMapper().writeValueAsString(rows); + } + catch (JsonProcessingException exception) { + throw new IllegalArgumentException( + String.format("Can't convert table rows '%s' to JSON object", rows), exception); + } + } } diff --git a/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly b/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly index 01b762b..8997d57 100644 --- a/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly +++ b/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly @@ -1,9 +1,9 @@ + xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:dt="/data-tables"> - + @@ -15,77 +15,22 @@ - -
- - - - - - - - - - - - - - - - - - - - - -
- - Job - - - - Environment - - - - Release - - - - Result - - - - Completed - -
- - ${unit.getJob().name} - - - - ${environment.getName()} - - - - ${environment.getCurrentAction().buildNumber} - - - - - ${environment.getCurrentAction().run.timestampString} -
-
- + + + + + + -