Skip to content

Commit 1c5d82b

Browse files
author
Wei Li
committed
✨ Fixed #JENKINS-44586. add support for managing credentials
1 parent cec3f47 commit 1c5d82b

File tree

10 files changed

+1044
-38
lines changed

10 files changed

+1044
-38
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.offbytwo.jenkins.integration;
2+
3+
import com.offbytwo.jenkins.JenkinsServer;
4+
import com.offbytwo.jenkins.model.Plugin;
5+
import com.offbytwo.jenkins.model.credentials.*;
6+
import org.apache.commons.lang.RandomStringUtils;
7+
import org.testng.SkipException;
8+
import org.testng.annotations.Test;
9+
10+
import java.io.IOException;
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import static org.testng.Assert.*;
15+
16+
@Test(groups = { Groups.NO_EXECUTOR_GROUP} )
17+
public class NoExecutorStartedManageCredentialsIT extends AbstractJenkinsIntegrationCase {
18+
19+
@Test
20+
public void credentialCRUDL() throws IOException {
21+
List<Plugin> plugins = jenkinsServer.getPluginManager().getPlugins();
22+
Plugin credentialPlugin = jenkinsServer.findPluginWithName("credentials");
23+
if (credentialPlugin == null) {
24+
throw new SkipException("No credentials plugin found. Skip Test");
25+
}
26+
27+
String pluginVersion = credentialPlugin.getVersion();
28+
if (pluginVersion.startsWith("1.")) {
29+
runTest(jenkinsServer);
30+
31+
//test CertificateCredential with upload cert file. The 2.x version may throw exceptions.
32+
CertificateCredential certificateCredential = new CertificateCredential();
33+
certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24));
34+
certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.UPLOAD_CERT_FILE);
35+
certificateCredential.setCertificateContent("testcert".getBytes());
36+
certificateCredential.setPassword("testpasssword");
37+
38+
credentialOperations(jenkinsServer, certificateCredential);
39+
40+
} else {
41+
runTest(jenkinsServer);
42+
43+
//test SecretTextCredential, this is v2 only
44+
SecretTextCredential secretText = new SecretTextCredential();
45+
secretText.setId("secrettextcredentialTest-" + RandomStringUtils.randomAlphanumeric(24));
46+
secretText.setSecret("testsecrettext");
47+
48+
credentialOperations(jenkinsServer, secretText);
49+
}
50+
}
51+
52+
private void runTest(JenkinsServer jenkinsServer) throws IOException {
53+
String testUsername = "testusername";
54+
String testPassword = "testpassword";
55+
String credentialDescription = "testDescription";
56+
//test UsernamePasswordCredential
57+
UsernamePasswordCredential testUPCredential = new UsernamePasswordCredential();
58+
testUPCredential.setId("usernamepasswordcredentialTest-" + RandomStringUtils.randomAlphanumeric(24));
59+
testUPCredential.setUsername(testUsername);
60+
testUPCredential.setPassword(testPassword);
61+
testUPCredential.setDescription(credentialDescription);
62+
63+
credentialOperations(jenkinsServer, testUPCredential);
64+
65+
//test SSHKeyCredential
66+
SSHKeyCredential sshCredential = new SSHKeyCredential();
67+
sshCredential.setId("sshusercredentialTest-" + RandomStringUtils.randomAlphanumeric(24));
68+
sshCredential.setUsername(testUsername);
69+
sshCredential.setPassphrase(testPassword);
70+
sshCredential.setPrivateKeyType(SSHKeyCredential.PRIVATE_KEY_TYPES.DIRECT_ENTRY);
71+
sshCredential.setPrivateKeyValue("testPrivateKeyContent");
72+
73+
credentialOperations(jenkinsServer, sshCredential);
74+
75+
//test credential
76+
CertificateCredential certificateCredential = new CertificateCredential();
77+
certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24));
78+
certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER);
79+
certificateCredential.setCertificatePath("/tmp/test");
80+
certificateCredential.setPassword("testpasssword");
81+
82+
credentialOperations(jenkinsServer, certificateCredential);
83+
84+
}
85+
86+
private void credentialOperations(JenkinsServer jenkinsServer, Credential credential) throws IOException {
87+
//create the credential
88+
String credentialId = credential.getId();
89+
jenkinsServer.createCredential(credential, false);
90+
91+
//check if has been created by listing
92+
Map<String, Credential> credentials = jenkinsServer.listCredentials();
93+
Credential found = credentials.get(credentialId);
94+
assertNotNull(found);
95+
assertEquals(credential.getTypeName(), found.getTypeName());
96+
97+
//compare fields
98+
assertEquals(credentialId, found.getId());
99+
assertNotNull(found.getDisplayName());
100+
101+
//update the credential
102+
String updateDescription = "updatedDescription";
103+
credential.setDescription(updateDescription);
104+
jenkinsServer.updateCredential(credentialId, credential, false);
105+
106+
//verify it is updated
107+
credentials = jenkinsServer.listCredentials();
108+
found = credentials.get(credentialId);
109+
assertEquals(updateDescription, found.getDescription());
110+
111+
//delete the credential
112+
jenkinsServer.deleteCredential(credentialId, false);
113+
credentials = jenkinsServer.listCredentials();
114+
assertFalse(credentials.containsKey(credentialId));
115+
}
116+
}

jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java

Lines changed: 109 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,32 @@
66

77
package com.offbytwo.jenkins;
88

9-
import java.io.IOException;
10-
import java.net.URI;
11-
import java.util.Arrays;
12-
import java.util.List;
13-
import java.util.Map;
14-
15-
import javax.xml.bind.JAXBException;
16-
17-
import org.apache.http.HttpStatus;
18-
import org.apache.http.client.HttpResponseException;
19-
import org.apache.http.entity.ContentType;
20-
import org.dom4j.DocumentException;
21-
import org.slf4j.Logger;
22-
import org.slf4j.LoggerFactory;
23-
249
import com.google.common.base.Function;
2510
import com.google.common.base.Optional;
2611
import com.google.common.collect.ImmutableMap;
2712
import com.google.common.collect.Maps;
2813
import com.offbytwo.jenkins.client.JenkinsHttpClient;
2914
import com.offbytwo.jenkins.client.util.EncodingUtils;
3015
import com.offbytwo.jenkins.helper.JenkinsVersion;
31-
import com.offbytwo.jenkins.model.Build;
32-
import com.offbytwo.jenkins.model.Computer;
33-
import com.offbytwo.jenkins.model.ComputerSet;
34-
import com.offbytwo.jenkins.model.FolderJob;
35-
import com.offbytwo.jenkins.model.Job;
36-
import com.offbytwo.jenkins.model.JobConfiguration;
37-
import com.offbytwo.jenkins.model.JobWithDetails;
38-
import com.offbytwo.jenkins.model.LabelWithDetails;
39-
import com.offbytwo.jenkins.model.MainView;
40-
import com.offbytwo.jenkins.model.MavenJobWithDetails;
41-
import com.offbytwo.jenkins.model.PluginManager;
42-
import com.offbytwo.jenkins.model.Queue;
43-
import com.offbytwo.jenkins.model.QueueItem;
44-
import com.offbytwo.jenkins.model.QueueReference;
45-
import com.offbytwo.jenkins.model.View;
16+
import com.offbytwo.jenkins.model.*;
17+
import com.offbytwo.jenkins.model.credentials.Credential;
18+
import com.offbytwo.jenkins.model.credentials.CredentialManager;
19+
import org.apache.commons.collections.CollectionUtils;
20+
import org.apache.commons.collections.Predicate;
21+
import org.apache.http.HttpStatus;
22+
import org.apache.http.client.HttpResponseException;
23+
import org.apache.http.entity.ContentType;
24+
import org.dom4j.DocumentException;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import javax.xml.bind.JAXBException;
29+
import java.io.IOException;
30+
import java.net.URI;
31+
import java.rmi.server.ExportException;
32+
import java.util.Arrays;
33+
import java.util.List;
34+
import java.util.Map;
4635

