Skip to content

Commit

Permalink
JENKINS-38584 - rest API for returning available steps, not whitelisted
Browse files Browse the repository at this point in the history
  • Loading branch information
kzantow committed Nov 11, 2016
1 parent 0ee8e89 commit 54f11f7
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -2,3 +2,6 @@ node/
node_modules/
target/
work/
/.classpath
/.project
/.settings/
20 changes: 17 additions & 3 deletions pom.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
Expand Down Expand Up @@ -44,9 +43,24 @@
<dependency>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean</artifactId>
<version>1.0.0-b07</version>
<version>1.0.0-b11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-rest</artifactId>
<version>1.0.0-b11</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>2.8</version>
</dependency>
</dependencies>

</project>
@@ -0,0 +1,55 @@
package io.blueocean.rest.pipeline.editor;

import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

@ExportedBean
public interface PipelineStepMetadata {
/**
* Identifier used for the 'function' name in the pipeline step, used in the pipeline file
*/
@Exported
public String getFunctionName();

/**
* The Java class name for this step (since we can't seem to export a Class<?>...)
*/
@Exported
public String getType();

/**
* Display Name of the pipeline step, used in the pipeline file
*/
@Exported
public String getDisplayName();

/**
* The Java class names that this pipeline step exports into context
*/
@Exported
public String[] getProvidedContext();

/**
* The Java class names that this pipeline requires to be in context
*/
@Exported
public String[] getRequiredContext();

/**
* Indicates this step wraps a block of other steps
*/
@Exported
public boolean getIsBlockContainer();

/**
* Snippetizer URL for this step (these are the same with different POST parameters...)
*/
@Exported
public String getSnippetizerUrl();

/**
* Properties the steps supports
*/
@Exported
public PipelineStepPropertyMetadata[] getProperties();
}
@@ -0,0 +1,214 @@
package io.blueocean.rest.pipeline.editor;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.workflow.cps.Snippetizer;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.verb.GET;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;

import hudson.Extension;
import hudson.ExtensionList;
import io.jenkins.blueocean.commons.stapler.TreeResponse;
import io.jenkins.blueocean.rest.ApiRoutable;
import jenkins.model.Jenkins;

