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): - * - *

- */ - @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 =