Skip to content

Commit

Permalink
feat: add caching feature in global config (refs #272) (#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasbjerre committed Aug 19, 2023
1 parent ef3289a commit 811e50b
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 17 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,20 @@ 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 jobs

When plugin is used in large installations it may need some time to retrieve all configured jobs. This can be cached by enabling it in the global configuration. When enabled, the plugin will cache configured jobs for a configured time. The plugin will automatically refresh the cache so that any 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 in invocations where a `token` is supplied.

## Trigger exactly one build

Expand Down Expand Up @@ -106,26 +112,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 +378,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
60 changes: 60 additions & 0 deletions src/main/java/org/jenkinsci/plugins/gwt/global/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.jenkinsci.plugins.gwt.global;

import hudson.Extension;
import java.io.Serializable;
import java.util.Optional;
import jenkins.model.GlobalConfiguration;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.StaplerRequest;

@Extension
public class CacheConfig extends GlobalConfiguration implements Serializable {

private static final long serialVersionUID = -3077539230674127483L;
private static final int DEFAULT_GET_JOBS_CACHE_MINUTES = 15;

public static CacheConfig get() {
return GlobalConfiguration.all().get(CacheConfig.class);
}

private boolean cacheGetJobs;
private int cacheGetJobsMinutes;

public CacheConfig(final boolean cacheGetJobs, final Integer cacheGetJobsMinutes) {
this.cacheGetJobs = cacheGetJobs;
this.cacheGetJobsMinutes = cacheGetJobsMinutes;
}

public CacheConfig() {
this.load();
}

@Override
public boolean configure(final StaplerRequest req, final JSONObject json) throws FormException {
req.bindJSON(this, json);
this.save();
return true;
}

@DataBoundSetter
public void setCacheGetJobs(final boolean cacheGetJobs) {
this.cacheGetJobs = cacheGetJobs;
}

public boolean isCacheGetJobs() {
return this.cacheGetJobs;
}

@DataBoundSetter
public void setCacheGetJobsMinutes(final int cacheGetJobsMinutes) {
if (cacheGetJobsMinutes < 1) {
this.cacheGetJobsMinutes = 1;
}
this.cacheGetJobsMinutes = cacheGetJobsMinutes;
}

public int getCacheGetJobsMinutes() {
return Optional.ofNullable(this.cacheGetJobsMinutes).orElse(DEFAULT_GET_JOBS_CACHE_MINUTES);
}
}
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 Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,134 @@
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;
import org.jenkinsci.plugins.gwt.global.CacheConfig;

public class JobFinderImpersonater {
public List<ParameterizedJob> getAllParameterizedJobs(boolean impersonate) {
private static final boolean DO_IMPERSONATE = true;
private static Logger LOGGER = Logger.getLogger(JobFinderImpersonater.class.getName());
private static final long CACHE_REFRESH_INITIAL_DELAY = 0;
private ScheduledExecutorService scheduledExecutorService = null;
private LoadingCache<Boolean, List<ParameterizedJob>> loadingCache = null;
private boolean cacheGetJobs = false;
private int cacheGetJobsMinutes = 0;

public JobFinderImpersonater() {}

public List<ParameterizedJob> getAllParameterizedJobs(final boolean impersonate) {
this.reconfigureCachingIfNecessary();
final boolean useCache = CacheConfig.get().isCacheGetJobs();
if (useCache && impersonate) {
try {
LOGGER.log(Level.FINE, "Using the cache");
return this.getCachedJobs();
} catch (final ExecutionException e) {
LOGGER.log(Level.SEVERE, "Was unable to getAllParameterizedJobs from cache.", e);
return doGetAllParameterizedJobs(impersonate);
}
} else if (useCache && !impersonate) {
LOGGER.log(
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");
return doGetAllParameterizedJobs(impersonate);
}

private List<ParameterizedJob> getCachedJobs() throws ExecutionException {
return this.loadingCache.get(DO_IMPERSONATE);
}

synchronized void reconfigureCachingIfNecessary() {
final boolean configCacheGetJobs = CacheConfig.get().isCacheGetJobs();
final int configCacheGetJobsMinutes = CacheConfig.get().getCacheGetJobsMinutes();
final boolean shouldReconfigure =
this.cacheGetJobs != configCacheGetJobs
|| this.cacheGetJobsMinutes != configCacheGetJobsMinutes;
if (shouldReconfigure) {
LOGGER.log(
Level.INFO,
"Reconfiguring cache, was (enabled: "
+ this.cacheGetJobs
+ ", minutes: "
+ this.cacheGetJobsMinutes
+ ") changing to (enabled: "
+ configCacheGetJobs
+ ", minutes: "
+ configCacheGetJobsMinutes
+ ")");
} else {
return;
}

if (configCacheGetJobs) {
this.stopCaching();
this.startCaching();
try {
// Make a call to add the entry to cache
this.getCachedJobs();
} catch (final ExecutionException e) {
LOGGER.log(Level.SEVERE, "Was unable to trigger cache", e);
}
} else {
this.stopCaching();
}
this.cacheGetJobs = configCacheGetJobs;
this.cacheGetJobsMinutes = configCacheGetJobsMinutes;
}

private void startCaching() {
final int cacheMinutes = CacheConfig.get().getCacheGetJobsMinutes();
final int cacheRefreshDuration = cacheMinutes > 1 ? cacheMinutes - 1 : cacheMinutes;

this.loadingCache =
CacheBuilder.newBuilder() //
.refreshAfterWrite(Duration.ofMinutes(cacheMinutes)) //
.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);
return doGetAllParameterizedJobs(impersonate);
}
});
this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
this.scheduledExecutorService.scheduleWithFixedDelay(
() -> {
LOGGER.log(Level.FINE, "Triggering cache refresh");
this.loadingCache.asMap().keySet().forEach((key) -> this.loadingCache.refresh(key));
},
CACHE_REFRESH_INITIAL_DELAY,
cacheRefreshDuration,
TimeUnit.MINUTES);
}

private void stopCaching() {
if (this.scheduledExecutorService != null) {
this.scheduledExecutorService.shutdown();
}
if (this.loadingCache != null) {
this.loadingCache.invalidateAll();
}
}

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
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core"
xmlns:d="jelly:define"
xmlns:f="/lib/form"
xmlns:l="/lib/layout"
xmlns:st="jelly:stapler"
xmlns:t="/lib/hudson"
xmlns:c="/lib/credentials">

<f:section title="Generic Webhook Trigger Cache">
<f:entry title="Cache Get Jobs" field="cacheGetJobs">
<f:checkbox/>
<f:description>
If checked, the plugin will cache available configured jobs. So that the plugin does not need to retrieve that list when invoked. It will only cache when the <code>token</code>-parameter is supplied.
</f:description>
</f:entry>

<f:entry title="Cache Get Jobs Minutes">
<f:textbox field="cacheGetJobsMinutes"/>
<f:description>
Time, in minutes, to keep the jobs in cache before they are refreshed.
</f:description>
</f:entry>
</f:section>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div>
<p>
This feature is also documented <a href="https://github.com/jenkinsci/generic-webhook-trigger-plugin">here</a>.
</p>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
xmlns:t="/lib/hudson"
xmlns:c="/lib/credentials">

<f:section title="Generic Webhook Trigger">
<f:section title="Generic Webhook Trigger Whitelist">

<f:optionalBlock field="enabled" title="Whitelist enabled" inline="true">
<f:entry title="Whitelisted servers">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div>
<p>
See <a href="https://github.com/jenkinsci/generic-webhook-trigger-plugin">Generic Webhook Trigger Plugin</a> for details on how to configure and use this plugin.
This feature is also documented <a href="https://github.com/jenkinsci/generic-webhook-trigger-plugin">here</a>.
</p>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public void before() {
this.allParameterizedJobsByImpersonation = new ArrayList<>();
final JobFinderImpersonater jobFinderImpersonater =
new JobFinderImpersonater() {
@Override
synchronized void reconfigureCachingIfNecessary() {}

@Override
public List<ParameterizedJob> getAllParameterizedJobs(final boolean impersonate) {
JobFinderTest.this.didImpersonate = impersonate;
Expand Down

0 comments on commit 811e50b

Please sign in to comment.