-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Problem Statement
Organizations embedding OPA often need custom storage implementations that differ from the built-in inmem and disk backends. While performance optimization PRs targeting core storage are often rejected (as noted in #8245), there's currently no clean way to plug in custom storage backends when using OPA as a server via opa run.
Current State
OPA provides several extension points:
runtime.RegisterPlugin()for custom pluginstopdown.RegisterBuiltinFunc()for custom built-in functionsbundle.RegisterStoreFunc()for bundle storage (used byrego,tester,sdk)runtime.Params.StoreBuilderfor programmatic runtime storage configuration
However, there are gaps:
-
runtime.Params.StoreBuilderrequires direct access toParamsat runtime creation. This works for SDK users but not for CLI users runningopa run, since you can't set a function pointer via command-line flags. -
bundle.BundleExtStore(set viabundle.RegisterStoreFunc()) is used in specific contexts (v1/rego/rego.go:1400,v1/tester/runner.go:1179,v1/sdk/opa.go:187) but is not consulted by the main runtime storage initialization in v1/runtime/runtime.go:447-461:
switch {
case params.DiskStorage != nil:
store, err = disk.New(ctx, logger, metrics, *params.DiskStorage)
case params.StoreBuilder != nil:
store, err = params.StoreBuilder(ctx, logger, metrics, config, params.ID)
default:
store = inmem.NewWithOpts(...)
}Proposed Solution
Add a global storage backend registration mechanism similar to runtime.RegisterPlugin(), following the existing pattern:
package runtime
// StorageFactory creates a storage.Store instance
type StorageFactory func(
ctx context.Context,
logger logging.Logger,
metrics prometheus.Registerer,
config []byte,
id string,
) (storage.Store, error)
// RegisterStorageBackend registers a custom storage factory.
// When called before creating a Runtime, the registered factory will be used
// instead of the default inmem/disk backends (unless overridden by Params.StoreBuilder).
func RegisterStorageBackend(factory StorageFactory) {
registeredStorageMux.Lock()
defer registeredStorageMux.Unlock()
registeredStorage = factory
}The runtime initialization logic would check for registered storage:
switch {
case params.DiskStorage != nil:
store, err = disk.New(ctx, logger, metrics, *params.DiskStorage)
case params.StoreBuilder != nil:
store, err = params.StoreBuilder(ctx, logger, metrics, config, params.ID)
case registeredStorage != nil:
store, err = registeredStorage(ctx, logger, metrics, config, params.ID)
default:
store = inmem.NewWithOpts(...)
}Use Case
This allows embedding users to create custom main.go that registers storage before invoking the standard CLI:
package main
import (
"github.com/open-policy-agent/opa/cmd"
"github.com/open-policy-agent/opa/v1/runtime"
"mycompany/optimized-storage"
)
func main() {
runtime.RegisterStorageBackend(optimizedstorage.NewStore)
// Now users can run: ./opa run -s ...
// and get custom storage without modifying OPA core
if err := cmd.RootCommand.Execute(); err != nil {
os.Exit(1)
}
}Benefits
- Preserves OPA core maintainability - keeps experimental/specialized storage out of the main repository
- Follows established patterns - mirrors
RegisterPlugin,RegisterBuiltin, etc. - Minimal implementation complexity - ~30 lines of code, no breaking changes
- Enables innovation - teams can experiment with storage optimizations without waiting for upstream approval
- Works with CLI - unlike
Params.StoreBuilder, this approach works withopa run
Implementation Notes
- The registration should be optional and non-breaking (defaults to existing behavior)
- The precedence order should be:
Params.StoreBuilder>DiskStorage>RegisterStorageBackend()> defaultinmem - Thread-safety should follow the same pattern as
registeredPluginsMuxin v1/runtime/runtime.go:67 - Consider whether to unify this with
bundle.RegisterStoreFunc()or keep them separate (the former is bundle-specific, this would be runtime-global)
Alternative Considered
Extending bundle.RegisterStoreFunc() to also affect runtime storage initialization. However, this conflates bundle storage with runtime storage, which may have different lifecycle and configuration requirements.