4736
/**
4837
* The main starting point for interacting with a Jenkins server.
@@ -52,6 +41,8 @@ public class JenkinsServer {
5241

5342
private final JenkinsHttpClient client;
5443

44+
private CredentialManager credentialManager;
45+
5546
/**
5647
* Create a new Jenkins server reference given only the server address
5748
*
@@ -934,24 +925,104 @@ private String toViewBaseUrl(FolderJob folder, String name) {
934925
* @param jobName the fullName of the job.
935926
* @return the path of the job including folders if present.
936927
*/
937-
private String parseFullName(String jobName)
938-
{
928+
private String parseFullName(String jobName) {
939929
if (!jobName.contains("/")) {
940930
return jobName;
941931
}
942-
932+
943933
List<String> foldersAndJob = Arrays.asList(jobName.split("/"));
944-
934+
945935
String foldersAndJobName = "";
946-
936+
947937
for (int i = 0; i < foldersAndJob.size(); i++) {
948938
foldersAndJobName += foldersAndJob.get(i);
949-
950-
if (i != foldersAndJob.size() -1) {
939+
940+
if (i != foldersAndJob.size() - 1) {
951941
foldersAndJobName += "/job/";
952942
}
953943
}
954-
944+
955945
return foldersAndJobName;
946+
947+
}
948+
949+
/**
950+
* List the credentials from the Jenkins server.
951+
* @return a hash map of the credentials. The key is the id of each credential.
952+
* @throws IOException
953+
*/
954+
public Map<String, Credential> listCredentials() throws IOException {
955+
return this.getCredentialManager().listCredentials();
956+
}
957+
958+
/**
959+
* Create the given credential
960+
* @param credential a credential instance
961+
* @param crumbFlag
962+
* @throws IOException
963+
*/
964+
public void createCredential(Credential credential, boolean crumbFlag) throws IOException {
965+
this.getCredentialManager().createCredential(credential, crumbFlag);
966+
}
967+
968+
/**
969+
* Update an existing credential
970+
* @param credentialId the id of the credential
971+
* @param credential the updated credential instance
972+
* @param crumbFlag
973+
* @throws IOException
974+
*/
975+
public void updateCredential(String credentialId, Credential credential, boolean crumbFlag) throws IOException {
976+
this.getCredentialManager().updateCredential(credentialId, credential, crumbFlag);
977+
}
978+
979+
/**
980+
* Delete an existing credential
981+
* @param credentialId the id of the credential to delete
982+
* @param crumbFlag
983+
* @throws IOException
984+
*/
985+
public void deleteCredential(String credentialId, boolean crumbFlag) throws IOException {
986+
this.getCredentialManager().deleteCredential(credentialId, crumbFlag);
987+
}
988+
989+
/**
990+
* Return the credentialManager instance. Will initialise it if it's never used before.
991+
* @return the credentialManager instance
992+
* @throws IOException
993+
*/
994+
private CredentialManager getCredentialManager() throws IOException {
995+
if (this.credentialManager == null) {
996+
Plugin credentialPlugin = findPluginWithName("credentials");
997+
if (credentialPlugin == null) {
998+
throw new ExportException("credential plugin is not installed");
999+
}
1000+
String version = credentialPlugin.getVersion();
1001+
this.credentialManager = new CredentialManager(version, this.client);
1002+
}
1003+
return this.credentialManager;
1004+
}
1005+
1006+
/**
1007+
* Find a plugin that matches the given short name
1008+
* @param pluginShortName the short name of the plugin to find
1009+
* @return the pluin object that is found. Can be null if no match found.
1010+
* @throws IOException
1011+
*/
1012+
public Plugin findPluginWithName(final String pluginShortName) throws IOException {
1013+
List<Plugin> plugins = this.getPluginManager().getPlugins();
1014+
Object foundPlugin = CollectionUtils.find(plugins, new Predicate() {
1015+
@Override
1016+
public boolean evaluate(Object o) {
1017+
Plugin p = (Plugin) o;
1018+
if (p.getShortName().equalsIgnoreCase(pluginShortName)) {
1019+
return true;
1020+
} else {
1021+
return false;
1022+
}
1023+
}
1024+
});
1025+
1026+
return foundPlugin == null ? null : (Plugin) foundPlugin;
9561027
}
9571028
}

jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,58 @@ public HttpResponse post_form_with_result(String path, List<NameValuePair> data,
348348
return response;
349349
}
350350

351+
/**
352+
* Perform a POST request using form url encoding.
353+
*
354+
* This method was added for the purposes of creating credentials, but may be
355+
* useful for other API calls as well.
356+
*
357+
* Unlike post and post_xml, the path is *not* modified by adding
358+
* "/api/json". Additionally, the params in data are provided as both
359+
* request parameters including a json parameter, *and* in the
360+
* JSON-formatted StringEntity, because this is what the folder creation
361+
* call required. It is unclear if any other jenkins APIs operate in this
362+
* fashion.
363+
*
364+
* @param path path to request, can be relative or absolute
365+
* @param data data to post
366+
* @param crumbFlag true / false.
367+
* @throws IOException in case of an error.
368+
*/
369+
public void post_form_json(String path, Map<String, Object> data, boolean crumbFlag) throws IOException {
370+
HttpPost request;
371+
if (data != null) {
372+
// https://gist.github.com/stuart-warren/7786892 was slightly
373+
// helpful here
374+
List<String> queryParams = Lists.newArrayList();
375+
queryParams.add("json=" + EncodingUtils.encodeParam(JSONObject.fromObject(data).toString()));
376+
String value = mapper.writeValueAsString(data);
377+
StringEntity stringEntity = new StringEntity(value, ContentType.APPLICATION_FORM_URLENCODED);
378+
request = new HttpPost(noapi(path) + StringUtils.join(queryParams, "&"));
379+
request.setEntity(stringEntity);
380+
} else {
381+
request = new HttpPost(noapi(path));
382+
}
383+
384+
if (crumbFlag == true) {
385+
Crumb crumb = get("/crumbIssuer", Crumb.class);
386+
if (crumb != null) {
387+
request.addHeader(new BasicHeader(crumb.getCrumbRequestField(), crumb.getCrumb()));
388+
}
389+
}
390+
391+
HttpResponse response = client.execute(request, localContext);
392+
getJenkinsVersionFromHeader(response);
393+
394+
try {
395+
httpResponseValidator.validateResponse(response);
396+
} finally {
397+
EntityUtils.consume(response.getEntity());
398+
releaseConnection(request);
399+
}
400+
}
401+
402+
351403
/**
352404
* Perform a POST request of XML (instead of using json mapper) and return a
353405
* string rendering of the response entity.

0 commit comments

Comments
 (0)