Feature/jenkins 38848: Credential enumeration API (#593)
* Adding routable API to blueocean api path * JENKINS-38848# Credential GET API - Allows plugins to serve their object graphs from /organizations/:id/ API. - Doesn't quite work for credentials plugin as for POST requests it requires form submissions * Credential search API - credential reponse description elements defaults to displayName:domain:type. * Fixed links * Organization route extensibility simplified - OrganizationAction is all one needs to expose it's object graph inside organization route - ApiRoute remains to be extension point to be added at root of bluocean route * Added missing file * Added domain to credential object. * Credential creation API * Refactoring. Moved OrganizationRoute to blueocean-rest.
This commit is contained in:
parent
873c0c2f85
commit
71b813e2c6
|
@ -75,6 +75,12 @@
|
|||
<version>1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins</groupId>
|
||||
<artifactId>credentials</artifactId>
|
||||
<version>2.1.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Not needed by blueocean runtime but adds to blueocean experience -->
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins</groupId>
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
package io.jenkins.blueocean.rest.impl.pipeline.credential;
|
||||
|
||||
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
|
||||
import com.cloudbees.plugins.credentials.common.IdCredentials;
|
||||
import com.cloudbees.plugins.credentials.domains.Domain;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import io.jenkins.blueocean.rest.Navigable;
|
||||
import io.jenkins.blueocean.rest.Reachable;
|
||||
import io.jenkins.blueocean.rest.hal.Link;
|
||||
import io.jenkins.blueocean.rest.model.Container;
|
||||
import io.jenkins.blueocean.rest.model.CreateResponse;
|
||||
import io.jenkins.blueocean.rest.model.Resource;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
import org.kohsuke.stapler.export.Exported;
|
||||
import org.kohsuke.stapler.json.JsonBody;
|
||||
import org.kohsuke.stapler.verb.POST;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Credential API implementation.
|
||||
*
|
||||
* TODO: Remove it once proper REST API is implemented in Credentials plugin
|
||||
*
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public class CredentialApi extends Resource {
|
||||
|
||||
private final CredentialsStoreAction credentialStoreAction;
|
||||
private final Reachable parent;
|
||||
|
||||
public CredentialApi(CredentialsStoreAction ca, Reachable parent) {
|
||||
this.credentialStoreAction = ca;
|
||||
this.parent = parent;
|
||||
|
||||
}
|
||||
|
||||
@Exported
|
||||
public String getStore(){
|
||||
return credentialStoreAction.getUrlName();
|
||||
}
|
||||
|
||||
@Navigable
|
||||
public Container<CredentialDomain> getDomains(){
|
||||
return new Container<CredentialDomain>() {
|
||||
private final Link self = CredentialApi.this.getLink().rel("domains");
|
||||
|
||||
Map<String, CredentialsStoreAction.DomainWrapper> map = credentialStoreAction.getDomains();
|
||||
@Override
|
||||
public CredentialDomain get(String name) {
|
||||
return new CredentialDomain(map.get(name), getLink());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return self;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<CredentialDomain> iterator() {
|
||||
final Iterator<CredentialsStoreAction.DomainWrapper> i = map.values().iterator();
|
||||
return new Iterator<CredentialDomain>(){
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return i.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialDomain next() {
|
||||
return new CredentialDomain(i.next(), getLink());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new ServiceException.NotImplementedException("Not implemented yet");
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return parent.getLink().rel(getStore());
|
||||
}
|
||||
|
||||
public static class CredentialDomain extends Resource{
|
||||
|
||||
private final Link self;
|
||||
private final CredentialsStoreAction.DomainWrapper domainWrapper;
|
||||
|
||||
public CredentialDomain(CredentialsStoreAction.DomainWrapper domainWrapper, Link parent) {
|
||||
this.self = parent.rel(domainWrapper.getUrlName());
|
||||
this.domainWrapper = domainWrapper;
|
||||
}
|
||||
|
||||
@Exported(inline = true, merge = true)
|
||||
public CredentialsStoreAction.DomainWrapper getDomain(){
|
||||
return domainWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return self;
|
||||
}
|
||||
|
||||
@Navigable
|
||||
public CredentialValueContainer getCredentials(){
|
||||
return new CredentialValueContainer(domainWrapper, this);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CredentialValueContainer extends Container<Credential>{
|
||||
private final CredentialsStoreAction.DomainWrapper domainWrapper;
|
||||
private final Link self;
|
||||
|
||||
public CredentialValueContainer(CredentialsStoreAction.DomainWrapper domainWrapper, Reachable parent) {
|
||||
this.domainWrapper = domainWrapper;
|
||||
this.self = parent.getLink().rel("credentials");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Credential get(String name) {
|
||||
return new Credential(domainWrapper.getCredential(name), self);
|
||||
}
|
||||
|
||||
@POST
|
||||
@WebMethod(name = "")
|
||||
public CreateResponse create(@JsonBody JSONObject body, StaplerRequest request) throws IOException {
|
||||
|
||||
final IdCredentials credentials = request.bindJSON(IdCredentials.class, body.getJSONObject("credentials"));
|
||||
domainWrapper.getStore().addCredentials(domainWrapper.getDomain(), credentials);
|
||||
|
||||
|
||||
final Domain domain = domainWrapper.getDomain();
|
||||
domainWrapper.getStore().addCredentials(domain, credentials);
|
||||
|
||||
return new CreateResponse(new Credential(domainWrapper.getCredentials().get(credentials.getId()), getLink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return self;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Credential> iterator() {
|
||||
final Iterator<CredentialsStoreAction.CredentialsWrapper> i = domainWrapper.getCredentialsList().iterator();
|
||||
return new Iterator<Credential>(){
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return i.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Credential next() {
|
||||
return new Credential(i.next(),self);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new ServiceException.NotImplementedException("Not implemented yet");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Credential extends Resource{
|
||||
|
||||
private final Link self;
|
||||
private final CredentialsStoreAction.CredentialsWrapper credentialsWrapper;
|
||||
|
||||
public Credential(CredentialsStoreAction.CredentialsWrapper credentialsWrapper, Link parent) {
|
||||
this.self = parent.rel(credentialsWrapper.getUrlName());
|
||||
this.credentialsWrapper = credentialsWrapper;
|
||||
}
|
||||
|
||||
@Exported(merge = true, inline = true)
|
||||
public CredentialsStoreAction.CredentialsWrapper getCredential(){
|
||||
return credentialsWrapper;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return self;
|
||||
}
|
||||
|
||||
@Exported
|
||||
public String getDomain(){
|
||||
return credentialsWrapper.getDomain().getUrlName();
|
||||
}
|
||||
|
||||
/**
|
||||
* If description is empty its displayName:domain:type, otherwise just the given description name
|
||||
*/
|
||||
@Exported
|
||||
public String getDescription(){
|
||||
if(credentialsWrapper.getDescription() == null || credentialsWrapper.getDescription().trim().isEmpty()){
|
||||
return String.format("%s:%s:%s",credentialsWrapper.getDisplayName(),credentialsWrapper.getDomain().getUrlName(), credentialsWrapper.getTypeName());
|
||||
}
|
||||
return credentialsWrapper.getDescription();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package io.jenkins.blueocean.rest.impl.pipeline.credential;
|
||||
|
||||
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
|
||||
import com.cloudbees.plugins.credentials.ViewCredentialsAction;
|
||||
import hudson.Extension;
|
||||
import hudson.ExtensionList;
|
||||
import io.jenkins.blueocean.rest.hal.Link;
|
||||
import io.jenkins.blueocean.rest.model.BlueOrganization;
|
||||
import io.jenkins.blueocean.rest.model.Container;
|
||||
import io.jenkins.blueocean.rest.OrganizationRoute;
|
||||
import org.kohsuke.stapler.export.ExportedBean;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Credential API container
|
||||
*
|
||||
* TODO: can very well be moved in it's own plugin along with {@link CredentialApi}
|
||||
*
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension
|
||||
@ExportedBean
|
||||
public class CredentialContainer extends Container<CredentialApi> implements OrganizationRoute {
|
||||
private final Link self;
|
||||
|
||||
public CredentialContainer() {
|
||||
BlueOrganization organization=null;
|
||||
for(BlueOrganization action: ExtensionList.lookup(BlueOrganization.class)){
|
||||
organization = action;
|
||||
};
|
||||
this.self = (organization != null) ? organization.getLink().rel("credentials")
|
||||
: new Link("/organizations/jenkins/credentials/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return self;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialApi get(String name) {
|
||||
for(CredentialApi api: this){
|
||||
if(api.getStore().equals(name)){
|
||||
return api;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<CredentialApi> iterator() {
|
||||
List<CredentialApi> apis = new ArrayList<>();
|
||||
for(ViewCredentialsAction action: ExtensionList.lookup(ViewCredentialsAction.class)){
|
||||
for(CredentialsStoreAction c:action.getStoreActions()){
|
||||
apis.add(new CredentialApi(c, this));
|
||||
}
|
||||
};
|
||||
return apis.iterator();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package io.jenkins.blueocean.rest.impl.pipeline.credential;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.ExtensionList;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import io.jenkins.blueocean.rest.OmniSearch;
|
||||
import io.jenkins.blueocean.rest.Query;
|
||||
import io.jenkins.blueocean.rest.pageable.Pageable;
|
||||
import io.jenkins.blueocean.rest.pageable.Pageables;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Credential search API
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension
|
||||
public class CredentialSearch extends OmniSearch<CredentialApi.Credential> {
|
||||
@Override
|
||||
public String getType() {
|
||||
return "credential";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pageable<CredentialApi.Credential> search(Query q) {
|
||||
List<CredentialApi.Credential> credentials = new ArrayList<>();
|
||||
String domain = q.param("domain");
|
||||
|
||||
ExtensionList<CredentialContainer> extensionList = ExtensionList.lookup(CredentialContainer.class);
|
||||
if(!extensionList.isEmpty()) {
|
||||
CredentialContainer credentialContainer = extensionList.get(0);
|
||||
|
||||
Iterator<CredentialApi> it = credentialContainer.iterator();
|
||||
|
||||
if (it.hasNext()) {
|
||||
CredentialApi api = it.next();
|
||||
if(domain != null){
|
||||
CredentialApi.CredentialDomain d = api.getDomains().get(domain);
|
||||
if(d == null){
|
||||
throw new ServiceException.BadRequestExpception("Credential domain "+ domain +" not found");
|
||||
}
|
||||
for (CredentialApi.Credential c : d.getCredentials()) {
|
||||
credentials.add(c);
|
||||
}
|
||||
}else {
|
||||
for (CredentialApi.CredentialDomain d : api.getDomains()) {
|
||||
for (CredentialApi.Credential c : d.getCredentials()) {
|
||||
credentials.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pageables.wrap(credentials);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package io.jenkins.blueocean.rest.impl.pipeline;
|
||||
|
||||
import com.cloudbees.plugins.credentials.CredentialsProvider;
|
||||
import com.cloudbees.plugins.credentials.CredentialsScope;
|
||||
import com.cloudbees.plugins.credentials.CredentialsStore;
|
||||
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
|
||||
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
|
||||
import com.cloudbees.plugins.credentials.ViewCredentialsAction;
|
||||
import com.cloudbees.plugins.credentials.domains.Domain;
|
||||
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import hudson.ExtensionList;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public class CredentialApiTest extends PipelineBaseTest {
|
||||
|
||||
@Test
|
||||
public void listCredentials() throws IOException {
|
||||
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
|
||||
CredentialsStore systemStore = system.getStore(j.getInstance());
|
||||
systemStore.addDomain(new Domain("domain1", null, null));
|
||||
systemStore.addCredentials(systemStore.getDomainByName("domain1"), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null,null, "admin", "pass$wd"));
|
||||
|
||||
|
||||
CredentialsStoreAction credentialsStoreAction = ExtensionList.lookup(ViewCredentialsAction.class).get(0).getStore("system");
|
||||
CredentialsStoreAction.DomainWrapper domainWrapper = credentialsStoreAction.getDomain("domain1");
|
||||
CredentialsStoreAction.CredentialsWrapper credentialsWrapper = domainWrapper.getCredentialsList().get(0);
|
||||
|
||||
|
||||
List<Map> creds = get("/organizations/jenkins/credentials/system/domains/domain1/credentials/", List.class);
|
||||
Assert.assertEquals(1, creds.size());
|
||||
Map cred = creds.get(0);
|
||||
Assert.assertNotNull(cred.get("id"));
|
||||
|
||||
Map cred1 = get("/organizations/jenkins/credentials/system/domains/domain1/credentials/"+cred.get("id")+"/");
|
||||
|
||||
Assert.assertEquals(credentialsWrapper.getId(),cred1.get("id"));
|
||||
Assert.assertEquals(credentialsWrapper.getTypeName(),cred1.get("typeName"));
|
||||
Assert.assertEquals(credentialsWrapper.getDisplayName(),cred1.get("displayName"));
|
||||
Assert.assertEquals(credentialsWrapper.getFullName(),cred1.get("fullName"));
|
||||
Assert.assertEquals(String.format("%s:%s:%s", credentialsWrapper.getDisplayName(), credentialsWrapper.getDomain().getUrlName(), credentialsWrapper.getTypeName()),cred1.get("description"));
|
||||
Assert.assertEquals(credentialsWrapper.getDomain().getUrlName(),cred1.get("domain"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listAllCredentials() throws IOException {
|
||||
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
|
||||
CredentialsStore systemStore = system.getStore(j.getInstance());
|
||||
systemStore.addDomain(new Domain("domain1", null, null));
|
||||
systemStore.addDomain(new Domain("domain2", null, null));
|
||||
systemStore.addCredentials(systemStore.getDomainByName("domain1"), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null,null, "admin", "pass$wd"));
|
||||
systemStore.addCredentials(systemStore.getDomainByName("domain2"), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null,null, "joe", "pass$wd"));
|
||||
|
||||
CredentialsStoreAction credentialsStoreAction = ExtensionList.lookup(ViewCredentialsAction.class).get(0).getStore("system");
|
||||
CredentialsStoreAction.DomainWrapper domain1 = credentialsStoreAction.getDomain("domain1");
|
||||
CredentialsStoreAction.DomainWrapper domain2 = credentialsStoreAction.getDomain("domain2");
|
||||
|
||||
CredentialsStoreAction.CredentialsWrapper credentials1 = domain1.getCredentialsList().get(0);
|
||||
CredentialsStoreAction.CredentialsWrapper credentials2 = domain2.getCredentialsList().get(0);
|
||||
List<Map> creds = get("/search?q=type:credential", List.class);
|
||||
Assert.assertEquals(2, creds.size());
|
||||
Assert.assertEquals(credentials1.getId(), creds.get(0).get("id"));
|
||||
Assert.assertEquals(credentials2.getId(), creds.get(1).get("id"));
|
||||
|
||||
creds = get("/search?q=type:credential;domain:domain2", List.class);
|
||||
Assert.assertEquals(1, creds.size());
|
||||
Assert.assertEquals(credentials2.getId(), creds.get(0).get("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSshCredentialUsingSshFileOnMaster() throws IOException {
|
||||
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
|
||||
CredentialsStore systemStore = system.getStore(j.getInstance());
|
||||
systemStore.addDomain(new Domain("domain1", null, null));
|
||||
|
||||
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
|
||||
ImmutableMap.of("credentials",
|
||||
new ImmutableMap.Builder<String,Object>()
|
||||
.put("privateKeySource", ImmutableMap.of("privateKeyFile", "~/.ssh/blah", "stapler-class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$FileOnMasterPrivateKeySource"))
|
||||
.put("passphrase", "ssh2")
|
||||
.put("scope", "GLOBAL")
|
||||
.put("description", "ssh2 desc")
|
||||
.put("$class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey")
|
||||
.put("username", "ssh2").build()
|
||||
)
|
||||
, 201);
|
||||
Assert.assertEquals("SSH Username with private key", resp.get("typeName"));
|
||||
Assert.assertEquals("domain1", resp.get("domain"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSshCredentialUsingDefaultSshOnMaster() throws IOException {
|
||||
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
|
||||
CredentialsStore systemStore = system.getStore(j.getInstance());
|
||||
systemStore.addDomain(new Domain("domain1", null, null));
|
||||
|
||||
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
|
||||
ImmutableMap.of("credentials",
|
||||
new ImmutableMap.Builder<String,Object>()
|
||||
.put("privateKeySource", ImmutableMap.of("stapler-class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$UsersPrivateKeySource"))
|
||||
.put("passphrase", "ssh2")
|
||||
.put("scope", "GLOBAL")
|
||||
.put("description", "ssh2 desc")
|
||||
.put("$class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey")
|
||||
.put("username", "ssh2").build()
|
||||
)
|
||||
, 201);
|
||||
Assert.assertEquals("SSH Username with private key", resp.get("typeName"));
|
||||
Assert.assertEquals("domain1", resp.get("domain"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createSshCredentialUsingDirectSsh() throws IOException {
|
||||
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
|
||||
CredentialsStore systemStore = system.getStore(j.getInstance());
|
||||
systemStore.addDomain(new Domain("domain1", null, null));
|
||||
|
||||
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
|
||||
ImmutableMap.of("credentials",
|
||||
new ImmutableMap.Builder<String,Object>()
|
||||
.put("privateKeySource", ImmutableMap.of(
|
||||
"privateKey", "abcabc1212",
|
||||
"stapler-class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource"))
|
||||
.put("passphrase", "ssh2")
|
||||
.put("scope", "GLOBAL")
|
||||
.put("description", "ssh2 desc")
|
||||
.put("$class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey")
|
||||
.put("username", "ssh2").build()
|
||||
)
|
||||
, 201);
|
||||
Assert.assertEquals("SSH Username with private key", resp.get("typeName"));
|
||||
Assert.assertEquals("domain1", resp.get("domain"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void createUsingUsernamePassword() throws IOException {
|
||||
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
|
||||
CredentialsStore systemStore = system.getStore(j.getInstance());
|
||||
systemStore.addDomain(new Domain("domain1", null, null));
|
||||
|
||||
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
|
||||
ImmutableMap.of("credentials",
|
||||
new ImmutableMap.Builder<String,Object>()
|
||||
.put("password", "abcd")
|
||||
.put("stapler-class", "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl")
|
||||
.put("scope", "GLOBAL")
|
||||
.put("description", "joe desc")
|
||||
.put("$class", "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl")
|
||||
.put("username", "joe").build()
|
||||
)
|
||||
, 201);
|
||||
Assert.assertEquals("Username with password", resp.get("typeName"));
|
||||
Assert.assertEquals("domain1", resp.get("domain"));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,19 +1,21 @@
|
|||
package io.jenkins.blueocean.service.embedded.rest;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import hudson.Util;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.model.Action;
|
||||
import hudson.model.User;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import io.jenkins.blueocean.commons.stapler.JsonBody;
|
||||
import io.jenkins.blueocean.rest.ApiHead;
|
||||
import io.jenkins.blueocean.rest.OrganizationRoute;
|
||||
import io.jenkins.blueocean.rest.hal.Link;
|
||||
import io.jenkins.blueocean.rest.model.BlueOrganization;
|
||||
import io.jenkins.blueocean.rest.model.BluePipelineContainer;
|
||||
import io.jenkins.blueocean.rest.model.BlueUser;
|
||||
import io.jenkins.blueocean.rest.model.BlueUserContainer;
|
||||
import io.jenkins.blueocean.rest.model.GenericResource;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.kohsuke.stapler.WebMethod;
|
||||
import org.kohsuke.stapler.export.ExportedBean;
|
||||
import org.kohsuke.stapler.verb.DELETE;
|
||||
import org.kohsuke.stapler.verb.PUT;
|
||||
|
||||
|
@ -35,9 +37,6 @@ public class OrganizationImpl extends BlueOrganization {
|
|||
*/
|
||||
public static final OrganizationImpl INSTANCE = new OrganizationImpl();
|
||||
|
||||
private OrganizationImpl() {
|
||||
}
|
||||
|
||||
/**
|
||||
* In embedded mode, there's only one organization
|
||||
*/
|
||||
|
@ -95,4 +94,39 @@ public class OrganizationImpl extends BlueOrganization {
|
|||
return ApiHead.INSTANCE().getLink().rel("organizations/"+getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Give plugins chance to handle this API route.
|
||||
*
|
||||
* @param route URL path that needs handling. e.g. for requested url /rest/organizations/:id/xyz, route param value will be 'xyz'
|
||||
* @return stapler object that can handle give route. Could be null
|
||||
*/
|
||||
public Object getDynamic(String route){
|
||||
//First look for OrganizationActions
|
||||
for(OrganizationRoute organizationRoute: ExtensionList.lookup(OrganizationRoute.class)){
|
||||
if(organizationRoute.getUrlName() != null && organizationRoute.getUrlName().equals(route)){
|
||||
return wrap(organizationRoute);
|
||||
}
|
||||
}
|
||||
|
||||
// No OrganizationRoute found, now lookup in available actions from Jenkins instance serving root
|
||||
for(Action action:Jenkins.getInstance().getActions()) {
|
||||
if (action.getUrlName() != null && action.getUrlName().equals(route)) {
|
||||
return wrap(action);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object wrap(Object action){
|
||||
if (isExportedBean(action.getClass())) {
|
||||
return action;
|
||||
} else {
|
||||
return new GenericResource<>(action);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExportedBean(Class clz){
|
||||
return clz.getAnnotation(ExportedBean.class) != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ public final class ApiHead implements RootRoutable, Reachable {
|
|||
|
||||
private volatile Map<String,ApiRoutable> apis;
|
||||
|
||||
public static final String URL_NAME="rest";
|
||||
|
||||
/**
|
||||
* Search API
|
||||
*
|
||||
|
@ -55,7 +57,7 @@ public final class ApiHead implements RootRoutable, Reachable {
|
|||
*/
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "rest";
|
||||
return URL_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -119,7 +121,7 @@ public final class ApiHead implements RootRoutable, Reachable {
|
|||
if(apiMap == null){
|
||||
Map<String,ApiRoutable> apiMapTmp = new HashMap<>();
|
||||
for ( ApiRoutable api : ExtensionList.lookup(ApiRoutable.class)) {
|
||||
apiMapTmp.put(api.getUrlName(),api);
|
||||
apiMapTmp.put(api.getUrlName(), api);
|
||||
}
|
||||
apis = apiMap = apiMapTmp;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.jenkins.blueocean.Routable;
|
|||
* Marks the REST API endpoints that are exposed by {@link ApiHead}
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public interface ApiRoutable extends Routable, ExtensionPoint {
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package io.jenkins.blueocean.rest;
|
||||
|
||||
import hudson.ExtensionPoint;
|
||||
import io.jenkins.blueocean.Routable;
|
||||
|
||||
/**
|
||||
* Route contributing to {@link io.jenkins.blueocean.rest.model.BlueOrganization}: url path /organization/:id/:organizationRoute.urlName()
|
||||
*
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public interface OrganizationRoute extends Routable, ExtensionPoint {
|
||||
}
|
|
@ -66,7 +66,6 @@ public abstract class BlueExtensionClassContainer implements ApiRoutable, Extens
|
|||
public String getUrlName() {
|
||||
return "classes";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.jenkins.blueocean.rest.model;
|
||||
|
||||
import io.jenkins.blueocean.Routable;
|
||||
import io.jenkins.blueocean.rest.Navigable;
|
||||
import io.jenkins.blueocean.rest.annotation.Capability;
|
||||
import org.kohsuke.stapler.export.Exported;
|
||||
|
@ -12,11 +13,16 @@ import static io.jenkins.blueocean.rest.model.KnownCapabilities.BLUE_ORGANIZATIO
|
|||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
@Capability(BLUE_ORGANIZATION)
|
||||
public abstract class BlueOrganization extends Resource {
|
||||
public abstract class BlueOrganization extends Resource implements Routable{
|
||||
public static final String NAME="name";
|
||||
public static final String DISPLAY_NAME="name";
|
||||
public static final String PIPELINES="pipelines";
|
||||
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Exported(name = NAME)
|
||||
public abstract String getName();
|
||||
|
||||
|
@ -42,5 +48,6 @@ public abstract class BlueOrganization extends Resource {
|
|||
*/
|
||||
@Navigable
|
||||
public abstract BlueUser getUser();
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,10 @@ import hudson.ExtensionPoint;
|
|||
import io.jenkins.blueocean.rest.ApiRoutable;
|
||||
|
||||
/**
|
||||
* This is the head of the blue ocean API.
|
||||
* Container of BlueOcean {@link BlueOrganization}s
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
public abstract class BlueOrganizationContainer extends Container<BlueOrganization> implements ApiRoutable, ExtensionPoint {
|
||||
|
||||
|
|
|
@ -57,21 +57,15 @@ public class GenericResource<T> extends Resource {
|
|||
// TODO: this only take care of getXyz() style methods. It needs to take care of other types of url
|
||||
// path handling
|
||||
Method m = clz.getMethod("get"+ Character.toUpperCase(token.charAt(0))+token.substring(1));
|
||||
|
||||
Link subResLink = getLink().rel(token+"/");
|
||||
|
||||
final Object v = m.invoke(value);
|
||||
if(v instanceof List){
|
||||
return Containers.from(subResLink,(List)v);
|
||||
}else if(v instanceof Map){
|
||||
return Containers.from(subResLink,(Map)v);
|
||||
}else if(v instanceof String){
|
||||
return new PrimitiveTypeResource(subResLink,v);
|
||||
return getResource(token, m);
|
||||
} catch (NoSuchMethodException e) {
|
||||
for(Method m:clz.getMethods()){
|
||||
Exported exported = m.getAnnotation(Exported.class);
|
||||
if(exported != null && exported.name().equals(token)){
|
||||
return getResource(token, m);
|
||||
}
|
||||
}
|
||||
return new GenericResource<>(v);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
throw new ServiceException.NotFoundException("Path "+token+" is not found");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +74,25 @@ public class GenericResource<T> extends Resource {
|
|||
return (self !=null) ? self.getLink() : new Link(Stapler.getCurrentRequest().getPathInfo());
|
||||
}
|
||||
|
||||
private Object getResource(String token, Method m){
|
||||
final Object v;
|
||||
try {
|
||||
v = m.invoke(value);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
throw new ServiceException.NotFoundException("Path "+token+" is not found");
|
||||
}
|
||||
Link subResLink = getLink().rel(token+"/");
|
||||
if(v instanceof List){
|
||||
return Containers.from(subResLink,(List)v);
|
||||
}else if(v instanceof Map){
|
||||
return Containers.from(subResLink,(Map)v);
|
||||
}else if(v instanceof String){
|
||||
return new PrimitiveTypeResource(subResLink,v);
|
||||
}
|
||||
return new GenericResource<>(v);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resource that exposes primitive type value as JSON bean
|
||||
|
|
Loading…
Reference in New Issue