/* This file is part of Delivery Pipeline Plugin. Delivery Pipeline Plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Delivery Pipeline Plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Delivery Pipeline Plugin. If not, see <http://www.gnu.org/licenses/>. */ package se.diabol.jenkins.pipeline; import com.google.common.collect.Sets; import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.model.AbstractBuild; import hudson.model.AbstractDescribableImpl; import hudson.model.AbstractProject; import hudson.model.Api; import hudson.model.Cause; import hudson.model.CauseAction; import hudson.model.Descriptor; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.ParametersAction; import hudson.model.TopLevelItem; import hudson.model.View; import hudson.model.ViewDescriptor; import hudson.model.ViewGroup; import hudson.model.listeners.ItemListener; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.model.Jenkins; import org.acegisecurity.AuthenticationException; import org.acegisecurity.BadCredentialsException; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.bind.JavaScriptMethod; import org.kohsuke.stapler.export.Exported; import se.diabol.jenkins.pipeline.domain.Component; import se.diabol.jenkins.pipeline.domain.Pipeline; import se.diabol.jenkins.pipeline.domain.PipelineException; import se.diabol.jenkins.pipeline.sort.ComponentComparator; import se.diabol.jenkins.pipeline.sort.ComponentComparatorDescriptor; import se.diabol.jenkins.pipeline.trigger.ManualTrigger; import se.diabol.jenkins.pipeline.trigger.ManualTriggerFactory; import se.diabol.jenkins.pipeline.trigger.TriggerException; import se.diabol.jenkins.pipeline.util.FullScreen; import se.diabol.jenkins.pipeline.util.JenkinsUtil; import se.diabol.jenkins.pipeline.util.PipelineUtils; import se.diabol.jenkins.pipeline.util.ProjectUtil; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.annotation.Nonnull; import javax.servlet.ServletException; public class DeliveryPipelineView extends View { private static final Logger LOG = Logger.getLogger(DeliveryPipelineView.class.getName()); private static final int DEFAULT_INTERVAL = 2; private static final int DEFAULT_NO_OF_PIPELINES = 3; private static final int MAX_NO_OF_PIPELINES = 50; private static final String OLD_NONE_SORTER = "se.diabol.jenkins.pipeline.sort.NoOpComparator"; private static final String NONE_SORTER = "none"; public static final String DEFAULT_THEME = "default"; private List<ComponentSpec> componentSpecs; private int noOfPipelines = DEFAULT_NO_OF_PIPELINES; private boolean showAggregatedPipeline = false; private int noOfColumns = 1; private String sorting = NONE_SORTER; private String fullScreenCss = null; private String embeddedCss = null; private boolean showAvatars = false; private int updateInterval = DEFAULT_INTERVAL; private boolean showChanges = false; private boolean allowManualTriggers = false; private boolean showTotalBuildTime = false; private boolean allowRebuild = false; private boolean allowPipelineStart = false; private boolean showDescription = false; private boolean showPromotions = false; private boolean showTestResults = false; private boolean showStaticAnalysisResults = false; private boolean linkRelative = false; private boolean pagingEnabled = false; private boolean showAggregatedChanges = false; private String aggregatedChangesGroupingPattern = null; private String theme = DEFAULT_THEME; private int maxNumberOfVisiblePipelines = -1; private List<RegExpSpec> regexpFirstJobs; private boolean linkToConsoleLog = false; private String description = null; private transient String error; @DataBoundConstructor public DeliveryPipelineView(String name) { super(name); } public DeliveryPipelineView(String name, ViewGroup owner) { super(name, owner); } public List<RegExpSpec> getRegexpFirstJobs() { return regexpFirstJobs; } public void setRegexpFirstJobs(List<RegExpSpec> regexpFirstJobs) { this.regexpFirstJobs = regexpFirstJobs; } public boolean getShowAvatars() { return showAvatars; } public void setShowAvatars(boolean showAvatars) { this.showAvatars = showAvatars; } public String getSorting() { /* Removed se.diabol.jenkins.pipeline.sort.NoOpComparator since it in some cases did sorting*/ if (OLD_NONE_SORTER.equals(sorting)) { this.sorting = NONE_SORTER; } return sorting; } public void setSorting(String sorting) { /* Removed se.diabol.jenkins.pipeline.sort.NoOpComparator since it in some cases did sorting*/ if (OLD_NONE_SORTER.equals(sorting)) { this.sorting = NONE_SORTER; } else { this.sorting = sorting; } } public List<ComponentSpec> getComponentSpecs() { return componentSpecs; } public void setComponentSpecs(List<ComponentSpec> componentSpecs) { this.componentSpecs = componentSpecs; } public int getNoOfPipelines() { return noOfPipelines; } public boolean isShowAggregatedPipeline() { return showAggregatedPipeline; } public void setNoOfPipelines(int noOfPipelines) { this.noOfPipelines = noOfPipelines; } public boolean isShowChanges() { return showChanges; } public void setShowChanges(boolean showChanges) { this.showChanges = showChanges; } @Exported public boolean isShowTotalBuildTime() { return showTotalBuildTime; } public void setShowTotalBuildTime(boolean showTotalBuildTime) { this.showTotalBuildTime = showTotalBuildTime; } public void setShowAggregatedPipeline(boolean showAggregatedPipeline) { this.showAggregatedPipeline = showAggregatedPipeline; } @Exported public boolean isAllowPipelineStart() { return allowPipelineStart; } public void setAllowPipelineStart(boolean allowPipelineStart) { this.allowPipelineStart = allowPipelineStart; } @Exported public boolean isAllowManualTriggers() { return allowManualTriggers; } public void setAllowManualTriggers(boolean allowManualTriggers) { this.allowManualTriggers = allowManualTriggers; } public int getNoOfColumns() { return noOfColumns; } public void setNoOfColumns(int noOfColumns) { this.noOfColumns = noOfColumns; } public String getFullScreenCss() { return fullScreenCss; } public int getUpdateInterval() { //This occurs when the plugin has been updated and as long as the view has not been updated //Jenkins will set the default value to 0 if (updateInterval == 0) { updateInterval = DEFAULT_INTERVAL; } return updateInterval; } public void setUpdateInterval(int updateInterval) { this.updateInterval = updateInterval; } public void setFullScreenCss(String fullScreenCss) { if (fullScreenCss != null && "".equals(fullScreenCss.trim())) { this.fullScreenCss = null; } else { this.fullScreenCss = fullScreenCss; } } public String getEmbeddedCss() { return embeddedCss; } public void setEmbeddedCss(String embeddedCss) { if (embeddedCss != null && "".equals(embeddedCss.trim())) { this.embeddedCss = null; } else { this.embeddedCss = embeddedCss; } } @Exported public boolean getPagingEnabled() { return pagingEnabled; } public String getTheme() { return this.theme == null ? DEFAULT_THEME : this.theme; } public void setTheme(String theme) { this.theme = theme; } public boolean isFullScreenView() { return FullScreen.isFullScreenRequest(Stapler.getCurrentRequest()); } public void onProjectRenamed(Item item, String oldName, String newName) { if (componentSpecs != null) { Iterator<ComponentSpec> it = componentSpecs.iterator(); while (it.hasNext()) { ComponentSpec componentSpec = it.next(); if (componentSpec.getFirstJob().equals(oldName)) { if (newName == null) { it.remove(); } else { componentSpec.setFirstJob(newName); } } if (componentSpec.getLastJob() != null && componentSpec.getLastJob().equals(oldName)) { if (newName == null) { it.remove(); } else { componentSpec.setLastJob(newName); } } } } } @Override @Exported public String getViewUrl() { return super.getViewUrl(); } @Override public Api getApi() { return new PipelineApi(this); } @Exported public String getLastUpdated() { return PipelineUtils.formatTimestamp(System.currentTimeMillis()); } @Exported public String getError() { return error; } @Exported public boolean isAllowRebuild() { return allowRebuild; } public void setAllowRebuild(boolean allowRebuild) { this.allowRebuild = allowRebuild; } @Exported public boolean isShowDescription() { return showDescription; } @Exported public boolean isShowPromotions() { return showPromotions; } @Exported public boolean isShowTestResults() { return showTestResults; } @Exported public boolean isShowStaticAnalysisResults() { return showStaticAnalysisResults; } @Exported public boolean isLinkRelative() { return linkRelative; } public void setLinkRelative(boolean linkRelative) { this.linkRelative = linkRelative; } public void setShowDescription(boolean showDescription) { this.showDescription = showDescription; } public void setShowPromotions(boolean showPromotions) { this.showPromotions = showPromotions; } public void setShowTestResults(boolean showTestResults) { this.showTestResults = showTestResults; } public void setShowStaticAnalysisResults(boolean showStaticAnalysisResults) { this.showStaticAnalysisResults = showStaticAnalysisResults; } public void setPagingEnabled(boolean pagingEnabled) { this.pagingEnabled = pagingEnabled; } @Exported public boolean isShowAggregatedChanges() { return showAggregatedChanges; } public void setShowAggregatedChanges(boolean showAggregatedChanges) { this.showAggregatedChanges = showAggregatedChanges; } @Exported public String getAggregatedChangesGroupingPattern() { return aggregatedChangesGroupingPattern; } public void setAggregatedChangesGroupingPattern(String aggregatedChangesGroupingPattern) { this.aggregatedChangesGroupingPattern = aggregatedChangesGroupingPattern; } public int getMaxNumberOfVisiblePipelines() { return maxNumberOfVisiblePipelines; } public void setMaxNumberOfVisiblePipelines(int maxNumberOfVisiblePipelines) { this.maxNumberOfVisiblePipelines = maxNumberOfVisiblePipelines; } @Exported public boolean isLinkToConsoleLog() { return linkToConsoleLog; } public void setLinkToConsoleLog(boolean linkToConsoleLog) { this.linkToConsoleLog = linkToConsoleLog; } @Override @Exported public String getDescription() { if (super.description == null) { setDescription(this.description); } return super.description; } public void setDescription(String description) { super.description = description; this.description = description; } @JavaScriptMethod public void triggerManual(String projectName, String upstreamName, String buildId) throws TriggerException, AuthenticationException { try { LOG.fine("Trigger manual build " + projectName + " " + upstreamName + " " + buildId); AbstractProject project = ProjectUtil.getProject(projectName, Jenkins.getInstance()); if (!project.hasPermission(Item.BUILD)) { throw new BadCredentialsException("Not authorized to trigger build"); } AbstractProject upstream = ProjectUtil.getProject(upstreamName, Jenkins.getInstance()); ManualTrigger trigger = ManualTriggerFactory.getManualTrigger(project, upstream); if (trigger != null) { trigger.triggerManual(project, upstream, buildId, getOwner().getItemGroup()); } else { String message = "Trigger not found for manual build " + projectName + " for upstream " + upstreamName + " id: " + buildId; LOG.log(Level.WARNING, message); throw new TriggerException(message); } } catch (TriggerException e) { LOG.log(Level.WARNING, triggerExceptionMessage(projectName, upstreamName, buildId), e); throw e; } } public void triggerRebuild(String projectName, String buildId) { AbstractProject project = ProjectUtil.getProject(projectName, Jenkins.getInstance()); if (!project.hasPermission(Item.BUILD)) { throw new BadCredentialsException("Not authorized to trigger build"); } AbstractBuild build = project.getBuildByNumber(Integer.parseInt(buildId)); @SuppressWarnings("unchecked") List<Cause> prevCauses = build.getCauses(); List<Cause> newCauses = new ArrayList<Cause>(); for (Cause cause : prevCauses) { if (!(cause instanceof Cause.UserIdCause)) { newCauses.add(cause); } } newCauses.add(new Cause.UserIdCause()); CauseAction causeAction = new CauseAction(newCauses); project.scheduleBuild2(project.getQuietPeriod(),null, causeAction, build.getAction(ParametersAction.class)); } protected static String triggerExceptionMessage(final String projectName, final String upstreamName, final String buildId) { String message = "Could not trigger manual build " + projectName + " for upstream " + upstreamName + " id: " + buildId; if (projectName.contains("/")) { message += ". Did you mean to specify " + withoutFolderPrefix(projectName) + "?"; } return message; } protected static String withoutFolderPrefix(final String projectName) { return projectName.substring(projectName.indexOf("/") + 1); } @Exported public List<Component> getPipelines() { try { LOG.fine("Getting pipelines!"); List<Component> components = new ArrayList<Component>(); if (componentSpecs != null) { for (ComponentSpec componentSpec : componentSpecs) { AbstractProject firstJob = ProjectUtil.getProject(componentSpec.getFirstJob(), getOwnerItemGroup()); AbstractProject lastJob = ProjectUtil.getProject(componentSpec.getLastJob(), getOwnerItemGroup()); if (firstJob != null) { components.add(getComponent(componentSpec.getName(), firstJob, lastJob, showAggregatedPipeline, (componentSpecs.indexOf(componentSpec) + 1), componentSpec.isShowUpstream())); } else { throw new PipelineException("Could not find project: " + componentSpec.getFirstJob()); } } } if (regexpFirstJobs != null) { for (RegExpSpec regexp : regexpFirstJobs) { Map<String, AbstractProject> matches = ProjectUtil.getProjects(regexp.getRegexp()); int index = 1; for (Map.Entry<String, AbstractProject> entry : matches.entrySet()) { components.add(getComponent(entry.getKey(), entry.getValue(), null, showAggregatedPipeline, index, regexp.isShowUpstream())); index++; } } } if (getSorting() != null && !getSorting().equals(NONE_SORTER)) { ComponentComparatorDescriptor comparatorDescriptor = ComponentComparator.all().find(sorting); if (comparatorDescriptor != null) { Collections.sort(components, comparatorDescriptor.createInstance()); } } if (maxNumberOfVisiblePipelines > 0) { LOG.fine("Limiting number of jobs to: " + maxNumberOfVisiblePipelines); components = components.subList(0, Math.min(components.size(), maxNumberOfVisiblePipelines)); } LOG.fine("Returning: " + components); error = null; return components; } catch (PipelineException e) { error = e.getMessage(); return new ArrayList<>(); } } private Component getComponent(String name, AbstractProject firstJob, AbstractProject lastJob, boolean showAggregatedPipeline, int componentNumber, boolean showUpstream) throws PipelineException { Pipeline pipeline = Pipeline.extractPipeline(name, firstJob, lastJob, showUpstream); Component component = new Component(name, firstJob.getName(), firstJob.getUrl(), firstJob.isParameterized(), noOfPipelines, pagingEnabled, componentNumber); List<Pipeline> pipelines = new ArrayList<Pipeline>(); if (showAggregatedPipeline) { pipelines.add(pipeline.createPipelineAggregated(getOwnerItemGroup(), showAggregatedChanges)); } pipelines.addAll(pipeline .createPipelineLatest(noOfPipelines, getOwnerItemGroup(), showPaging(), showChanges, component)); component.setPipelines(pipelines); return component; } protected boolean showPaging() { return !isFullScreenView() && getPagingEnabled(); } @Override public Collection<TopLevelItem> getItems() { Set<TopLevelItem> jobs = Sets.newHashSet(); addJobsFromComponentSpecs(jobs); addRegexpFirstJobs(jobs); return jobs; } private void addJobsFromComponentSpecs(Set<TopLevelItem> jobs) { if (componentSpecs == null) { return; } for (ComponentSpec spec : componentSpecs) { AbstractProject first = ProjectUtil.getProject(spec.getFirstJob(), getOwnerItemGroup()); AbstractProject last = ProjectUtil.getProject(spec.getLastJob(), getOwnerItemGroup()); Collection<AbstractProject<?, ?>> downstreamProjects = ProjectUtil.getAllDownstreamProjects(first, last).values(); for (AbstractProject project : downstreamProjects) { jobs.add((TopLevelItem) project); } } } private void addRegexpFirstJobs(Set<TopLevelItem> jobs) { if (regexpFirstJobs == null) { return; } for (RegExpSpec spec : regexpFirstJobs) { Map<String, AbstractProject> regexpJobs = ProjectUtil.getProjects(spec.getRegexp()); for (AbstractProject project : regexpJobs.values()) { jobs.add((TopLevelItem) project); } } } @Override public boolean contains(TopLevelItem item) { return getItems().contains(item); } @Override protected void submit(StaplerRequest req) throws IOException, ServletException, Descriptor.FormException { req.bindJSON(this, req.getSubmittedForm()); componentSpecs = req.bindJSONToList(ComponentSpec.class, req.getSubmittedForm().get("componentSpecs")); regexpFirstJobs = req.bindJSONToList(RegExpSpec.class, req.getSubmittedForm().get("regexpFirstJobs")); } @Override public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { if (!isDefault()) { return getOwner().getPrimaryView().doCreateItem(req, rsp); } else { return JenkinsUtil.getInstance().doCreateItem(req, rsp); } } @Extension public static class DescriptorImpl extends ViewDescriptor { public ListBoxModel doFillNoOfColumnsItems(@AncestorInPath ItemGroup<?> context) { ListBoxModel options = new ListBoxModel(); options.add("1", "1"); options.add("2", "2"); options.add("3", "3"); return options; } public ListBoxModel doFillNoOfPipelinesItems(@AncestorInPath ItemGroup<?> context) { ListBoxModel options = new ListBoxModel(); for (int i = 0; i <= MAX_NO_OF_PIPELINES; i++) { String opt = String.valueOf(i); options.add(opt, opt); } return options; } public ListBoxModel doFillSortingItems() { DescriptorExtensionList<ComponentComparator, ComponentComparatorDescriptor> descriptors = ComponentComparator.all(); ListBoxModel options = new ListBoxModel(); options.add("None", NONE_SORTER); for (ComponentComparatorDescriptor descriptor : descriptors) { options.add(descriptor.getDisplayName(), descriptor.getId()); } return options; } public FormValidation doCheckUpdateInterval(@QueryParameter String value) { int valueAsInt; try { valueAsInt = Integer.parseInt(value); } catch (NumberFormatException e) { return FormValidation.error(e, "Value must be an integer"); } if (valueAsInt <= 0) { return FormValidation.error("Value must be greater than 0"); } return FormValidation.ok(); } @Override public String getDisplayName() { return "Delivery Pipeline View"; } } public static class RegExpSpec extends AbstractDescribableImpl<RegExpSpec> { private String regexp; private boolean showUpstream; @DataBoundConstructor public RegExpSpec(String regexp, boolean showUpstream) { this.regexp = regexp != null ? regexp.trim() : null; this.showUpstream = showUpstream; } public String getRegexp() { return regexp; } public boolean isShowUpstream() { return showUpstream; } public void setShowUpstream(boolean showUpstream) { this.showUpstream = showUpstream; } @Extension public static class DescriptorImpl extends Descriptor<RegExpSpec> { @Nonnull @Override public String getDisplayName() { return "RegExp"; } public FormValidation doCheckRegexp(@QueryParameter String value) { if (value != null) { if (value.trim().equals("")) { return FormValidation.error("Regular expression cannot be blank"); } try { Pattern pattern = Pattern.compile(value); if (pattern.matcher("").groupCount() == 1) { return FormValidation.ok(); } else if (pattern.matcher("").groupCount() == 0) { return FormValidation.error("No capture group defined"); } else { return FormValidation.error("Too many capture groups defined"); } } catch (PatternSyntaxException e) { return FormValidation.error(e, "Syntax error in regular expression pattern"); } } return FormValidation.ok(); } } } public static class ComponentSpec extends AbstractDescribableImpl<ComponentSpec> { private String name; private String firstJob; private String lastJob; private boolean showUpstream; @DataBoundConstructor public ComponentSpec(String name, String firstJob, String lastJob, boolean showUpstream) { this.name = name; this.firstJob = firstJob; this.lastJob = lastJob; this.showUpstream = showUpstream; } public String getName() { return name; } public String getFirstJob() { return firstJob; } public void setFirstJob(String firstJob) { this.firstJob = firstJob; } public String getLastJob() { return lastJob; } public void setLastJob(String lastJob) { this.lastJob = lastJob; } public boolean isShowUpstream() { return showUpstream; } public void setShowUpstream(boolean showUpstream) { this.showUpstream = showUpstream; } @Extension public static class DescriptorImpl extends Descriptor<ComponentSpec> { @Nonnull @Override public String getDisplayName() { return ""; } public ListBoxModel doFillFirstJobItems(@AncestorInPath ItemGroup<?> context) { return ProjectUtil.fillAllProjects(context, AbstractProject.class); } public ListBoxModel doFillLastJobItems(@AncestorInPath ItemGroup<?> context) { ListBoxModel options = new ListBoxModel(); options.add(""); options.addAll(ProjectUtil.fillAllProjects(context, AbstractProject.class)); return options; } public FormValidation doCheckName(@QueryParameter String value) { if (value != null && !"".equals(value.trim())) { return FormValidation.ok(); } else { return FormValidation.error("Please supply a title"); } } } } @Extension public static class ItemListenerImpl extends ItemListener { @Override public void onRenamed(Item item, String oldName, String newName) { notifyView(item, oldName, newName); } @Override public void onDeleted(Item item) { notifyView(item, item.getFullName(), null); } private void notifyView(Item item, String oldName, String newName) { Collection<View> views = JenkinsUtil.getInstance().getViews(); for (View view : views) { if (view instanceof DeliveryPipelineView) { ((DeliveryPipelineView) view).onProjectRenamed(item, oldName, newName); } } } } }