diff --git a/jenkins-client-it-docker/plugins.txt b/jenkins-client-it-docker/plugins.txt index e802c9d0..6c1b1f79 100644 --- a/jenkins-client-it-docker/plugins.txt +++ b/jenkins-client-it-docker/plugins.txt @@ -8,3 +8,4 @@ job-dsl:1.41 config-file-provider:2.10.0 testng-plugin:1.10 cloudbees-folder: 5.12 +xcode-plugin: 2.0.0 \ No newline at end of file diff --git a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java index 5e524f2d..d215309c 100644 --- a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java +++ b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedManageCredentialsIT.java @@ -72,7 +72,7 @@ private void runTest(JenkinsServer jenkinsServer) throws IOException { credentialOperations(jenkinsServer, sshCredential); - //test credential + //test certificate credential CertificateCredential certificateCredential = new CertificateCredential(); certificateCredential.setId("certficateTest-" + RandomStringUtils.randomAlphanumeric(24)); certificateCredential.setCertificateSourceType(CertificateCredential.CERTIFICATE_SOURCE_TYPES.FILE_ON_MASTER); @@ -81,6 +81,13 @@ private void runTest(JenkinsServer jenkinsServer) throws IOException { credentialOperations(jenkinsServer, certificateCredential); + //test AppleDeveloperProfileCredential + AppleDeveloperProfileCredential appleDevProfile = new AppleDeveloperProfileCredential(); + appleDevProfile.setId("appleProfileTest-" + RandomStringUtils.randomAlphanumeric(24)); + appleDevProfile.setPassword(testPassword); + appleDevProfile.setDeveloperProfileContent("testprofile".getBytes()); + + credentialOperations(jenkinsServer, appleDevProfile); } private void credentialOperations(JenkinsServer jenkinsServer, Credential credential) throws IOException { diff --git a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java index bf39e06e..650e7d7d 100644 --- a/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java +++ b/jenkins-client-it-docker/src/test/java/com/offbytwo/jenkins/integration/NoExecutorStartedPluginManagerIT.java @@ -33,12 +33,12 @@ public void getPluginsShouldReturn9ForJenkins20() { } @Test - public void getPluginsShouldReturn27ForJenkins1651() { + public void getPluginsShouldReturn28ForJenkins1651() { JenkinsVersion jv = jenkinsServer.getVersion(); if (jv.isLessThan("1.651") && jv.isGreaterThan("1.651.3")) { throw new SkipException("Not Version 1.651 (" + jv.toString() + ")"); } - assertThat(pluginManager.getPlugins()).hasSize(27); + assertThat(pluginManager.getPlugins()).hasSize(28); } private Plugin createPlugin(String shortName, String version) { @@ -101,7 +101,7 @@ public void getPluginsShouldReturnTheListOfInstalledPluginsFor1651() { // instead of maintaining at two locations. //@formatter:off Plugin[] expectedPlugins = { - createPlugin("token-macro", "1.12.1"), + createPlugin("token-macro", "1.12.1"), createPlugin("translation", "1.10"), createPlugin("testng-plugin", "1.10"), createPlugin("matrix-project", "1.4.1"), @@ -127,7 +127,8 @@ public void getPluginsShouldReturnTheListOfInstalledPluginsFor1651() { createPlugin("throttle-concurrents", "1.9.0"), createPlugin("subversion", "1.54"), createPlugin("ssh-slaves", "1.9"), - createPlugin("cloudbees-folder", "5.12"), + createPlugin("cloudbees-folder", "5.12"), + createPlugin("xcode-plugin", "2.0.0"), }; //@formatter:on List plugins = pluginManager.getPlugins(); diff --git a/jenkins-client/pom.xml b/jenkins-client/pom.xml index baf14987..6050f92d 100644 --- a/jenkins-client/pom.xml +++ b/jenkins-client/pom.xml @@ -71,6 +71,11 @@ httpclient + + org.apache.httpcomponents + httpmime + + jaxen jaxen diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java index 6693e00a..c2eaf3b1 100755 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java @@ -30,6 +30,7 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; @@ -41,6 +42,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -397,6 +399,67 @@ public void post_form_json(String path, Map data, boolean crumbF } } + /** + * Perform a POST request using multipart-form. + * + * This method was added for the purposes of creating some types of credentials, but may be + * useful for other API calls as well. + * + * Unlike post and post_xml, the path is *not* modified by adding + * "/api/json". Additionally, the params in data are provided as both + * request parameters including a json parameter, *and* in the + * JSON-formatted StringEntity, because this is what the folder creation + * call required. It is unclear if any other jenkins APIs operate in this + * fashion. + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param crumbFlag true / false. + * @throws IOException in case of an error. + */ + public void post_multipart_form_json(String path, Map data, boolean crumbFlag) throws IOException { + HttpPost request; + if (data != null) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + for (Map.Entry entry : data.entrySet()) { + String fieldName = entry.getKey(); + Object fieldValue = entry.getValue(); + if (fieldValue instanceof String) { + builder.addTextBody(fieldName, (String) fieldValue); + } else if (fieldValue instanceof byte[]) { + builder.addBinaryBody(fieldName, (byte[]) fieldValue); + } else if (fieldValue instanceof File) { + builder.addBinaryBody(fieldName, (File) fieldValue); + } else if (fieldValue instanceof InputStream) { + builder.addBinaryBody(fieldName, (InputStream) fieldValue); + } else { + throw new IllegalArgumentException("type of field " + fieldName + " is not String, byte[], File or InputStream"); + } + } + request = new HttpPost(noapi(path)); + request.setEntity(builder.build()); + } else { + request = new HttpPost(noapi(path)); + } + + if (crumbFlag == true) { + Crumb crumb = get("/crumbIssuer", Crumb.class); + if (crumb != null) { + request.addHeader(new BasicHeader(crumb.getCrumbRequestField(), crumb.getCrumb())); + } + } + + HttpResponse response = client.execute(request, localContext); + getJenkinsVersionFromHeader(response); + + try { + httpResponseValidator.validateResponse(response); + } finally { + EntityUtils.consume(response.getEntity()); + releaseConnection(request); + } + } + /** * Perform a POST request of XML (instead of using json mapper) and return a diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java new file mode 100644 index 00000000..8b84ab8a --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/AppleDeveloperProfileCredential.java @@ -0,0 +1,107 @@ +package com.offbytwo.jenkins.model.credentials; + +import net.sf.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Apple developer profile credential type. + * + * NOTE: this type is only available on Jenkins after the xcode plugin (https://wiki.jenkins.io/display/JENKINS/Xcode+Plugin) is installed. + */ +public class AppleDeveloperProfileCredential extends Credential { + public static final String TYPENAME = "Apple Developer Profile"; + + private static final String BASECLASS = "au.com.rayh.DeveloperProfile"; + private static final String FILE_ZERO_FIELD_NAME = "file0"; + private static final String FILE_ONE_FIELD_NAME = "file1"; + + private String password; + private byte[] developerProfileContent; + + public String getPassword() { + return password; + } + + /** + * Set the password of the developer profile + * @param password + */ + public void setPassword(String password) { + this.password = password; + } + + public byte[] getDeveloperProfileContent() { + return developerProfileContent; + } + + /** + * Set the content of the developer profile. A developer profile file is a zip with the following structure: + * + * developerprofile/ + * - account.keychain (can be empty. Required for validation. The plugin will create a new keychain before build) + * - identities + * |- .p12 (A exported P12 file. Should contain both certificate and private key) + * - profiles + * |- .mobileprovision (A mobile provisioning profile) + * @param developerProfileContent + */ + public void setDeveloperProfileContent(byte[] developerProfileContent) { + this.developerProfileContent = developerProfileContent; + } + + @Override + public boolean useMultipartForm() { + return true; + } + + @Override + public Map dataForCreate() { + Map credentialMap = new HashMap(); + credentialMap.put("image", FILE_ZERO_FIELD_NAME); + credentialMap.put("password", this.getPassword()); + credentialMap.put("id", this.getId()); + credentialMap.put("description", this.getDescription()); + credentialMap.put("stapler-class", BASECLASS); + credentialMap.put("$class", BASECLASS); + + + Map jsonData = new HashMap<>(); + jsonData.put("", "1"); + jsonData.put("credentials", credentialMap); + + Map formFields = new HashMap(); + formFields.put(FILE_ZERO_FIELD_NAME, this.getDeveloperProfileContent()); + formFields.put("_.scope", SCOPE_GLOBAL); + formFields.put("_.password", this.getPassword()); + formFields.put("_.id", this.getId()); + formFields.put("_.description", this.getDescription()); + formFields.put("stapler-class", BASECLASS); + formFields.put("$class", BASECLASS); + formFields.put("json", JSONObject.fromObject(jsonData).toString()); + return formFields; + } + + @Override + public Map dataForUpdate() { + Map credentialMap = new HashMap(); + credentialMap.put("image", FILE_ONE_FIELD_NAME); + credentialMap.put("password", this.getPassword()); + credentialMap.put("id", this.getId()); + credentialMap.put("description", this.getDescription()); + credentialMap.put("stapler-class", BASECLASS); + credentialMap.put("", true); + + + Map formFields = new HashMap(); + formFields.put(FILE_ONE_FIELD_NAME, this.getDeveloperProfileContent()); + formFields.put("_.", "on"); + formFields.put("_.password", this.getPassword()); + formFields.put("_.id", this.getId()); + formFields.put("_.description", this.getDescription()); + formFields.put("stapler-class", BASECLASS); + formFields.put("json", JSONObject.fromObject(credentialMap).toString()); + return formFields; + } +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java index 790e5279..e0a39268 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/Credential.java @@ -11,7 +11,8 @@ @JsonSubTypes({@JsonSubTypes.Type(value = UsernamePasswordCredential.class, name = UsernamePasswordCredential.TYPENAME), @JsonSubTypes.Type(value = SSHKeyCredential.class, name = SSHKeyCredential.TYPENAME), @JsonSubTypes.Type(value = SecretTextCredential.class, name = SecretTextCredential.TYPENAME), - @JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME)}) + @JsonSubTypes.Type(value = CertificateCredential.class, name = CertificateCredential.TYPENAME), + @JsonSubTypes.Type(value = AppleDeveloperProfileCredential.class, name = AppleDeveloperProfileCredential.TYPENAME)}) /** * Base class for credentials. Should not be instantiated directly. */ @@ -99,4 +100,12 @@ public void setDisplayName(String displayName) { public abstract Map dataForCreate(); public abstract Map dataForUpdate(); + + /** + * Indicate if the request should be sent as multipart/form data + * @return + */ + public boolean useMultipartForm() { + return false; + } } diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java index 02ce3feb..501d6747 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/credentials/CredentialManager.java @@ -67,7 +67,11 @@ public Map listCredentials() throws IOException { */ public void createCredential(Credential credential, Boolean crumbFlag) throws IOException { String url = String.format("%s/%s?", this.baseUrl, "createCredentials"); - this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag); + if (credential.useMultipartForm()) { + this.jenkinsClient.post_multipart_form_json(url, credential.dataForCreate(), crumbFlag); + } else { + this.jenkinsClient.post_form_json(url, credential.dataForCreate(), crumbFlag); + } } /** @@ -80,7 +84,11 @@ public void createCredential(Credential credential, Boolean crumbFlag) throws IO public void updateCredential(String credentialId, Credential credential, Boolean crumbFlag) throws IOException { credential.setId(credentialId); String url = String.format("%s/%s/%s/%s?", this.baseUrl, "credential", credentialId, "updateSubmit"); - this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag); + if (credential.useMultipartForm()) { + this.jenkinsClient.post_multipart_form_json(url, credential.dataForUpdate(), crumbFlag); + } else { + this.jenkinsClient.post_form_json(url, credential.dataForUpdate(), crumbFlag); + } } /** diff --git a/pom.xml b/pom.xml index cb1291fb..bb77ee5a 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ 17.0 2.4 4.3.6 + 4.3.6 2.3.4 @@ -148,6 +149,12 @@ ${httpclient.version} + + org.apache.httpcomponents + httpmime + ${httpmime.version} + + jaxen jaxen