Skip to content

Commit f02238e

Browse files
author
MerkushevKirill
committed
refactored cleaner and push trigger to work with hook manager
1 parent a38c6ef commit f02238e

2 files changed

Lines changed: 83 additions & 116 deletions

File tree

src/main/java/com/cloudbees/jenkins/Cleaner.java

Lines changed: 36 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,20 @@
33
import com.cloudbees.jenkins.GitHubPushTrigger.DescriptorImpl;
44
import hudson.Extension;
55
import hudson.model.AbstractProject;
6-
import hudson.model.Hudson;
76
import hudson.model.PeriodicWork;
87
import hudson.triggers.Trigger;
9-
import hudson.util.TimeUnit2;
10-
import org.kohsuke.github.GHException;
11-
import org.kohsuke.github.GHHook;
12-
import org.kohsuke.github.GHRepository;
8+
import jenkins.model.Jenkins;
9+
import org.jenkinsci.plugins.github.webhook.WebhookManager;
1310

14-
import java.io.IOException;
1511
import java.net.URL;
16-
import java.util.ArrayList;
17-
import java.util.HashSet;
1812
import java.util.List;
19-
import java.util.Set;
20-
import java.util.logging.Level;
21-
import java.util.logging.Logger;
13+
import java.util.Queue;
14+
import java.util.concurrent.ConcurrentLinkedQueue;
15+
import java.util.concurrent.TimeUnit;
16+
17+
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
18+
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.associatedNames;
19+
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger;
2220

2321
/**
2422
* Removes post-commit hooks from repositories that we no longer care.
@@ -29,70 +27,52 @@
2927
*/
3028
@Extension
3129
public class Cleaner extends PeriodicWork {
32-
private final Set<GitHubRepositoryName> couldHaveBeenRemoved = new HashSet<GitHubRepositoryName>();
30+
/**
31+
* Queue contains repo names prepared to cleanup.
32+
* After configure method on job, trigger calls {@link #onStop(AbstractProject)}
33+
* which converts to repo names with help of contributors.
34+
*
35+
* This queue is thread-safe, so any thread can write or
36+
* fetch names to this queue without additional sync
37+
*/
38+
private final Queue<GitHubRepositoryName> namesq = new ConcurrentLinkedQueue<GitHubRepositoryName>();
3339

3440
/**
3541
* Called when a {@link GitHubPushTrigger} is about to be removed.
3642
*/
37-
synchronized void onStop(AbstractProject<?,?> job) {
38-
couldHaveBeenRemoved.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job));
43+
/* package */ void onStop(AbstractProject<?, ?> job) {
44+
namesq.addAll(GitHubRepositoryNameContributor.parseAssociatedNames(job));
3945
}
4046

4147
@Override
4248
public long getRecurrencePeriod() {
43-
return TimeUnit2.MINUTES.toMillis(3);
49+
return TimeUnit.MINUTES.toMillis(3);
4450
}
4551

52+
/**
53+
* Each run this work fetches alive repo names (which has trigger for it)
54+
* then if names queue is not empty (any job was reconfigured with GH trigger change),
55+
* next name passed to {@link WebhookManager} with list of active names to check and unregister old hooks
56+
*
57+
* @throws Exception
58+
*/
4659
@Override
4760
protected void doRun() throws Exception {
48-
List<GitHubRepositoryName> names;
49-
synchronized (this) {// atomically obtain what we need to check
50-
names = new ArrayList<GitHubRepositoryName>(couldHaveBeenRemoved);
51-
couldHaveBeenRemoved.clear();
52-
}
61+
URL url = Trigger.all().get(DescriptorImpl.class).getHookUrl();
5362

54-
// subtract all the live repositories
55-
for (AbstractProject<?,?> job : Hudson.getInstance().getAllItems(AbstractProject.class)) {
56-
GitHubPushTrigger trigger = job.getTrigger(GitHubPushTrigger.class);
57-
if (trigger!=null) {
58-
names.removeAll(GitHubRepositoryNameContributor.parseAssociatedNames(job));
59-
}
60-
}
63+
List<AbstractProject> jobs = Jenkins.getInstance().getAllItems(AbstractProject.class);
64+
List<GitHubRepositoryName> alive = from(jobs)
65+
.filter(withTrigger(GitHubPushTrigger.class)) // live repos
66+
.transformAndConcat(associatedNames()).toList();
6167

62-
// these are the repos that we are no longer interested.
63-
// erase our hooks
64-
OUTER:
65-
for (GitHubRepositoryName r : names) {
66-
for (GHRepository repo : r.resolve()) {
67-
try {
68-
removeHook(repo, Trigger.all().get(DescriptorImpl.class).getHookUrl());
69-
LOGGER.fine("Removed a hook from "+r+"");
70-
continue OUTER;
71-
} catch (Throwable e) {
72-
LOGGER.log(Level.WARNING,"Failed to remove hook from "+r,e);
73-
}
74-
}
75-
}
76-
}
68+
while (!namesq.isEmpty()) {
69+
GitHubRepositoryName name = namesq.poll();
7770

78-
//Maybe we should create a remove hook method in the Github API
79-
//something like public void removeHook(String name, Map<String,String> config)
80-
private void removeHook(GHRepository repo, URL url) {
81-
try {
82-
String urlExternalForm = url.toExternalForm();
83-
for (GHHook h : repo.getHooks()) {
84-
if (h.getName().equals("jenkins") && h.getConfig().get("jenkins_hook_url").equals(urlExternalForm)) {
85-
h.delete();
86-
}
87-
}
88-
} catch (IOException e) {
89-
throw new GHException("Failed to update post-commit hooks", e);
71+
WebhookManager.forHookUrl(url).unregisterFor(name, alive);
9072
}
9173
}
9274

9375
public static Cleaner get() {
9476
return PeriodicWork.all().get(Cleaner.class);
9577
}
96-
97-
private static final Logger LOGGER = Logger.getLogger(Cleaner.class.getName());
9878
}

