UX-196# API for walking folder tree

- Folder contains other pipelines and folder and other buildable items
- Nested folder or pipeline can be accessed using recursive REST path
- Access p1 pipeline nested inside a folder:
   /organizations/jenkins/pipelines/folder1/pipelines/folder2/pipelines/p1
- Folder vs pipeline detection to happen with data extensibility/capability work
This commit is contained in:
Vivek Pandey 2016-06-02 00:12:09 -07:00
parent 91e266ee0d
commit 603d36011d
10 changed files with 379 additions and 33 deletions

View File

@ -50,7 +50,7 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
throw new ServiceException.UnexpectedErrorException("no master branch to favorite");
}
FavoriteUtil.favoriteJob(job, favoriteAction.isFavorite());
FavoriteUtil.favoriteJob(job.getFullName(), favoriteAction.isFavorite());
}
@Override
@ -63,6 +63,11 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
return mbp.getDisplayName();
}
@Override
public String getFullName() {
return mbp.getFullName();
}
@Override
public int getTotalNumberOfBranches(){
return countJobs(false);
@ -95,7 +100,7 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
@Override
@SuppressWarnings("unchecked")
public int getWeatherScore(){
public Integer getWeatherScore(){
/**
* TODO: this code need cleanup once MultiBranchProject exposes default branch. At present
*
@ -229,5 +234,4 @@ public class MultiBranchPipelineImpl extends BlueMultiBranchPipeline {
}
};
}
}

View File

@ -1,6 +1,8 @@
package io.jenkins.blueocean.service.embedded.rest;
import hudson.model.BuildableItem;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.model.BluePipeline;
@ -10,6 +12,7 @@ import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@ -17,13 +20,37 @@ import java.util.List;
* @author Vivek Pandey
*/
public class PipelineContainerImpl extends BluePipelineContainer {
private final ItemGroup itemGroup;
public PipelineContainerImpl(ItemGroup itemGroup) {
this.itemGroup = itemGroup;
}
public PipelineContainerImpl() {
this.itemGroup = null;
}
@Override
public BluePipeline get(String name) {
Item item;
if(itemGroup == null){
item = Jenkins.getActiveInstance().getItem(name);
}else{
item = itemGroup.getItem(name);
}
for (BluePipeline bluePipeline : this) {
if (bluePipeline.getName().equals(name)) {
return bluePipeline;
if(item == null){
throw new ServiceException.NotFoundException(String.format("Pipeline %s not found", name));
}
if (item instanceof BuildableItem) {
if (item instanceof MultiBranchProject) {
return new MultiBranchPipelineImpl((MultiBranchProject) item);
} else if (!isMultiBranchProjectJob((BuildableItem) item) && item instanceof Job) {
return new PipelineImpl((Job) item);
}
} else if (item instanceof ItemGroup) {
return new PipelineFolderImpl((ItemGroup) item);
}
// TODO: I'm going to turn this into a decorator annotation
@ -31,20 +58,31 @@ public class PipelineContainerImpl extends BluePipelineContainer {
}
@Override
@SuppressWarnings("unchecked")
public Iterator<BluePipeline> iterator() {
List<BuildableItem> items = Jenkins.getActiveInstance().getAllItems(BuildableItem.class);
if(itemGroup != null){
return getPipelines(itemGroup.getItems());
}else{
return getPipelines(Jenkins.getActiveInstance().getAllItems(Item.class));
}
}
protected static boolean isMultiBranchProjectJob(BuildableItem item){
return item instanceof WorkflowJob && item.getParent() instanceof MultiBranchProject;
}
protected static Iterator<BluePipeline> getPipelines(Collection<Item> items){
List<BluePipeline> pipelines = new ArrayList<>();
for (BuildableItem item : items) {
for (Item item : items) {
if(item instanceof MultiBranchProject){
pipelines.add(new MultiBranchPipelineImpl((MultiBranchProject) item));
}else if(!isMultiBranchProjectJob(item) && item instanceof Job){
}else if(item instanceof BuildableItem && !isMultiBranchProjectJob((BuildableItem) item)
&& item instanceof Job){
pipelines.add(new PipelineImpl((Job) item));
}else if(item instanceof ItemGroup){
pipelines.add(new PipelineFolderImpl((ItemGroup) item));
}
}
return pipelines.iterator();
}
private boolean isMultiBranchProjectJob(BuildableItem item){
return item instanceof WorkflowJob && item.getParent() instanceof MultiBranchProject;
}
}

View File

@ -0,0 +1,80 @@
package io.jenkins.blueocean.service.embedded.rest;
import hudson.model.ItemGroup;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.rest.model.BluePipelineContainer;
import io.jenkins.blueocean.rest.model.BluePipelineFolder;
import io.jenkins.blueocean.service.embedded.util.FavoriteUtil;
import org.kohsuke.stapler.json.JsonBody;
/**
* @author Vivek Pandey
*/
public class PipelineFolderImpl extends BluePipelineFolder {
private final ItemGroup folder;
private final BluePipelineContainer container;
public PipelineFolderImpl(ItemGroup folder) {
this.folder = folder;
this.container = new PipelineContainerImpl(folder);
}
@Override
public String getOrganization() {
return OrganizationImpl.INSTANCE.getName();
}
@Override
public String getName() {
return folder.getDisplayName();
}
@Override
public String getDisplayName() {
return folder.getDisplayName();
}
@Override
public String getFullName() {
return folder.getFullName();
}
@Override
public BluePipelineContainer getPipelines() {
return new PipelineContainerImpl(folder);
}
@Override
public Integer getNumberOfFolders() {
int count=0;
for(BluePipeline p:getPipelines ()){
if(p instanceof BluePipelineFolder){
count++;
}
}
return count;
}
@Override
public Integer getNumberOfPipelines() {
int count=0;
for(BluePipeline p:getPipelines ()){
if(!(p instanceof BluePipelineFolder)){
count++;
}
}
return count;
}
@Override
public void favorite(@JsonBody FavoriteAction favoriteAction) {
if(favoriteAction == null) {
throw new ServiceException.BadRequestExpception("Must provide pipeline name");
}
FavoriteUtil.favoriteJob(folder.getFullName(), favoriteAction.isFavorite());
}
}

View File

@ -1,11 +1,15 @@
package io.jenkins.blueocean.service.embedded.rest;
import hudson.model.BuildableItem;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.rest.model.BlueRun;
import io.jenkins.blueocean.rest.model.BlueRunContainer;
import io.jenkins.blueocean.service.embedded.util.FavoriteUtil;
import jenkins.branch.MultiBranchProject;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.json.JsonBody;
@ -14,6 +18,7 @@ import org.kohsuke.stapler.verb.DELETE;
import java.io.IOException;
import static io.jenkins.blueocean.rest.Utils.ensureTrailingSlash;
import static io.jenkins.blueocean.service.embedded.rest.PipelineContainerImpl.isMultiBranchProjectJob;
/**
* @author Kohsuke Kawaguchi
@ -21,10 +26,19 @@ import static io.jenkins.blueocean.rest.Utils.ensureTrailingSlash;
public class PipelineImpl extends BluePipeline {
/*package*/ final Job job;
protected PipelineImpl(Job job) {
private final ItemGroup folder;
protected PipelineImpl(ItemGroup folder, Job job) {
this.job = job;
this.folder = folder;
}
public PipelineImpl(ItemGroup folder) {
this(folder, null);
}
public PipelineImpl(Job job) {
this(null, job);
}
@Override
public String getOrganization() {
return OrganizationImpl.INSTANCE.getName();
@ -41,7 +55,7 @@ public class PipelineImpl extends BluePipeline {
}
@Override
public int getWeatherScore() {
public Integer getWeatherScore() {
return job.getBuildHealth().getScore();
}
@ -93,6 +107,30 @@ public class PipelineImpl extends BluePipeline {
throw new ServiceException.BadRequestExpception("Must provide pipeline name");
}
FavoriteUtil.favoriteJob(job, favoriteAction.isFavorite());
FavoriteUtil.favoriteJob(job.getFullName(), favoriteAction.isFavorite());
}
@Override
public String getFullName(){
return job.getFullName();
}
public BluePipeline getPipelines(String name){
assert folder != null;
return getPipelines(folder, name);
}
protected static BluePipeline getPipelines(ItemGroup itemGroup, String name){
Item item = itemGroup.getItem(name);
if(item instanceof BuildableItem){
if(item instanceof MultiBranchProject){
return new MultiBranchPipelineImpl((MultiBranchProject) item);
}else if(!isMultiBranchProjectJob((BuildableItem) item) && item instanceof Job){
return new PipelineImpl(itemGroup, (Job) item);
}
}else if(item instanceof ItemGroup){
return new PipelineImpl((ItemGroup) item, null);
}
throw new ServiceException.NotFoundException(String.format("Pipeline %s not found", name));
}
}

View File

@ -1,25 +1,23 @@
package io.jenkins.blueocean.service.embedded.util;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.User;
import hudson.plugins.favorite.FavoritePlugin;
import hudson.plugins.favorite.user.FavoriteUserProperty;
import io.jenkins.blueocean.commons.ServiceException;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.kohsuke.stapler.Stapler;
import java.io.IOException;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.User;
import hudson.plugins.favorite.FavoritePlugin;
import hudson.plugins.favorite.user.FavoriteUserProperty;
import io.jenkins.blueocean.commons.ServiceException;
import jenkins.model.Jenkins;
/**
* @author Ivan Meredith
*/
public class FavoriteUtil {
public static void favoriteJob(Job job, boolean favorite) {
public static void favoriteJob(String fullName, boolean favorite) {
User user = User.current();
if(user == null) {
throw new ServiceException.ForbiddenException("Must be logged in to use set favotites");
@ -27,7 +25,7 @@ public class FavoriteUtil {
boolean set = false;
FavoriteUserProperty fup = user.getProperty(FavoriteUserProperty.class);
if(fup != null) {
set = fup.isJobFavorite(job.getFullName());
set = fup.isJobFavorite(fullName);
}
//TODO: FavoritePlugin is null
FavoritePlugin plugin = Jenkins.getInstance().getPlugin(FavoritePlugin.class);
@ -36,7 +34,7 @@ public class FavoriteUtil {
}
if(favorite != set) {
try {
plugin.doToggleFavorite(Stapler.getCurrentRequest(), Stapler.getCurrentResponse(), job.getFullName(), Jenkins.getAuthentication().getName(), false);
plugin.doToggleFavorite(Stapler.getCurrentRequest(), Stapler.getCurrentResponse(), fullName, Jenkins.getAuthentication().getName(), false);
} catch (IOException e) {
throw new ServiceException.UnexpectedErrorException("Something went wrong setting the favorite", e);
}

View File

@ -253,6 +253,7 @@ public abstract class BaseTest {
Assert.assertEquals("jenkins", resp.get("organization"));
Assert.assertEquals(p.getName(), resp.get("name"));
Assert.assertEquals(p.getDisplayName(), resp.get("displayName"));
Assert.assertEquals(p.getFullName(), resp.get("fullName"));
Assert.assertEquals(p.getBuildHealth().getScore(), resp.get("weatherScore"));
if(p.getLastSuccessfulBuild() != null){
Run b = p.getLastSuccessfulBuild();

View File

@ -37,11 +37,41 @@ public class PipelineApiTest extends BaseTest {
MockFolder folder = j.createFolder("folder1");
Project p = folder.createProject(FreeStyleProject.class, "test1");
Map response = get("/organizations/jenkins/pipelines/test1");
Map response = get("/organizations/jenkins/pipelines/folder1/test1");
validatePipeline(p, response);
}
@Test
public void getNestedFolderPipelineTest() throws IOException {
MockFolder folder1 = j.createFolder("folder1");
Project p1 = folder1.createProject(FreeStyleProject.class, "test1");
MockFolder folder2 = folder1.createProject(MockFolder.class, "folder2");
MockFolder folder3 = folder1.createProject(MockFolder.class, "folder3");
Project p2 = folder2.createProject(FreeStyleProject.class, "test2");
Map response = get("/organizations/jenkins/pipelines/folder1/pipelines/folder2/test2");
validatePipeline(p2, response);
List<Map> pipelines = get("/organizations/jenkins/pipelines/folder1/pipelines/folder2/pipelines/", List.class);
Assert.assertEquals(1, pipelines.size());
validatePipeline(p2, pipelines.get(0));
pipelines = get("/organizations/jenkins/pipelines/folder1/pipelines/", List.class);
Assert.assertEquals(3, pipelines.size());
Assert.assertEquals("folder2", pipelines.get(0).get("name"));
Assert.assertEquals("folder1/folder2", pipelines.get(0).get("fullName"));
response = get("/organizations/jenkins/pipelines/folder1");
Assert.assertEquals("folder1", response.get("name"));
Assert.assertEquals("folder1", response.get("displayName"));
Assert.assertEquals(2, response.get("numberOfFolders"));
Assert.assertEquals(1, response.get("numberOfPipelines"));
Assert.assertEquals("folder1", response.get("fullName"));
}
@Test
public void getPipelineTest() throws IOException {
Project p = j.createFreeStyleProject("pipeline1");

View File

@ -63,6 +63,7 @@ $$
"organization" : "jenkins",
"name" : "pipeline1",
"displayName": "pipeline1",
"fullName": "pipeline1",
"weatherScore": 100,
"estimatedDurationInMillis": 20264,
"lastSuccessfulRun": "http://localhost:64106/jenkins/blue/rest/organizations/jenkins/pipelines/pipeline1/runs/1",
@ -100,10 +101,68 @@ $$
"organization" : "jenkins",
"name" : "pipeline1",
"displayName": "pipeline1",
"fullName" : "pipeline1",
"weatherScore": 100,
"estimatedDurationInMillis": 280,
}
]
## Get a Folder
curl -v -X GET http://localhost:63934/jenkins/blue/rest/organizations/jenkins/pipelines/folder1/
{
"_class" : "io.jenkins.blueocean.service.embedded.rest.PipelineFolderImpl",
"displayName" : "folder1",
"fullName" : "folder1",
"name" : "folder1",
"organization" : "jenkins",
"numberOfFolders" : 1,
"numberOfPipelines" : 1
}
## Get Nested Pipeline Inside A Folder
curl -v -X GET http://localhost:62054/jenkins/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/folder2/test2/
{
"_class" : "io.jenkins.blueocean.service.embedded.rest.PipelineImpl",
"displayName" : "test2",
"estimatedDurationInMillis" : -1,
"fullName" : "folder1/folder2/test2",
"lastSuccessfulRun" : null,
"latestRun" : null,
"name" : "test2",
"fullName" : "test2",
"organization" : "jenkins",
"weatherScore" : 100
}
## Get nested Folder and Pipeline
Pipelines can be nested inside folder.
curl -v -X GET http://localhost:62054/jenkins/blue/rest/organizations/jenkins/pipelines/folder1/pipelines/
[ {
"_class" : "io.jenkins.blueocean.service.embedded.rest.PipelineFolderImpl",
"displayName" : "folder2",
"fullName" : "folder1/folder2",
"name" : "folder2",
"organization" : "jenkins",
"numberOfFolders" : 0,
"numberOfPipelines" : 1
}, {
"_class" : "io.jenkins.blueocean.service.embedded.rest.PipelineImpl",
"displayName" : "test1",
"estimatedDurationInMillis" : -1,
"fullName" : "folder1/test1",
"lastSuccessfulRun" : null,
"latestRun" : null,
"name" : "test1",
"organization" : "jenkins",
"weatherScore" : 100
} ]
## Get all runs in a pipeline

View File

@ -3,11 +3,8 @@ package io.jenkins.blueocean.rest.model;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.json.JsonBody;
import org.kohsuke.stapler.json.JsonResponse;
import org.kohsuke.stapler.verb.PUT;
import javax.xml.ws.WebFault;
/**
* Defines pipeline state and its routing
*
@ -17,6 +14,7 @@ public abstract class BluePipeline extends Resource {
public static final String ORGANIZATION="organization";
public static final String NAME="name";
public static final String DISPLAY_NAME="displayName";
public static final String FULL_NAME="fullName";
public static final String WEATHER_SCORE ="weatherScore";
public static final String LATEST_RUN = "latestRun";
public static final String ESTIMATED_DURATION = "estimatedDurationInMillis";
@ -41,11 +39,17 @@ public abstract class BluePipeline extends Resource {
@Exported(name = DISPLAY_NAME)
public abstract String getDisplayName();
/**
* @return Includes parent folders if any. For example folder1/folder2/p1
*/
@Exported(name = FULL_NAME)
public abstract String getFullName();
/**
* @return weather health score percentile
*/
@Exported(name = WEATHER_SCORE)
public abstract int getWeatherScore();
public abstract Integer getWeatherScore();
/**
* @return The Latest Run for the branch

View File

@ -0,0 +1,94 @@
package io.jenkins.blueocean.rest.model;
import org.kohsuke.stapler.export.Exported;
/**
* Folder has pipelines, could also hold another BluePipelineFolders.
*
* BluePipelineFolder subclasses BluePipeline in order to handle recursive pipelines path:
*
* /pipelines/f1/pipelines/f2/pipelines/p1
*
*
* @author Vivek Pandey
*
* @see BluePipelineContainer
*/
public abstract class BluePipelineFolder extends BluePipeline {
private static final String NUMBER_OF_PIPELINES = "numberOfPipelines";
private static final String NUMBER_OF_FOLDERS = "numberOfFolders";
/**
* @return Gives pipeline container
*/
public abstract BluePipelineContainer getPipelines();
/**
*
* Gets nested BluePipeline inside the BluePipelineFolder
*
* For example for: /pipelines/folder1/pipelines/folder2/pipelines/p1, call sequnce will be:
*
* <ul>
* <li>getPipelines().get("folder1")</li>
* <li>getPipelines().get(folder2)</li>
* <li>getDynamics(p1)</li>
* </ul>
*
* @param name name of pipeline
*
* @return a {@link BluePipeline}
*/
public BluePipeline getDynamic(String name){
return getPipelines().get(name);
}
/**
* @return Number of folders in this folder
*/
@Exported(name = NUMBER_OF_FOLDERS)
public abstract Integer getNumberOfFolders();
/**
* @return Number of pipelines in this folder. Pipeline is any buildable type.
*/
@Exported(name = NUMBER_OF_PIPELINES)
public abstract Integer getNumberOfPipelines();
@Override
@Exported(skipNull = true)
public Integer getWeatherScore() {
return null;
}
@Override
@Exported(skipNull = true)
public BlueRun getLatestRun() {
return null;
}
@Override
@Exported(skipNull = true)
public String getLastSuccessfulRun() {
return null;
}
@Override
@Exported(skipNull = true)
public Long getEstimatedDurationInMillis() {
return null;
}
@Override
@Exported(skipNull = true)
public BlueRunContainer getRuns() {
return null;
}
}