From 5fa1952a2e582f2c428584c5ccc1800132559df0 Mon Sep 17 00:00:00 2001 From: Igor Pashev Date: Tue, 5 Dec 2017 11:37:56 +0300 Subject: Version 0.1.0 --- ...tAdditionalParameterEnvironmentContributor.java | 39 +++ .../plugins/bbprb/BitbucketBuildListener.java | 69 +++++ .../plugins/bbprb/BitbucketBuildTrigger.java | 303 +++++++++++++++++++++ .../jenkinsci/plugins/bbprb/BitbucketCause.java | 80 ++++++ .../plugins/bbprb/BitbucketHookReceiver.java | 140 ++++++++++ .../plugins/bbprb/bitbucket/ApiClient.java | 246 +++++++++++++++++ .../plugins/bbprb/bitbucket/BuildState.java | 9 + 7 files changed, 886 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java (limited to 'src/main/java/org/jenkinsci/plugins') diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java new file mode 100644 index 0000000..2e9d8bc --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java @@ -0,0 +1,39 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.*; + +import java.io.IOException; + +@Extension +public class BitbucketAdditionalParameterEnvironmentContributor + extends EnvironmentContributor { + @Override + public void buildEnvironmentFor(Run run, EnvVars envVars, + TaskListener taskListener) + throws IOException, InterruptedException { + + BitbucketCause cause = (BitbucketCause)run.getCause(BitbucketCause.class); + if (cause == null) { + return; + } + + putEnvVar(envVars, "destinationRepository", + cause.getDestinationRepository()); + putEnvVar(envVars, "pullRequestAuthor", cause.getPullRequestAuthor()); + putEnvVar(envVars, "pullRequestId", cause.getPullRequestId()); + putEnvVar(envVars, "pullRequestTitle", cause.getPullRequestTitle()); + putEnvVar(envVars, "sourceBranch", cause.getSourceBranch()); + putEnvVar(envVars, "sourceRepository", cause.getSourceRepository()); + putEnvVar(envVars, "targetBranch", cause.getTargetBranch()); + } + + private static void putEnvVar(EnvVars envs, String name, String value) { + envs.put(name, getString(value, "")); + } + + private static String getString(String actual, String d) { + return actual == null ? d : actual; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java new file mode 100644 index 0000000..efcc13a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java @@ -0,0 +1,69 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.Extension; +import hudson.model.AbstractBuild; +import hudson.model.Job; +import hudson.model.TaskListener; +import hudson.model.listeners.RunListener; +import hudson.triggers.Trigger; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import hudson.model.Result; + +import org.jenkinsci.plugins.bbprb.bitbucket.BuildState; + +@Extension +public class BitbucketBuildListener extends RunListener> { + + @Override + public void onStarted(AbstractBuild build, TaskListener listener) { + BitbucketCause cause = build.getCause(BitbucketCause.class); + if (cause == null) { + return; + } + + BitbucketBuildTrigger trigger = extractTrigger(build); + if (trigger == null) { + return; + } + + LOGGER.log(Level.FINE, "Started by BitbucketBuildTrigger"); + trigger.setPRState(cause, BuildState.INPROGRESS, build.getUrl()); + try { + build.setDescription( + build.getCause(BitbucketCause.class).getShortDescription()); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not set build description: {0}", + e.getMessage()); + } + } + + @Override + public void onCompleted(AbstractBuild build, TaskListener listener) { + BitbucketBuildTrigger trigger = extractTrigger(build); + if (trigger != null) { + LOGGER.log(Level.FINE, "Completed after BitbucketBuildTrigger"); + Result result = build.getResult(); + BuildState state = (result == Result.SUCCESS) ? BuildState.SUCCESSFUL + : BuildState.FAILED; + BitbucketCause cause = build.getCause(BitbucketCause.class); + trigger.setPRState(cause, state, build.getUrl()); + } + } + + private static BitbucketBuildTrigger + extractTrigger(AbstractBuild build) { + BitbucketBuildTrigger trigger = + build.getProject().getTrigger(BitbucketBuildTrigger.class); + + if ((trigger != null) && (trigger instanceof BitbucketBuildTrigger)) { + return trigger; + } else { + return null; + } + } + + private static final Logger LOGGER = + Logger.getLogger(BitbucketBuildListener.class.getName()); +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java new file mode 100644 index 0000000..b0c6ece --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java @@ -0,0 +1,303 @@ +package org.jenkinsci.plugins.bbprb; + +import antlr.ANTLRException; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import hudson.Extension; +import hudson.model.AbstractProject; +import hudson.model.Cause; +import hudson.model.Executor; +import hudson.model.Item; +import hudson.model.ParameterDefinition; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.ParameterValue; +import hudson.model.queue.QueueTaskFuture; +import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import hudson.plugins.git.RevisionParameterAction; +import hudson.security.ACL; +import hudson.triggers.Trigger; +import hudson.triggers.TriggerDescriptor; +import hudson.util.ListBoxModel; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn; +import net.sf.json.JSONObject; +import org.acegisecurity.context.SecurityContext; +import org.acegisecurity.context.SecurityContextHolder; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; + +import org.jenkinsci.plugins.bbprb.bitbucket.ApiClient; +import org.jenkinsci.plugins.bbprb.bitbucket.BuildState; + +public class BitbucketBuildTrigger extends Trigger> { + private final String ciKey; + private final String ciName; + private final String credentialsId; + private final String destinationRepository; + private final boolean cancelOutdatedJobs; + private final boolean checkDestinationCommit; + + // XXX: This is for Jelly. + // https://wiki.jenkins.io/display/JENKINS/Basic+guide+to+Jelly+usage+in+Jenkins + public String getCiKey() { + return this.ciKey; + } + public String getCiName() { + return this.ciName; + } + public String getCredentialsId() { + return this.credentialsId; + } + public String getDestinationRepository() { + return this.destinationRepository; + } + public boolean getCancelOutdatedJobs() { + return this.cancelOutdatedJobs; + } + public boolean getCheckDestinationCommit() { + return this.checkDestinationCommit; + } + + private transient ApiClient apiClient; + + public static final BitbucketBuildTriggerDescriptor descriptor = + new BitbucketBuildTriggerDescriptor(); + + @DataBoundConstructor + public BitbucketBuildTrigger(String credentialsId, + String destinationRepository, String ciKey, + String ciName, boolean checkDestinationCommit, + boolean cancelOutdatedJobs) + throws ANTLRException { + super(); + this.apiClient = null; + this.cancelOutdatedJobs = cancelOutdatedJobs; + this.checkDestinationCommit = checkDestinationCommit; + this.ciKey = ciKey; + this.ciName = ciName; + this.credentialsId = credentialsId; + this.destinationRepository = destinationRepository; + } + + @Override + public void start(AbstractProject project, boolean newInstance) { + logger.log(Level.FINE, "Started for `{0}`", project.getFullName()); + + super.start(project, newInstance); + + if (credentialsId != null && !credentialsId.isEmpty()) { + logger.log(Level.FINE, "Looking up credentials `{0}`", + this.credentialsId); + List all = + CredentialsProvider.lookupCredentials( + UsernamePasswordCredentials.class, (Item)null, ACL.SYSTEM, + URIRequirementBuilder.fromUri("https://bitbucket.org").build()); + UsernamePasswordCredentials creds = CredentialsMatchers.firstOrNull( + all, CredentialsMatchers.withId(this.credentialsId)); + if (creds != null) { + logger.log(Level.INFO, "Creating Bitbucket API client"); + this.apiClient = new ApiClient( + creds.getUsername(), creds.getPassword().getPlainText(), + this.destinationRepository, this.ciKey, this.ciName); + } else { + logger.log(Level.SEVERE, "Credentials `{0}` not found", + this.credentialsId); + } + } else { + logger.log(Level.WARNING, "Missing Bitbucket API credentials"); + } + } + + public void setPRState(BitbucketCause cause, BuildState state, String path) { + if (this.apiClient != null) { + logger.log(Level.INFO, "Setting status of PR #{0} to {1} for {2}", + new Object[] {cause.getPullRequestId(), state, + cause.getDestinationRepository()}); + this.apiClient.setBuildStatus(cause.getSourceCommitHash(), state, + getInstance().getRootUrl() + path, null, + this.job.getFullName()); + } else { + logger.log(Level.INFO, + "Will not set Bitbucket PR build status (not configured)"); + } + } + + private void startJob(BitbucketCause cause) { + if (this.cancelOutdatedJobs) { + SecurityContext orig = ACL.impersonate(ACL.SYSTEM); + cancelPreviousJobsInQueueThatMatch(cause); + abortRunningJobsThatMatch(cause); + SecurityContextHolder.setContext(orig); + } + + setPRState(cause, BuildState.INPROGRESS, this.job.getUrl()); + + this.job.scheduleBuild2( + 0, cause, new ParametersAction(this.getDefaultParameters()), + new RevisionParameterAction(cause.getSourceCommitHash())); + } + + private void + cancelPreviousJobsInQueueThatMatch(@Nonnull BitbucketCause cause) { + logger.log(Level.FINE, "Looking for queued jobs that match PR #{0}", + cause.getPullRequestId()); + Queue queue = getInstance().getQueue(); + + for (Queue.Item item : queue.getItems()) { + if (hasCauseFromTheSamePullRequest(item.getCauses(), cause)) { + logger.fine("Canceling item in queue: " + item); + queue.cancel(item); + } + } + } + + private Jenkins getInstance() { + final Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + throw new IllegalStateException("Jenkins instance is NULL!"); + } + return instance; + } + + private void + abortRunningJobsThatMatch(@Nonnull BitbucketCause bitbucketCause) { + logger.log(Level.FINE, "Looking for running jobs that match PR #{0}", + bitbucketCause.getPullRequestId()); + for (Object o : job.getBuilds()) { + if (o instanceof Run) { + Run build = (Run)o; + if (build.isBuilding() && + hasCauseFromTheSamePullRequest(build.getCauses(), bitbucketCause)) { + logger.fine("Aborting build: " + build + " since PR is outdated"); + setBuildDescription(build); + final Executor executor = build.getExecutor(); + if (executor == null) { + throw new IllegalStateException("Executor can't be NULL"); + } + executor.interrupt(Result.ABORTED); + } + } + } + } + + private void setBuildDescription(final Run build) { + try { + build.setDescription( + "Aborting build by `Bitbucket Pullrequest Builder Plugin`: " + build + + " since PR is outdated"); + } catch (IOException e) { + logger.warning("Could not set build description: " + e.getMessage()); + } + } + + private boolean + hasCauseFromTheSamePullRequest(@Nullable List causes, + @Nullable BitbucketCause pullRequestCause) { + if (causes != null && pullRequestCause != null) { + for (Cause cause : causes) { + if (cause instanceof BitbucketCause) { + BitbucketCause sc = (BitbucketCause)cause; + if (StringUtils.equals(sc.getPullRequestId(), + pullRequestCause.getPullRequestId()) && + StringUtils.equals(sc.getSourceRepository(), + pullRequestCause.getSourceRepository())) { + return true; + } + } + } + } + return false; + } + + private ArrayList getDefaultParameters() { + Map values = new HashMap(); + ParametersDefinitionProperty definitionProperty = + this.job.getProperty(ParametersDefinitionProperty.class); + + if (definitionProperty != null) { + for (ParameterDefinition definition : + definitionProperty.getParameterDefinitions()) { + values.put(definition.getName(), definition.getDefaultParameterValue()); + } + } + return new ArrayList(values.values()); + } + + public void handlePR(JSONObject pr) { + JSONObject src = pr.getJSONObject("source"); + JSONObject dst = pr.getJSONObject("destination"); + String dstRepository = + dst.getJSONObject("repository").getString("full_name"); + BitbucketCause cause = new BitbucketCause( + src.getJSONObject("branch").getString("name"), + dst.getJSONObject("branch").getString("name"), + src.getJSONObject("repository").getString("full_name"), + pr.getString("id"), // FIXME: it is integer + dstRepository, pr.getString("title"), + src.getJSONObject("commit").getString("hash"), + dst.getJSONObject("commit").getString("hash"), + pr.getJSONObject("author").getString("username")); + if (!dstRepository.equals(this.destinationRepository)) { + logger.log(Level.FINE, + "Job `{0}`: repository `{1}` does not match `{2}`. Skipping.", + new Object[] {this.job.getFullName(), dstRepository, + this.destinationRepository}); + return; + } + startJob(cause); + } + + @Extension + @Symbol("bbprb") + public static final class BitbucketBuildTriggerDescriptor + extends TriggerDescriptor { + public BitbucketBuildTriggerDescriptor() { + load(); + } + + @Override + public boolean isApplicable(Item item) { + return item instanceof AbstractProject; + } + + @Override + public String getDisplayName() { + return "Bitbucket Pull Requests Builder"; + } + + @Override + public boolean configure(StaplerRequest req, JSONObject json) + throws FormException { + save(); + return super.configure(req, json); + } + + public ListBoxModel doFillCredentialsIdItems() { + return new StandardListBoxModel().withEmptySelection().withMatching( + instanceOf(UsernamePasswordCredentials.class), + CredentialsProvider.lookupCredentials( + StandardUsernamePasswordCredentials.class)); + } + } + private static final Logger logger = + Logger.getLogger(BitbucketBuildTrigger.class.getName()); +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java new file mode 100644 index 0000000..a4a20a6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java @@ -0,0 +1,80 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.model.Cause; + +/** + * Created by nishio + */ +public class BitbucketCause extends Cause { + private final String sourceBranch; + private final String targetBranch; + private final String sourceRepository; + private final String pullRequestId; + private final String destinationRepository; + private final String pullRequestTitle; + private final String sourceCommitHash; + private final String destinationCommitHash; + private final String pullRequestAuthor; + public static final String BITBUCKET_URL = "https://bitbucket.org/"; + + public BitbucketCause(String sourceBranch, String targetBranch, + String sourceRepository, String pullRequestId, + String destinationRepository, String pullRequestTitle, + String sourceCommitHash, String destinationCommitHash, + String pullRequestAuthor) { + this.sourceBranch = sourceBranch; + this.targetBranch = targetBranch; + this.sourceRepository = sourceRepository; + this.pullRequestId = pullRequestId; + this.destinationRepository = destinationRepository; + this.pullRequestTitle = pullRequestTitle; + this.sourceCommitHash = sourceCommitHash; + this.destinationCommitHash = destinationCommitHash; + this.pullRequestAuthor = pullRequestAuthor; + } + + public String getSourceBranch() { + return sourceBranch; + } + public String getTargetBranch() { + return targetBranch; + } + + public String getSourceRepository() { + return sourceRepository; + } + + public String getPullRequestId() { + return pullRequestId; + } + + public String getDestinationRepository() { + return destinationRepository; + } + + public String getPullRequestTitle() { + return pullRequestTitle; + } + + public String getSourceCommitHash() { + return sourceCommitHash; + } + + public String getDestinationCommitHash() { + return destinationCommitHash; + } + + @Override + public String getShortDescription() { + String description = + "#" + this.getPullRequestId() + " " + + this.getPullRequestTitle() + ""; + return description; + } + + public String getPullRequestAuthor() { + return this.pullRequestAuthor; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java new file mode 100644 index 0000000..18b2688 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java @@ -0,0 +1,140 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.Extension; +import hudson.model.UnprotectedRootAction; +import hudson.security.ACL; +import hudson.triggers.Trigger; +import hudson.triggers.TriggerDescriptor; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn.ParameterizedJob; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import org.acegisecurity.context.SecurityContext; +import org.acegisecurity.context.SecurityContextHolder; +import org.apache.commons.io.IOUtils; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +@Extension +public class BitbucketHookReceiver implements UnprotectedRootAction { + + private static final String BITBUCKET_HOOK_URL = "bbprb-hook"; + private static final String BITBUCKET_UA = "Bitbucket-Webhooks/2.0"; + + public void doIndex(StaplerRequest req, StaplerResponse resp) + throws IOException { + + String userAgent = req.getHeader("user-agent"); + if (!BITBUCKET_UA.equals(userAgent)) { + LOGGER.log(Level.WARNING, "Bad user agent: `{0}`, expected `{1}`", + new Object[] {userAgent, BITBUCKET_UA}); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + + String uri = req.getRequestURI(); + if (!uri.contains("/" + BITBUCKET_HOOK_URL + "/")) { + LOGGER.log(Level.WARNING, + "BitBucket hook URI does not contain `/{0}/`: `{1}`", + new Object[] {BITBUCKET_HOOK_URL, uri}); + resp.setStatus(StaplerResponse.SC_NOT_FOUND); + return; + } + + String event = req.getHeader("x-event-key"); + if (event == null) { + LOGGER.log(Level.WARNING, "Missing the `x-event-key` header"); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + + String body = IOUtils.toString(req.getInputStream()); + if (body.isEmpty()) { + LOGGER.log(Level.WARNING, "Received empty request body"); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + + String contentType = req.getContentType(); + if (contentType != null && + contentType.startsWith("application/x-www-form-urlencoded")) { + body = URLDecoder.decode(body, "UTF-8"); + } + if (body.startsWith("payload=")) { + body = body.substring(8); + } + + LOGGER.log(Level.FINE, + "Received commit hook notification, key: `{0}`, body: `{1}`", + new Object[] {event, body}); + + try { + JSONObject payload = JSONObject.fromObject(body); + if (event.startsWith("pullrequest:")) { + JSONObject pr = payload.getJSONObject("pullrequest"); + String state = pr.getString("state"); + if (!"OPEN".equals(state)) { + LOGGER.log( + Level.INFO, "Ignoring closed PR ({0}): #{1} {2}", + new Object[] {state, pr.getInt("id"), pr.getString("title")}); + return; + } + for (BitbucketBuildTrigger trigger : getBitbucketTriggers()) { + trigger.handlePR(pr); + } + return; + } + } catch (JSONException e) { + LOGGER.log(Level.WARNING, e.getMessage()); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + } + + private static List getBitbucketTriggers() { + List bbtriggers = new ArrayList<>(); + + SecurityContext orig = ACL.impersonate(ACL.SYSTEM); + List jobs = + Jenkins.getInstance().getAllItems(ParameterizedJob.class); + SecurityContextHolder.setContext(orig); + + for (ParameterizedJob job : jobs) { + String jobName = job.getFullName(); + LOGGER.log(Level.FINER, "Found job: `{0}`", jobName); + + Map> triggers = job.getTriggers(); + + for (Trigger trigger : triggers.values()) { + if (trigger instanceof BitbucketBuildTrigger) { + LOGGER.log(Level.FINE, "Will consider job: `{0}`", jobName); + bbtriggers.add((BitbucketBuildTrigger)trigger); + } + } + } + + return bbtriggers; + } + + private static final Logger LOGGER = + Logger.getLogger(BitbucketHookReceiver.class.getName()); + + public String getIconFileName() { + return null; + } + + public String getDisplayName() { + return null; + } + + public String getUrlName() { + return BITBUCKET_HOOK_URL; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java new file mode 100644 index 0000000..80ae3aa --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java @@ -0,0 +1,246 @@ +package org.jenkinsci.plugins.bbprb.bitbucket; + +import org.apache.commons.httpclient.*; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; +import org.codehaus.jackson.type.TypeReference; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jenkins.model.Jenkins; +import hudson.ProxyConfiguration; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.util.EncodingUtil; + +/** + * Created by nishio + */ +public class ApiClient { + private static final Logger logger = + Logger.getLogger(ApiClient.class.getName()); + private static final String V1_API_BASE_URL = + "https://bitbucket.org/api/1.0/repositories/"; + private static final String V2_API_BASE_URL = + "https://bitbucket.org/api/2.0/repositories/"; + private static final String COMPUTED_KEY_FORMAT = "%s-%s"; + private String repository; + private Credentials credentials; + private String key; + private String name; + private HttpClientFactory factory; + + public static final byte MAX_KEY_SIZE_BB_API = 40; + + public static class HttpClientFactory { + public static final HttpClientFactory INSTANCE = new HttpClientFactory(); + private static final int DEFAULT_TIMEOUT = 60000; + + public HttpClient getInstanceHttpClient() { + HttpClient client = new HttpClient(); + + HttpClientParams params = client.getParams(); + params.setConnectionManagerTimeout(DEFAULT_TIMEOUT); + params.setSoTimeout(DEFAULT_TIMEOUT); + + if (Jenkins.getInstance() == null) + return client; + + ProxyConfiguration proxy = getInstance().proxy; + if (proxy == null) + return client; + + logger.log(Level.FINE, "Jenkins proxy: {0}:{1}", + new Object[] {proxy.name, proxy.port}); + client.getHostConfiguration().setProxy(proxy.name, proxy.port); + String username = proxy.getUserName(); + String password = proxy.getPassword(); + + // Consider it to be passed if username specified. Sufficient? + if (username != null && !"".equals(username.trim())) { + logger.log(Level.FINE, "Using proxy authentication (user={0})", + username); + client.getState().setProxyCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + } + + return client; + } + + private Jenkins getInstance() { + final Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + throw new IllegalStateException("Jenkins instance is NULL!"); + } + return instance; + } + } + + public ApiClient(String username, + String password, + String repository, String key, + String name) { + this.credentials = new UsernamePasswordCredentials(username, password); + this.repository = repository; + this.key = key; + this.name = name; + this.factory = HttpClientFactory.INSTANCE; + } + + public String getName() { + return this.name; + } + + private static MessageDigest SHA1 = null; + + /** + * Retrun + * @param keyExPart + * @return key parameter for call BitBucket API + */ + private String computeAPIKey(String keyExPart) { + String computedKey = + String.format(COMPUTED_KEY_FORMAT, this.key, keyExPart); + + if (computedKey.length() > MAX_KEY_SIZE_BB_API) { + try { + if (SHA1 == null) + SHA1 = MessageDigest.getInstance("SHA1"); + return new String( + Hex.encodeHex(SHA1.digest(computedKey.getBytes("UTF-8")))); + } catch (NoSuchAlgorithmException e) { + logger.log(Level.WARNING, "Failed to create hash provider", e); + } catch (UnsupportedEncodingException e) { + logger.log(Level.WARNING, "Failed to create hash provider", e); + } + } + return (computedKey.length() <= MAX_KEY_SIZE_BB_API) + ? computedKey + : computedKey.substring(0, MAX_KEY_SIZE_BB_API); + } + + public String buildStatusKey(String bsKey) { + return this.computeAPIKey(bsKey); + } + + public boolean hasBuildStatus(String revision, String keyEx) { + String url = v2("/commit/" + revision + "/statuses/build/" + + this.computeAPIKey(keyEx)); + String reqBody = get(url); + return reqBody != null && reqBody.contains("\"state\""); + } + + public void setBuildStatus(String revision, BuildState state, String buildUrl, + String comment, String keyEx) { + String url = v2("/commit/" + revision + "/statuses/build"); + String computedKey = this.computeAPIKey(keyEx); + NameValuePair[] data = new NameValuePair[] { + new NameValuePair("description", comment), + new NameValuePair("key", computedKey), + new NameValuePair("name", this.name), + new NameValuePair("state", state.toString()), + new NameValuePair("url", buildUrl), + }; + logger.log(Level.FINE, + "POST state {0} to {1} with key {2} with response {3}", + new Object[] {state, url, computedKey, post(url, data)}); + } + + public void deletePullRequestApproval(String pullRequestId) { + delete(v2("/pullrequests/" + pullRequestId + "/approve")); + } + + public void deletePullRequestComment(String pullRequestId, String commentId) { + delete(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId)); + } + + public void updatePullRequestComment(String pullRequestId, String content, + String commentId) { + NameValuePair[] data = new NameValuePair[] { + new NameValuePair("content", content), + }; + put(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId), data); + } + + private HttpClient getHttpClient() { + return this.factory.getInstanceHttpClient(); + } + + private String v1(String path) { + return V1_API_BASE_URL + this.repository + path; + } + + private String v2(String path) { + return V2_API_BASE_URL + this.repository + path; + } + + private String get(String path) { + return send(new GetMethod(path)); + } + + private String post(String path, NameValuePair[] data) { + PostMethod req = new PostMethod(path); + req.setRequestBody(data); + req.getParams().setContentCharset("utf-8"); + return send(req); + } + + private void delete(String path) { + send(new DeleteMethod(path)); + } + + private void put(String path, NameValuePair[] data) { + PutMethod req = new PutMethod(path); + req.setRequestBody(EncodingUtil.formUrlEncode(data, "utf-8")); + req.getParams().setContentCharset("utf-8"); + send(req); + } + + private String send(HttpMethodBase req) { + HttpClient client = getHttpClient(); + client.getState().setCredentials(AuthScope.ANY, credentials); + client.getParams().setAuthenticationPreemptive(true); + try { + int statusCode = client.executeMethod(req); + if (statusCode != HttpStatus.SC_OK) { + logger.log(Level.WARNING, "Response status: " + req.getStatusLine() + + " URI: " + req.getURI()); + } else { + return req.getResponseBodyAsString(); + } + } catch (HttpException e) { + logger.log(Level.WARNING, "Failed to send request.", e); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to send request.", e); + } finally { + req.releaseConnection(); + } + return null; + } + + private R parse(String response, Class cls) throws IOException { + return new ObjectMapper().readValue(response, cls); + } + private R parse(String response, JavaType type) throws IOException { + return new ObjectMapper().readValue(response, type); + } + private R parse(String response, TypeReference ref) + throws IOException { + return new ObjectMapper().readValue(response, ref); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java new file mode 100644 index 0000000..8fe1cdd --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java @@ -0,0 +1,9 @@ +package org.jenkinsci.plugins.bbprb.bitbucket; + +/** + * Valid build states for a pull request + * + * @see + * "https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html" + */ +public enum BuildState { FAILED, INPROGRESS, SUCCESSFUL } -- cgit v1.2.3