src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java

Lines changed: 47 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
package com.cloudbees.jenkins;
22

3+
import com.google.common.base.Charsets;
4+
import com.google.common.base.Function;
35
import hudson.Extension;
46
import hudson.Util;
57
import hudson.console.AnnotatedLargeText;
8+
import hudson.model.AbstractProject;
69
import hudson.model.Action;
7-
import hudson.model.Hudson;
8-
import hudson.model.Hudson.MasterComputer;
910
import hudson.model.Item;
10-
import hudson.model.AbstractProject;
1111
import hudson.model.Project;
1212
import hudson.triggers.Trigger;
1313
import hudson.triggers.TriggerDescriptor;
1414
import hudson.util.FormValidation;
1515
import hudson.util.SequentialExecutionQueue;
1616
import hudson.util.StreamTaskListener;
17+
import jenkins.model.Jenkins;
18+
import jenkins.model.Jenkins.MasterComputer;
19+
import net.sf.json.JSONObject;
20+
import org.apache.commons.codec.binary.Base64;
21+
import org.apache.commons.jelly.XMLOutput;
22+
import org.jenkinsci.main.modules.instance_identity.InstanceIdentity;
23+
import org.jenkinsci.plugins.github.webhook.WebhookManager;
24+
import org.kohsuke.stapler.DataBoundConstructor;
25+
import org.kohsuke.stapler.QueryParameter;
26+
import org.kohsuke.stapler.StaplerRequest;
1727

28+
import javax.inject.Inject;
1829
import java.io.File;
1930
import java.io.IOException;
2031
import java.io.PrintStream;
2132
import java.net.HttpURLConnection;
2233
import java.net.MalformedURLException;
2334
import java.net.URL;
24-
import java.nio.charset.Charset;
2535
import java.security.interfaces.RSAPublicKey;
2636
import java.text.DateFormat;
2737
import java.util.ArrayList;
@@ -33,19 +43,10 @@
3343
import java.util.logging.Level;
3444
import java.util.logging.Logger;
3545

36-
import jenkins.model.Jenkins;
37-
import net.sf.json.JSONObject;
38-
39-
import org.apache.commons.codec.binary.Base64;
40-
import org.apache.commons.jelly.XMLOutput;
41-
import org.jenkinsci.main.modules.instance_identity.InstanceIdentity;
42-
import org.kohsuke.github.GHException;
43-
import org.kohsuke.github.GHRepository;
44-
import org.kohsuke.stapler.DataBoundConstructor;
45-
import org.kohsuke.stapler.QueryParameter;
46-
import org.kohsuke.stapler.StaplerRequest;
47-
48-
import javax.inject.Inject;
46+
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
47+
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable;
48+
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger;
49+
import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl;
4950

