package com.cloudbees.jenkins;
import com.google.common.base.Function;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.RootAction;
import hudson.model.UnprotectedRootAction;
import hudson.util.SequentialExecutionQueue;
import jenkins.model.Jenkins;
import jenkins.scm.api.SCMEvent;
import org.apache.commons.lang3.Validate;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github.internal.GHPluginConfigException;
import org.jenkinsci.plugins.github.webhook.GHEventHeader;
import org.jenkinsci.plugins.github.webhook.GHEventPayload;
import org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GHEvent;
import org.kohsuke.stapler.Stapler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.net.URL;
import java.util.List;
import static hudson.model.Computer.threadPoolForRemoting;
import static org.apache.commons.lang3.Validate.notNull;
import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isInterestedIn;
import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.processEvent;
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive;
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable;
import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl;
/**
* Receives github hook.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class GitHubWebHook implements UnprotectedRootAction {
private static final Logger LOGGER = LoggerFactory.getLogger(GitHubWebHook.class);
public static final String URLNAME = "github-webhook";
// headers used for testing the endpoint configuration
public static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation";
public static final String X_INSTANCE_IDENTITY = "X-Instance-Identity";
private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting);
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return URLNAME;
}
/**
* If any wants to auto-register hook, then should call this method
* Example code:
* {@code GitHubWebHook.get().registerHookFor(job);}
*
* @param job not null project to register hook for
* @deprecated use {@link #registerHookFor(Item)}
*/
@Deprecated
public void registerHookFor(Job job) {
reRegisterHookForJob().apply(job);
}
/**
* If any wants to auto-register hook, then should call this method
* Example code:
* {@code GitHubWebHook.get().registerHookFor(item);}
*
* @param item not null item to register hook for
* @since 1.25.0
*/
public void registerHookFor(Item item) {
reRegisterHookForJob().apply(item);
}
/**
* Calls {@link #registerHookFor(Job)} for every project which have subscriber
*
* @return list of jobs which jenkins tried to register hook
*/
public List<Item> reRegisterAllHooks() {
return from(getJenkinsInstance().getAllItems(Item.class))
.filter(isBuildable())
.filter(isAlive())
.transform(reRegisterHookForJob())
.toList();
}
/**
* Receives the webhook call
*
* @param event GH event type. Never null
* @param payload Payload from hook. Never blank
*/
@SuppressWarnings("unused")
@RequirePostWithGHHookPayload
public void doIndex(@Nonnull @GHEventHeader GHEvent event, @Nonnull @GHEventPayload String payload) {
GHSubscriberEvent subscriberEvent =
new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest()), event, payload);
from(GHEventsSubscriber.all())
.filter(isInterestedIn(event))
.transform(processEvent(subscriberEvent)).toList();
}
private <T extends Item> Function<T, T> reRegisterHookForJob() {
return new Function<T, T>() {
@Override
public T apply(T job) {
LOGGER.debug("Calling registerHooks() for {}", notNull(job, "Item can't be null").getFullName());
// We should handle wrong url of self defined hook url here in any case with try-catch :(
URL hookUrl;
try {
hookUrl = GitHubPlugin.configuration().getHookUrl();
} catch (GHPluginConfigException e) {
LOGGER.error("Skip registration of GHHook ({})", e.getMessage());
return job;
}
Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job);
queue.execute(hookRegistrator);
return job;
}
};
}
public static GitHubWebHook get() {
return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class);
}
@Nonnull
public static Jenkins getJenkinsInstance() throws IllegalStateException {
Jenkins instance = Jenkins.getInstance();
Validate.validState(instance != null, "Jenkins has not been started, or was already shut down");
return instance;
}
/**
* Other plugins may be interested in listening for these updates.
*
* @since 1.8
* @deprecated working theory is that this API is not required any more with the {@link SCMEvent} based API,
* if wrong, please raise a JIRA
*/
@Deprecated
@Restricted(NoExternalUse.class)
public abstract static class Listener implements ExtensionPoint {
/**
* Called when there is a change notification on a specific repository.
*
* @param pusherName the pusher name.
* @param changedRepository the changed repository.
*
* @since 1.8
*/
public abstract void onPushRepositoryChanged(String pusherName, GitHubRepositoryName changedRepository);
}
}