/**
* This provides and Blueocean REST API endpoint to obtain pipeline step metadata.
*/
@Extension
public class PipelineStepMetadataService implements ApiRoutable {
ParameterNameDiscoverer nameFinder = new LocalVariableTableParameterNameDiscoverer();

@Override
public String getUrlName() {
return "pipeline-step-metadata";
}

/**
* Basic exported model for {@link PipelineStepMetadata}
*/
@ExportedBean
public static class BasicPipelineStepMetadata implements PipelineStepMetadata {
private String displayName;
private String functionName;
private Class<?> type;
private String descriptorUrl;
private List<Class<?>> requiredContext = new ArrayList<Class<?>>();
private List<Class<?>> providedContext = new ArrayList<Class<?>>();
private boolean isWrapper = false;
private String snippetizerUrl;
private List<PipelineStepPropertyMetadata> props = new ArrayList<PipelineStepPropertyMetadata>();

public BasicPipelineStepMetadata(String functionName, Class<?> type, String displayName) {
super();
this.displayName = displayName;
this.type = type;
this.functionName = functionName;
}

@Exported
@Override
public String getDisplayName() {
return displayName;
}

@Exported
@Override
public String getFunctionName() {
return functionName;
}

@Exported
@Override
public String[] getRequiredContext() {
List<String> out = new ArrayList<String>();
for (Class<?> c : requiredContext) {
out.add(c.getName());
}
return out.toArray(new String[out.size()]);
}

@Exported
@Override
public String[] getProvidedContext() {
List<String> out = new ArrayList<String>();
for (Class<?> c : providedContext) {
out.add(c.getName());
}
return out.toArray(new String[out.size()]);
}

@Exported
@Override
public String getSnippetizerUrl() {
return snippetizerUrl;
}

@Exported
public String descriptorUrl() {
return descriptorUrl;
}

@Exported
@Override
public boolean getIsBlockContainer() {
return isWrapper;
}

@Exported
@Override
public String getType() {
return type.getName();
}

@Exported
@Override
public PipelineStepPropertyMetadata[] getProperties() {
return props.toArray(new PipelineStepPropertyMetadata[props.size()]);
}
}

/**
* Basic exported model for {@link PipelineStepPropertyDescriptor)
*/
@ExportedBean
public static class BasicPipelineStepPropertyMetadata implements PipelineStepPropertyMetadata{
private String name;
private Class<?> type;
private boolean isRequired = false;

@Exported
@Override
public String getName() {
return name;
}

@Exported
@Override
public String getType() {
return type.getName();
}

@Exported
@Override
public boolean getIsRequired() {
return isRequired;
}
}

/**
* Function to return all step descriptors present in the system when accessed through the REST API
*/
@GET
@WebMethod(name = "")
@TreeResponse
public PipelineStepMetadata[] getPipelineStepMetadata() throws IOException {
Jenkins j = Jenkins.getInstance();
Snippetizer snippetizer = ExtensionList.create(j, Snippetizer.class).get(0);

List<PipelineStepMetadata> pd = new ArrayList<PipelineStepMetadata>();
// POST to this with parameter names
// e.g. json:{"time": "1", "unit": "NANOSECONDS", "stapler-class": "org.jenkinsci.plugins.workflow.steps.TimeoutStep", "$class": "org.jenkinsci.plugins.workflow.steps.TimeoutStep"}
String snippetizerUrl = Stapler.getCurrentRequest().getContextPath() + "/" + snippetizer.getUrlName() + "/generateSnippet";

for (StepDescriptor d : StepDescriptor.all()) {
PipelineStepMetadata step = descriptorMetadata(d, snippetizerUrl);
pd.add(step);
}

return pd.toArray(new PipelineStepMetadata[pd.size()]);
}

private PipelineStepMetadata descriptorMetadata(StepDescriptor d, String snippetizerUrl) {
BasicPipelineStepMetadata meta = new BasicPipelineStepMetadata(d.getFunctionName(), d.clazz, d.getDisplayName());
meta.snippetizerUrl = snippetizerUrl + "?$class=" + d.clazz.getName();

meta.isWrapper = d.takesImplicitBlockArgument();
meta.requiredContext.addAll(d.getRequiredContext());
meta.providedContext.addAll(d.getProvidedContext());
meta.descriptorUrl = d.getDescriptorUrl();

for (Method m : d.clazz.getDeclaredMethods()) {
if (m.isAnnotationPresent(DataBoundSetter.class)) {
String paramName = StringUtils.uncapitalize(m.getName().substring(3));
Class<?> paramType = m.getParameterTypes()[0];
BasicPipelineStepPropertyMetadata param = new BasicPipelineStepPropertyMetadata();
param.name = paramName;
param.type = paramType;
meta.props.add(param);
}
}

for (Constructor<?> c : d.clazz.getDeclaredConstructors()) {
if (c.isAnnotationPresent(DataBoundConstructor.class)) {
Class<?>[] paramTypes = c.getParameterTypes();
String[] paramNames = nameFinder.getParameterNames(c);
if(paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
Class<?> paramType = paramTypes[i];
BasicPipelineStepPropertyMetadata param = new BasicPipelineStepPropertyMetadata();
param.name = paramName;
param.type = paramType;
meta.props.add(param);
}
}
}
}

return meta;
}
}
@@ -0,0 +1,15 @@
package io.blueocean.rest.pipeline.editor;

import java.util.List;

import hudson.ExtensionPoint;

/**
* Allows plugins to modify property metadata, e.g. providing additional form fields and such
*/
public interface PipelineStepPropertyDecorator extends ExtensionPoint {
/**
* Adjust the PipelineStepPropertyMetadata for the pipeline step
*/
public PipelineStepPropertyMetadata decorate(PipelineStepMetadata step, List<PipelineStepPropertyMetadata> property);
}
@@ -0,0 +1,29 @@
package io.blueocean.rest.pipeline.editor;

import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;


/**
* Basic pipeline step property descriptor
*/
@ExportedBean
public interface PipelineStepPropertyMetadata {
/**
* Name of the property
*/
@Exported
public String getName();

/**
* Indicates this property is required
*/
@Exported
public boolean getIsRequired();

/**
* Java class name for the property
*/
@Exported
public String getType();
}

0 comments on commit 54f11f7

Please sign in to comment.