5051
/**
5152
* Triggers a build when we receive a GitHub post-commit webhook.
@@ -149,40 +150,16 @@ public void start(AbstractProject<?, ?> project, boolean newInstance) {
149150

150151
/**
151152
* Tries to register hook for current associated job.
153+
* Do this lazily to avoid blocking the UI thread.
152154
* Useful for using from groovy scripts.
153155
* @since 1.11.2
154156
*/
155157
public void registerHooks() {
156-
// make sure we have hooks installed. do this lazily to avoid blocking the UI thread.
157-
final Collection<GitHubRepositoryName> names = GitHubRepositoryNameContributor.parseAssociatedNames(job);
158-
159-
getDescriptor().queue.execute(new Runnable() {
160-
public void run() {
161-
LOGGER.log(Level.INFO, "Adding GitHub webhooks for {0}", names);
162-
163-
for (GitHubRepositoryName name : names) {
164-
for (GHRepository repo : name.resolve()) {
165-
try {
166-
if(createJenkinsHook(repo, getDescriptor().getHookUrl())) {
167-
break;
168-
}
169-
} catch (Throwable e) {
170-
LOGGER.log(Level.WARNING, "Failed to add GitHub webhook for "+name, e);
171-
}
172-
}
173-
}
174-
}
175-
});
158+
URL hookUrl = getDescriptor().getHookUrl();
159+
Runnable hookRegistrator = forHookUrl(hookUrl).registerFor(job);
160+
getDescriptor().queue.execute(hookRegistrator);
176161
}
177162

178-
private boolean createJenkinsHook(GHRepository repo, URL url) {
179-
try {
180-
repo.createHook("jenkins", Collections.singletonMap("jenkins_hook_url", url.toExternalForm()), null, true);
181-
return true;
182-
} catch (IOException e) {
183-
throw new GHException("Failed to update jenkins hooks", e);
184-
}
185-
}
186163

187164
@Override
188165
public void stop() {
@@ -233,7 +210,7 @@ public String getLog() throws IOException {
233210
* @since 1.350
234211
*/
235212
public void writeLogTo(XMLOutput out) throws IOException {
236-
new AnnotatedLargeText<GitHubWebHookPollingAction>(getLogFile(), Charset.defaultCharset(),true,this).writeHtmlTo(0,out.asWriter());
213+
new AnnotatedLargeText<GitHubWebHookPollingAction>(getLogFile(), Charsets.UTF_8, true, this).writeHtmlTo(0, out.asWriter());
237214
}
238215
}
239216

@@ -278,8 +255,14 @@ public void setManageHook(boolean v) {
278255
/**
279256
* Returns the URL that GitHub should post.
280257
*/
281-
public URL getHookUrl() throws MalformedURLException {
282-
return hookUrl!=null ? new URL(hookUrl) : new URL(Hudson.getInstance().getRootUrl()+GitHubWebHook.get().getUrlName()+'/');
258+
public URL getHookUrl() {
259+
try {
260+
return hookUrl != null
261+
? new URL(hookUrl)
262+
: new URL(Jenkins.getInstance().getRootUrl() + GitHubWebHook.get().getUrlName() + '/');
263+
} catch (MalformedURLException e) {
264+
throw new RuntimeException("Hook url is malformed", e);
265+
}
283266
}
284267

285268
public boolean hasOverrideURL() {
@@ -337,22 +320,26 @@ public FormValidation doReRegister() {
337320
return FormValidation.error("Works only when Jenkins manages hooks");
338321
}
339322

340-
int triggered = 0;
341-
for (AbstractProject<?,?> job : getJenkinsInstance().getAllItems(AbstractProject.class)) {
342-
if (!job.isBuildable()) {
343-
continue;
344-
}
323+
List<GitHubPushTrigger> registered = from(getJenkinsInstance().getAllItems(AbstractProject.class))
324+
.filter(isBuildable())
325+
.filter(withTrigger(GitHubPushTrigger.class))
326+
.transform(reRegisterHooks()).toList();
345327

346-
GitHubPushTrigger trigger = job.getTrigger(GitHubPushTrigger.class);
347-
if (trigger!=null) {
328+
329+
LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", registered.size());
330+
return FormValidation.ok("Called re-register hooks for %s jobs", registered.size());
331+
}
332+
333+
private Function<AbstractProject, GitHubPushTrigger> reRegisterHooks() {
334+
return new Function<AbstractProject, GitHubPushTrigger>() {
335+
@Override
336+
public GitHubPushTrigger apply(AbstractProject job) {
337+
GitHubPushTrigger trigger = (GitHubPushTrigger) job.getTrigger(GitHubPushTrigger.class);
348338
LOGGER.log(Level.FINE, "Calling registerHooks() for {0}", job.getFullName());
349339
trigger.registerHooks();
350-
triggered++;
340+
return trigger;
351341
}
352-
}
353-
354-
LOGGER.log(Level.INFO, "Called registerHooks() for {0} jobs", triggered);
355-
return FormValidation.ok("Called re-register hooks for " + triggered + " jobs");
342+
};
356343
}
357344

358345
public static final Jenkins getJenkinsInstance() throws IllegalStateException {

0 commit comments

Comments
 (0)