Skip to content

Commit

Permalink
feat: add caching feature to jobfinder (refs #272)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasbjerre committed Aug 19, 2023
1 parent ef3289a commit dcb9f98
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 20 deletions.
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,24 @@ The token can be supplied as a:

- Request parameter:

`curl -vs http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=abc123 2>&1`
`curl -vs "http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=abc123" 2>&1`
- Token header:

`curl -vs -H "token: abc123" http://localhost:8080/jenkins/generic-webhook-trigger/invoke 2>&1`
`curl -vs -H "token: abc123" "http://localhost:8080/jenkins/generic-webhook-trigger/invoke" 2>&1`
- It will also detect `X-Gitlab-Token`.
- _Authorization_ header of type _Bearer_ :

`curl -vs -H "Authorization: Bearer abc123" http://localhost:8080/jenkins/generic-webhook-trigger/invoke 2>&1`
`curl -vs -H "Authorization: Bearer abc123" "http://localhost:8080/jenkins/generic-webhook-trigger/invoke" 2>&1`

### Cache parameter

There is a special `usecache` parameter. When supplied, the plugin will cache configured jobs for `15` minutes. After the first invocation, useing this parameter, the plugin will automatically refresh the cache so that any future calls will use the cached value. This means the effect of any changes to any configured job will be delayed.

The cache will only be used if a `token` is also supplied.

The cache will be used when `usecache=true` parameter is passed like this:

`curl -vs "http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=abc123&usecache=true" 2>&1`

## Trigger exactly one build

Expand Down Expand Up @@ -106,26 +116,26 @@ If you are fiddling with expressions, you may want to checkout:
It's probably easiest to do with `curl`. Given that you have configured a Jenkins job to trigger on Generic Webhook, here are some examples of how to start the jobs.

```bash
curl -vs http://localhost:8080/jenkins/generic-webhook-trigger/invoke 2>&1
curl -vs "http://localhost:8080/jenkins/generic-webhook-trigger/invoke" 2>&1
```

This should start your job, if the job has no `token` configured and no security enabled. If you have security enabled you may need to authenticate:

```bash
curl -vs http://theusername:thepasssword@localhost:8080/jenkins/generic-webhook-trigger/invoke 2>&1
curl -vs "http://theusername:thepasssword@localhost:8080/jenkins/generic-webhook-trigger/invoke" 2>&1
```

If your job has a `token` you don't need to supply other credentials. You can specify the `token` like this:

```bash
curl -vs http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=TOKEN_HERE 2>&1
curl -vs "http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=TOKEN_HERE" 2>&1
```
Please keep in mind, using a token always runs the triggered jobs with SYSTEM privileges.

If you want to trigger with `token` and some post content, `curl` can dot that like this.

```bash
curl -v -H "Content-Type: application/json" -X POST -d '{ "app":{ "name":"some value" }}' http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=TOKEN_HERE
curl -v -H "Content-Type: application/json" -X POST -d '{ "app":{ "name":"some value" }}' "http://localhost:8080/jenkins/generic-webhook-trigger/invoke?token=TOKEN_HERE"
```

## Screenshots
Expand Down Expand Up @@ -372,7 +382,7 @@ pipeline {
It can be triggered with something like:

```bash
curl -X POST -H "Content-Type: application/json" -H "headerWithNumber: nbr123" -H "headerWithString: a b c" -d '{ "before": "1848f12", "after": "5cab1", "ref": "refs/heads/develop" }' -vs http://admin:admin@localhost:8080/jenkins/generic-webhook-trigger/invoke?requestWithNumber=nbr%20123\&requestWithString=a%20string
curl -X POST -H "Content-Type: application/json" -H "headerWithNumber: nbr123" -H "headerWithString: a b c" -d '{ "before": "1848f12", "after": "5cab1", "ref": "refs/heads/develop" }' -vs "http://admin:admin@localhost:8080/jenkins/generic-webhook-trigger/invoke?requestWithNumber=nbr%20123&requestWithString=a%20string"
```

And the job will have this in the log:
Expand Down
10 changes: 7 additions & 3 deletions debug.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/bin/sh
mvn versions:update-properties
mvnDebug -q hpi:run -Djava.util.logging.config.file=logging.properties -Djenkins.version=2.204.1 -Denforcer.skip=true

./mvnw versions:update-properties
MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n" \
./mvnw hpi:run \
-Djava.util.logging.config.file=logging.properties \
-Djenkins.version=2.361.4 \
-Denforcer.skip=true \
-Dhudson.model.ParametersAction.keepUndefinedParameters=true
8 changes: 6 additions & 2 deletions run.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#!/bin/sh
mvn versions:update-properties
mvn hpi:run -Djava.util.logging.config.file=logging.properties -Djenkins.version=2.346.3 -Denforcer.skip=true
./mvnw versions:update-properties
./mvnw hpi:run \
-Djava.util.logging.config.file=logging.properties \
-Djenkins.version=2.361.4 \
-Denforcer.skip=true \
-Dhudson.model.ParametersAction.keepUndefinedParameters=true
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ HttpResponse doInvoke(
final String postContent,
final String givenToken) {

final List<FoundJob> foundJobs = JobFinder.findAllJobsWithTrigger(givenToken);
final boolean useCache = this.shouldUseCache(parameterMap);

Check warning on line 173 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 173 is not covered by tests
final List<FoundJob> foundJobs = JobFinder.findAllJobsWithTrigger(givenToken, useCache);

Check warning on line 174 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 174 is not covered by tests
final Map<String, Object> triggerResultsMap = new HashMap<>();
boolean allSilent = true;
boolean errors = false;
Expand Down Expand Up @@ -213,6 +214,21 @@ HttpResponse doInvoke(
}
}

private boolean shouldUseCache(final Map<String, String[]> parameterMap) {
if (parameterMap == null) {

Check warning on line 218 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 218 is only partially covered, 2 branches are missing
return false;

Check warning on line 219 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 219 is not covered by tests
}
final String useCacheParamName = "usecache";

Check warning on line 221 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 221 is not covered by tests
if (parameterMap.containsKey(useCacheParamName)) {

Check warning on line 222 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 222 is only partially covered, 2 branches are missing
final String[] useCache = parameterMap.get(useCacheParamName);

Check warning on line 223 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 223 is not covered by tests
if (useCache.length == 1) {

Check warning on line 224 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 224 is only partially covered, 2 branches are missing
final String useCacheValue = useCache[0];

Check warning on line 225 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 225 is not covered by tests
return useCacheValue != null && (useCacheValue.equalsIgnoreCase("true"));

Check warning on line 226 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 226 is only partially covered, 4 branches are missing
}
}
return false;

Check warning on line 229 in src/main/java/org/jenkinsci/plugins/gwt/GenericWebHookRequestReceiver.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 229 is not covered by tests
}

String createMessageFromException(final Throwable t) {
String stacktraceInfo = "";
if (t.getStackTrace().length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

public final class JobFinder {

private static Logger LOG = Logger.getLogger(JobFinder.class.getSimpleName());
private static Logger LOG = Logger.getLogger(JobFinder.class.getName());

private JobFinder() {}

Expand All @@ -33,13 +33,14 @@ static void setJobFinderImpersonater(final JobFinderImpersonater jobFinderImpers
JobFinder.jobFinderImpersonater = jobFinderImpersonater;
}

public static List<FoundJob> findAllJobsWithTrigger(final String givenToken) {
public static List<FoundJob> findAllJobsWithTrigger(
final String givenToken, final boolean useCache) {

final List<FoundJob> found = new ArrayList<>();

final boolean impersonate = !isNullOrEmpty(givenToken);
final List<ParameterizedJob> candidateProjects =
jobFinderImpersonater.getAllParameterizedJobs(impersonate);
jobFinderImpersonater.getAllParameterizedJobs(impersonate, useCache);
for (final ParameterizedJob candidateJob : candidateProjects) {
final GenericTrigger genericTriggerOpt = findGenericTrigger(candidateJob.getTriggers());
if (genericTriggerOpt != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
package org.jenkinsci.plugins.gwt.jobfinder;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import hudson.security.ACL;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn.ParameterizedJob;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;

public class JobFinderImpersonater {
public List<ParameterizedJob> getAllParameterizedJobs(boolean impersonate) {
private static Logger LOGGER = Logger.getLogger(JobFinderImpersonater.class.getName());

private static final int CACHE_MINUTES = 15;
private static final int CACHE_REFRESH_INITIAL_DELAY = 0;
private static final int CACHE_REFRESH_DURATION = CACHE_MINUTES - 1;

private final ScheduledExecutorService newSingleThreadScheduledExecutor;
private final LoadingCache<Boolean, List<ParameterizedJob>> cache;

public JobFinderImpersonater() {
this.cache =
CacheBuilder.newBuilder() //
.refreshAfterWrite(Duration.ofMinutes(CACHE_MINUTES)) //
.build(
new CacheLoader<Boolean, List<ParameterizedJob>>() {
@Override
public List<ParameterizedJob> load(final Boolean impersonate) throws Exception {
LOGGER.log(Level.FINE, "Loading the cache with impersonate " + impersonate);

Check warning on line 38 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 38 is not covered by tests
return doGetAllParameterizedJobs(impersonate);

Check warning on line 39 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 39 is not covered by tests
}
});
this.newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
this.newSingleThreadScheduledExecutor.scheduleWithFixedDelay(
() -> {
LOGGER.log(Level.FINE, "Triggering cache refresh");
this.cache.asMap().keySet().parallelStream().forEach((key) -> this.cache.refresh(key));
},
CACHE_REFRESH_INITIAL_DELAY,
CACHE_REFRESH_DURATION,
TimeUnit.MINUTES);
}

public List<ParameterizedJob> getAllParameterizedJobs(
final boolean impersonate, final boolean useCache) {
if (useCache && impersonate) {

Check warning on line 55 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 55 is only partially covered, 4 branches are missing
try {
LOGGER.log(Level.FINE, "Using the cache");

Check warning on line 57 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 57 is not covered by tests
return this.cache.get(impersonate);

Check warning on line 58 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 58 is not covered by tests
} catch (final ExecutionException e) {

Check warning on line 59 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 59 is not covered by tests
LOGGER.log(Level.SEVERE, "Was unable to getAllParameterizedJobs from cache.", e);

Check warning on line 60 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 60 is not covered by tests
return doGetAllParameterizedJobs(impersonate);

Check warning on line 61 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 61 is not covered by tests
}
} else if (useCache && !impersonate) {

Check warning on line 63 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 63 is only partially covered, 4 branches are missing
LOGGER.log(

Check warning on line 64 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 64 is not covered by tests
Level.INFO,
"Not using the cache because jobs are not retreieved with impersonation SYSTEM. "
+ "SYSTEM is only impersonated when using a token."
+ " If SYSTEM is not impersonated, only jobs available for the currently authenticated user is found.");
}
LOGGER.log(Level.FINE, "Not using the cache");

Check warning on line 70 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 70 is not covered by tests
return doGetAllParameterizedJobs(impersonate);

Check warning on line 71 in src/main/java/org/jenkinsci/plugins/gwt/jobfinder/JobFinderImpersonater.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 71 is not covered by tests
}

private static List<ParameterizedJob> doGetAllParameterizedJobs(final boolean impersonate) {
SecurityContext orig = null;
try {
if (impersonate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public void before() {
final JobFinderImpersonater jobFinderImpersonater =
new JobFinderImpersonater() {
@Override
public List<ParameterizedJob> getAllParameterizedJobs(final boolean impersonate) {
public List<ParameterizedJob> getAllParameterizedJobs(
final boolean impersonate, final boolean usecache) {
JobFinderTest.this.didImpersonate = impersonate;
return JobFinderTest.this.allParameterizedJobsByImpersonation;
}
Expand Down Expand Up @@ -71,7 +72,7 @@ private ParameterizedJob createJob(final String authToken, final String genericT
}

private List<String> findAllJobs(final String givenToken) {
final List<FoundJob> foundJobs = JobFinder.findAllJobsWithTrigger(givenToken);
final List<FoundJob> foundJobs = JobFinder.findAllJobsWithTrigger(givenToken, false);
final List<String> names = new ArrayList<>();
for (final FoundJob found : foundJobs) {
names.add(found.getFullName());
Expand Down

0 comments on commit dcb9f98

Please sign in to comment.