diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java
index 73ce8dc1..62c0bf8a 100644
--- a/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java
+++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/config/Config.java
@@ -325,34 +325,15 @@ public String toString() {
+ '}';
}
}
-
+ public enum Trigger {
+ PERIODIC,
+ MANUAL
+ }
public static class BundleConfig {
- /** Default maximum bundle size (compressed download and decompressed contents): 512 MB. */
- public static final long DEFAULT_MAX_SIZE_BYTES = 512L * 1024 * 1024;
-
private PollingConfig polling;
private String service;
private String resource;
-
- /**
- * Maximum bundle size in bytes, applied both to the compressed HTTP download and to the
- * decompressed tarball contents. Defaults to {@link #DEFAULT_MAX_SIZE_BYTES} (512 MB).
- *
- *
Effective ceiling differs by path, because both paths buffer payloads
- * into a Java {@code byte[]}, whose maximum length is {@link Integer#MAX_VALUE} (~2 GB):
- *
- *
- * - HTTP download: The entire response is buffered in a single {@code
- * byte[]}, so the effective limit is capped at {@code Integer.MAX_VALUE} (~2 GB).
- * Configured values above that are silently clamped to {@code Integer.MAX_VALUE} on the
- * download path.
- *
- Decompressed tarball contents: The cumulative budget is tracked as a
- * {@code long} and may exceed 2 GB. Only each individual entry is bounded by the {@code
- * byte[]} ceiling (~2 GB per entry); the sum across all entries can be larger.
- *
- */
- @JsonProperty("max_size_bytes")
- private long maxSizeBytes = DEFAULT_MAX_SIZE_BYTES;
+ public static final long DEFAULT_MAX_SIZE_BYTES = 1024L * 1024L * 1024L;
public PollingConfig getPolling() {
return polling;
@@ -381,20 +362,6 @@ public BundleConfig setResource(String resource) {
return this;
}
- public long getMaxSizeBytes() {
- return maxSizeBytes;
- }
-
- /**
- * Set the maximum bundle size in bytes. Note that the HTTP download enforcement is capped at
- * {@link Integer#MAX_VALUE} (~2 GB); values above that are clamped on the download path. See
- * {@link #maxSizeBytes} for details.
- */
- public BundleConfig setMaxSizeBytes(long maxSizeBytes) {
- this.maxSizeBytes = maxSizeBytes;
- return this;
- }
-
@Override
public String toString() {
return "BundleConfig{"
@@ -406,8 +373,6 @@ public String toString() {
+ ", resource='"
+ resource
+ '\''
- + ", maxSizeBytes="
- + maxSizeBytes
+ '}';
}
}
diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundleDownloader.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundleDownloader.java
index 99af2444..17616d46 100644
--- a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundleDownloader.java
+++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundleDownloader.java
@@ -53,6 +53,7 @@ public abstract class BundleDownloader {
protected String service;
protected String resource;
protected Config.PollingConfig polling;
+ protected Config.Trigger trigger = Config.Trigger.PERIODIC;
protected String etag;
protected long lastModifiedTime = 0;
protected long maxSizeBytes = Config.BundleConfig.DEFAULT_MAX_SIZE_BYTES;
@@ -66,6 +67,11 @@ public abstract class BundleDownloader {
"binary/octet-stream",
"application/x-tar");
+ public BundleDownloader setTrigger(Config.Trigger trigger) {
+ this.trigger = trigger;
+ return this;
+ }
+
/**
* Construct a BundleDownloader.
*
@@ -180,6 +186,13 @@ public CompletableFuture getInitialActivation() {
* @return a future that completes when the initial bundle is downloaded and activated
*/
public CompletableFuture startPolling(ScheduledExecutorService scheduler) {
+
+ if (trigger == Config.Trigger.MANUAL) {
+ manager.getLogger().info(
+ "Bundle '%s': Manual trigger mode enabled; waiting for refresh()", name);
+ return initialActivation;
+ }
+
int minDelay =
(polling != null && polling.getMinDelaySeconds() != null)
? polling.getMinDelaySeconds()
@@ -189,12 +202,19 @@ public CompletableFuture startPolling(ScheduledExecutorService scheduler)
? polling.getMaxDelaySeconds()
: 120;
+
+
scheduler.schedule(this::downloadBundle, 0, TimeUnit.SECONDS);
scheduleNextPoll(scheduler, minDelay, maxDelay);
return initialActivation;
}
+ public CompletableFuture refresh() {
+ downloadBundle();
+ return initialActivation;
+ }
+
// Re-schedules the next download with a uniformly random delay in [minDelay, maxDelay],
// matching Go-OPA's jittered polling. ScheduledExecutorService has no built-in jitter, so the
// task chains itself. RejectedExecutionException after a shutdown breaks the chain cleanly.
diff --git a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundlePlugin.java b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundlePlugin.java
index 8f5d1904..58863c6b 100644
--- a/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundlePlugin.java
+++ b/opa-services/src/main/java/io/github/open_policy_agent/opa/plugins/BundlePlugin.java
@@ -73,13 +73,12 @@ public Plugin initialize(PluginManager manager) {
ServicePlugin.Service svc =
servicePlugin == null ? null : servicePlugin.getService(bundleConfig.getService());
- Bundle bundle =
+ plugin.bundles.put(
+ name,
new Bundle(name, manager, svc)
.setService(bundleConfig.getService())
.setResource(bundleConfig.getResource())
- .setPolling(bundleConfig.getPolling());
- bundle.setMaxSizeBytes(bundleConfig.getMaxSizeBytes());
- plugin.bundles.put(name, bundle);
+ .setPolling(bundleConfig.getPolling()));
}
}
@@ -184,7 +183,7 @@ public Config.PollingConfig getPolling() {
protected void activateBundle(byte[] bundleData) {
try {
// Load bundle using TarballBundleLoader
- TarballBundleLoader loader = new TarballBundleLoader(name, bundleData, maxSizeBytes);
+ TarballBundleLoader loader = new TarballBundleLoader(name, bundleData);
loader.load(manager.getStore());
manager.getLogger().info("Bundle '%s': Activated", name);
diff --git a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/BundlePluginTest.java b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/BundlePluginTest.java
index e7259044..7da79a05 100644
--- a/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/BundlePluginTest.java
+++ b/opa-services/src/test/java/io/github/open_policy_agent/opa/plugins/BundlePluginTest.java
@@ -45,6 +45,62 @@ void setUp() {
config.setServices(Collections.singletonMap("test-service", service));
}
+ @Test
+ void validate_manualTriggerWithPolling_returnsError() {
+ Config.BundleConfig bundle =
+ new Config.BundleConfig()
+ .setService("test-service")
+ .setResource("/bundles/test.tar.gz")
+ .setTrigger(Config.Trigger.MANUAL)
+ .setPolling(
+ new Config.PollingConfig()
+ .setMinDelaySeconds(60)
+ .setMaxDelaySeconds(120));
+
+ config.setBundles(Collections.singletonMap("test-bundle", bundle));
+
+ manager =
+ new PluginManager.Builder()
+ .withId("test-opa")
+ .withStore(store)
+ .withConfig(config)
+ .withLogger(mockLogger)
+ .build();
+
+ Set errors = new BundlePlugin().validate(manager);
+
+ assertTrue(
+ errors.stream()
+ .anyMatch(e -> e.contains("cannot specify polling when trigger is manual")));
+ }
+
+ @Test
+ void initialize_manualTrigger_setsTrigger() {
+ Config.BundleConfig bundle =
+ new Config.BundleConfig()
+ .setService("test-service")
+ .setResource("/bundles/test.tar.gz")
+ .setTrigger(Config.Trigger.MANUAL);
+
+ config.setBundles(Collections.singletonMap("test-bundle", bundle));
+
+ manager =
+ new PluginManager.Builder()
+ .withId("test-opa")
+ .withStore(store)
+ .withConfig(config)
+ .withLogger(mockLogger)
+ .build();
+
+ BundlePlugin plugin = (BundlePlugin) new BundlePlugin().initialize(manager);
+
+ assertNotNull(plugin.getBundle("test-bundle"));
+ assertEquals(
+ Config.Trigger.MANUAL,
+ plugin.getBundle("test-bundle").getTrigger());
+ }
+
+
@Test
void validate_noBundlesConfigured_returnsNoErrors() {
manager =