From 0622ff2b29294b1281d5e51c948671d690b97a98 Mon Sep 17 00:00:00 2001 From: Robert Young Date: Thu, 14 May 2026 16:17:05 +1200 Subject: [PATCH 1/2] Add alternate versioning scheme Assisted-by: Claude Sonnet 4.5 Signed-off-by: Robert Young --- .../110-versioned-plugin-configuration.md | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 proposals/110-versioned-plugin-configuration.md diff --git a/proposals/110-versioned-plugin-configuration.md b/proposals/110-versioned-plugin-configuration.md new file mode 100644 index 00000000..6e2dc537 --- /dev/null +++ b/proposals/110-versioned-plugin-configuration.md @@ -0,0 +1,218 @@ +# 110 - Versioned Plugin Configuration + +This proposal introduces explicit API versioning for plugins through a new `@Version` annotation and changes to the plugin type resolution mechanism. +The changes enable plugin implementations to declare their API stability level independently of the project's semantic version and allow users to explicitly reference plugin versions in configuration. +Version information also serves as a disambiguation mechanism when multiple plugin implementations share the same simple class name. + +## Current situation + +Plugins are referenced in configuration using either their simple class name or fully qualified name. +The proxy resolves these references at startup through ServiceLoader discovery, registering each plugin under both its fully qualified name and simple name. +When multiple plugins share the same simple name, the proxy logs an ambiguity warning and requires users to reference the plugin by its fully qualified name. +Plugin API stability is coupled to the project's semantic version, making it difficult to ship experimental or unstable plugin APIs alongside stable ones as the project approaches 1.0.0. + +## Motivation + +Kroxylicious is approaching its 1.0.0 release, which will establish backward compatibility guarantees for public APIs. +However, different plugins may have different maturity levels and API stability guarantees. +Coupling all plugin APIs to a single project version creates tension between shipping experimental features and maintaining stability. +The Record Encryption filter, for example, may need multiple major revisions before its API stabilizes, but tying those revisions to the project version would either delay 1.0.0 or force premature API commitments. + +Kubernetes-style API versioning (v1alpha1, v1beta1, v1) provides a well-understood model for expressing API maturity. +Adopting this model for individual plugins allows the project to reach 1.0.0 while clearly communicating that specific plugins remain experimental. +The versioning scheme also enables controlled migration paths where plugin authors can ship both alpha and beta implementations in the same JAR, allowing users to migrate gradually. + +Version-based disambiguation additionally solves a practical problem for plugin authors who wish to maintain multiple implementations during API transitions. +Currently, two classes named RecordEncryption in different packages create an ambiguity that can only be resolved through fully qualified names. +Adding version as part of the plugin identity allows RecordEncryption/v1alpha1 and RecordEncryption/v1beta1 to coexist without requiring users to remember package structures. + +## Proposal + +### Functional changes + +Plugin authors will annotate their implementations with `@Version("v1alpha1")` or similar version identifiers following Kubernetes conventions. +Users will reference these versioned plugins in configuration using the format `PluginName/version`. +The proxy will validate at configuration parse time that the referenced version matches the plugin's declared version, failing fast with a clear error message if they diverge. +Unversioned plugins and unversioned references will continue to work to maintain backward compatibility with existing deployments. + +For proxy users, configuration syntax extends from `type: RecordEncryption` to support `type: RecordEncryption/v1alpha1`. +When a version is present in the type string, the proxy uses it both for validation and for name resolution. +A plugin named RecordEncryption annotated with `@Version("v1alpha1")` can be referenced as either `RecordEncryption/v1alpha1` (explicit version) or `RecordEncryption` (implicit, triggers warning). +If multiple RecordEncryption implementations exist with different versions (v1alpha1 and v1beta1), users disambiguate by including the version rather than switching to fully qualified class names. + +Version validation ensures that configuration matches plugin implementation. +If a user references `RecordEncryption/v1alpha1` but the plugin declares `@Version("v1beta1")`, the proxy rejects the configuration during parsing with an error message identifying the mismatch. +If a user references `RecordEncryption/v1alpha1` but the plugin has no `@Version` annotation, the proxy similarly rejects the configuration. +This fail-fast behavior prevents runtime surprises from version mismatches. + +The enforcement policy evolves across Kroxylicious releases. +In versions before 1.0.0, version annotations are optional and version references in configuration are optional. +When a plugin declares a version but the configuration omits it, the proxy logs a warning but continues. +This allows plugin authors to begin adopting version annotations while maintaining compatibility with existing configurations. +In 1.0.0 and later, when a plugin declares a version, configurations must include that version. +The proxy rejects configurations that omit required versions. +An environment variable `KROXYLICIOUS_REQUIRE_PLUGIN_VERSIONS` allows operators to opt into strict enforcement before 1.0.0 for testing purposes. + +For filter authors and plugin developers, the `@Version` annotation becomes part of the public API contract. +Placing `@Version("v1alpha1")` on a filter implementation signals to users that the API is experimental and subject to breaking changes. +Transitioning from v1alpha1 to v1beta1 may involve incompatible configuration changes, but users explicitly opt into the new version by updating their configuration. +Plugin authors can ship both versions in the same JAR during migration periods, allowing gradual rollout. +When a plugin reaches stability, annotating it with `@Version("v1")` communicates that the API will remain backward compatible within the v1 series. + +Version-based disambiguation enables new packaging strategies. +A plugin author maintaining both alpha and beta implementations can ship `io.kroxylicious.filter.encryption.alpha.RecordEncryption` annotated with `@Version("v1alpha1")` and `io.kroxylicious.filter.encryption.beta.RecordEncryption` annotated with `@Version("v1beta1")` in the same JAR. +Users running configurations with `type: RecordEncryption/v1alpha1` receive the alpha implementation while users with `type: RecordEncryption/v1beta1` receive the beta implementation. +This eliminates the need for separate JARs or complex classloader isolation to support parallel versions. + +Nested plugin references inherit the same versioning behavior. +When a RecordEncryption filter configuration references a KmsService, that reference can include a version: `kms: VaultKmsService/v1alpha1`. +The same parsing and validation logic applies recursively to all plugin references in the configuration tree, ensuring consistency throughout. + +### Public API changes + +A new `@Version` annotation will be added to `io.kroxylicious.proxy.plugin` in the kroxylicious-api module: + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Version { + /** + * The API version identifier for this plugin implementation. + * Should follow Kubernetes-style versioning: v1alpha1, v1beta1, v1, etc. + */ + String value(); +} +``` + +A new `VersionMismatchException` will be added to `io.kroxylicious.proxy.plugin` in the kroxylicious-api module: + +```java +public class VersionMismatchException extends RuntimeException { + public VersionMismatchException(String message) { + super(message); + } +} +``` + +This exception is thrown when configuration version references do not match plugin version declarations. + +The `PluginFactory` interface in kroxylicious-runtime will gain a new method: + +```java +@Nullable +String pluginVersion(String instanceName); +``` + +This method returns the version from a plugin's `@Version` annotation or null if the plugin is not annotated. +It follows the same signature pattern as the existing `configType(String instanceName)` method and throws `UnknownPluginInstanceException` if the instance name cannot be resolved. + +No changes are required to existing filter APIs or configuration classes. +The `@PluginImplName` and `@PluginImplConfig` annotations already support arbitrary string values for the type field, so the slash-separated version syntax works without modification to the annotation processing infrastructure. + +### Migration plan + +The migration proceeds in two phases aligned with Kroxylicious release versions. + +#### Phase 1: Infrastructure and adoption (pre-1.0.0) + +Phase 1 ships in version 0.22.0 or similar pre-1.0 releases and continues until 1.0.0. + +- All version-related infrastructure is implemented: the `@Version` annotation, the parsing logic, the validation logic, and the enforcement policy. +- Enforcement defaults to warning mode. +- Built-in plugins receive `@Version` annotations indicating their stability level (e.g., `@Version("v1alpha1")` for experimental plugins like Record Encryption, `@Version("v1beta1")` or `@Version("v1")` for more mature filters). +- Documentation and examples are updated to show versioned plugin references. +- When users reference a plugin without including the version, and the plugin has a `@Version` annotation, the proxy logs a warning indicating that explicit versions will be required in 1.0.0. +- Example warning: "Plugin 'RecordEncryption' declares version 'v1alpha1' but config does not specify it. Update config to: type: RecordEncryption/v1alpha1 (required in 1.0.0+)". +- Plugin authors can ship multiple API versions in the same JAR for migration purposes. +- Users are encouraged to update configurations to include explicit versions to prepare for 1.0.0. +- All existing configurations continue to work without modification. +- The `KROXYLICIOUS_REQUIRE_PLUGIN_VERSIONS` environment variable allows operators to opt into strict enforcement before 1.0.0 by setting it to `true`, treating missing versions as errors rather than warnings for testing purposes (particularly useful in CI pipelines and staging environments). + +#### Phase 2: Enforcement (1.0.0+) + +Phase 2 begins with the 1.0.0 release. + +- The enforcement policy changes from warning to error when a plugin declares a version but the configuration omits it. +- Configurations without explicit versions fail during parsing with `VersionMismatchException`. +- This breaking change is acceptable at a major version boundary and has been signaled through warnings in all prior releases. +- Users who updated their configurations in response to warnings experience no disruption. +- Users who ignored warnings must add version suffixes to plugin type references before upgrading to 1.0.0. +- The migration path is clear and mechanical: each warning message indicates exactly which version string to add. + +## Affected/not affected projects + +### Affected + +The kroxylicious repository is affected. +Changes span the kroxylicious-api module (new annotation and exception types), the kroxylicious-runtime module (parsing, validation, and registration logic), and potentially filter implementation modules (adding `@Version` annotations). +The kroxylicious-kms module is affected as KMS provider plugins will need version annotations. +The kroxylicious-authorizer-api module is affected as authorizer plugins will need version annotations. + +### Not affected + +The kroxylicious-operator repository is not directly affected. +The operator passes user-provided configuration to the proxy without interpreting plugin type strings, so versioned syntax passes through transparently. +The Kubernetes CRD schema already permits arbitrary strings in plugin type fields. + +## Compatibility + +Before 1.0.0, all changes are backward compatible. +Existing configurations with unversioned plugin references continue to work. +Existing plugins without `@Version` annotations continue to work. +The only behavioral change is the addition of warning messages when versioned plugins are referenced without versions, and these warnings do not prevent startup or operation. + +At 1.0.0, the change becomes intentionally breaking for configurations that reference versioned plugins without including the version. +This is acceptable at a major version boundary and follows semantic versioning principles. +The breakage is limited to configurations that ignored warnings in prior releases. +Configurations that never referenced versioned plugins (because all their plugins remained unversioned) continue to work without modification even at 1.0.0. + +For plugin developers, adding a `@Version` annotation is a one-way commitment. +Once a plugin is annotated with a version, removing the annotation in a future release would break any configurations that include the version string. +Plugin authors should carefully consider API stability before adding version annotations. +The conventional approach is to start with v1alpha1 for experimental APIs and progress through v1beta1 to v1 as the API stabilizes. + +The Kubernetes CRD versioning approach provides precedent for version transitions. +Moving from v1alpha1 to v1beta1 may involve breaking changes to configuration structure. +Moving from v1beta1 to v1 should avoid breaking changes where possible but may make incompatible changes if necessary. +Moving from v1 to v2 is a major version transition requiring careful migration planning. +Kroxylicious adopts these same conventions, relying on users to understand the stability implications of each version level. + +## Rejected alternatives + +### Semver version strings + +Using semantic version strings (1.0.0, 1.1.0, 2.0.0) instead of Kubernetes-style versions was considered. +Semantic versioning provides more granular version information and is familiar to Java developers. +However, Kubernetes-style versioning has significant advantages. +The alpha/beta/stable progression clearly communicates API maturity in a way that semantic versions do not. +A plugin at version 0.5.0 might be quite stable or highly experimental; the version number alone does not convey this. +A plugin at version v1alpha1 unambiguously signals experimental status. +Kubernetes-style versioning is already used in the kroxylicious-kubernetes-api module for CRDs, so adopting it for plugin configuration maintains consistency across the project. + +### Version as separate configuration field + +Representing version as a separate field in configuration rather than as part of the type string was considered. +The configuration might look like `type: RecordEncryption` and `version: v1alpha1` as sibling fields. +This approach provides clearer separation between plugin identity and version, potentially simplifying parsing. +However, it increases configuration verbosity, requiring two fields instead of one. +It also diverges from patterns like Kubernetes resource definitions where apiVersion is a single field combining API group and version (e.g., apps/v1). +The composite type string approach is more concise and aligns with Kubernetes conventions. + +### Plugin version compatibility checking + +Implementing version compatibility checking rather than exact version matching was considered. +The proxy might accept a configuration referencing v1alpha1 when the plugin implements v1alpha2 if the versions are compatible according to some policy. +This would provide more flexibility during upgrades. +However, defining compatibility rules for alpha and beta versions is difficult because those version levels explicitly allow breaking changes. +For stable v1 versions, semantic versioning compatibility rules could apply, but mixing compatibility rules across version levels creates complexity. +The exact-match approach is simpler, more predictable, and encourages users to make deliberate version upgrade decisions. +When a plugin author releases v1alpha2, configurations must explicitly opt into the new version, ensuring users are aware of potential breaking changes. + +### Automatic version migration + +Providing automatic migration from v1alpha1 to v1beta1 through configuration transformations was considered. +The proxy might detect a v1alpha1 configuration and automatically transform it to v1beta1 format according to plugin-provided migration rules. +This would ease version transitions but adds significant complexity to the configuration system. +Migration rules would need to be discoverable, machine-readable, and composable across multiple plugin upgrades. +The approach also obscures what version is actually in use, potentially causing confusion. +Explicit version references in configuration make the actual plugin version transparent and keep migration responsibility with the configuration owner rather than embedding it in proxy logic. From d6d2119e50700ce66927054191f4980a0909629c Mon Sep 17 00:00:00 2001 From: Robert Young Date: Wed, 20 May 2026 15:28:17 +1200 Subject: [PATCH 2/2] Incorporate feedback from Tom to make this a subset of design #96 functionality Signed-off-by: Robert Young --- .../110-versioned-plugin-configuration.md | 343 ++++++++++++------ 1 file changed, 240 insertions(+), 103 deletions(-) diff --git a/proposals/110-versioned-plugin-configuration.md b/proposals/110-versioned-plugin-configuration.md index 6e2dc537..d0f94217 100644 --- a/proposals/110-versioned-plugin-configuration.md +++ b/proposals/110-versioned-plugin-configuration.md @@ -1,113 +1,178 @@ -# 110 - Versioned Plugin Configuration +# 110 - Plugin Configuration Versioning -This proposal introduces explicit API versioning for plugins through a new `@Version` annotation and changes to the plugin type resolution mechanism. -The changes enable plugin implementations to declare their API stability level independently of the project's semantic version and allow users to explicitly reference plugin versions in configuration. -Version information also serves as a disambiguation mechanism when multiple plugin implementations share the same simple class name. +This proposal introduces explicit configuration schema versioning for plugins by extending the existing `@Plugin` annotation with a `configVersion` attribute. +Plugin implementations can declare support for multiple configuration schema versions simultaneously, enabling controlled API evolution and migration periods. +Users explicitly specify which configuration schema version they are using via a `version` field in their YAML configuration. +This establishes a foundation for Kubernetes-style API versioning (v1alpha1, v1beta1, v1) that will be fully realized in PR #96's multi-file configuration system. ## Current situation Plugins are referenced in configuration using either their simple class name or fully qualified name. -The proxy resolves these references at startup through ServiceLoader discovery, registering each plugin under both its fully qualified name and simple name. -When multiple plugins share the same simple name, the proxy logs an ambiguity warning and requires users to reference the plugin by its fully qualified name. -Plugin API stability is coupled to the project's semantic version, making it difficult to ship experimental or unstable plugin APIs alongside stable ones as the project approaches 1.0.0. +The proxy resolves these references at startup through ServiceLoader discovery. +Each plugin has a single configuration schema tied to its `@Plugin(configType = ...)` annotation. +When a plugin's configuration needs to evolve, the plugin author must choose between breaking existing users or maintaining backward compatibility through complex conditional logic in a single configuration class. +Plugin API stability is implicitly coupled to the project's semantic version, making it difficult to ship experimental or unstable plugin configuration schemas alongside stable ones as the project approaches 1.0.0. ## Motivation Kroxylicious is approaching its 1.0.0 release, which will establish backward compatibility guarantees for public APIs. -However, different plugins may have different maturity levels and API stability guarantees. -Coupling all plugin APIs to a single project version creates tension between shipping experimental features and maintaining stability. -The Record Encryption filter, for example, may need multiple major revisions before its API stabilizes, but tying those revisions to the project version would either delay 1.0.0 or force premature API commitments. +However, different plugins may have different maturity levels and configuration schema stability guarantees. +Coupling all plugin configuration schemas to a single project version creates tension between shipping experimental features and maintaining stability. +The Record Encryption filter, for example, may need multiple configuration schema revisions before it stabilizes, but tying those revisions to the project version would either delay 1.0.0 or force premature API commitments. -Kubernetes-style API versioning (v1alpha1, v1beta1, v1) provides a well-understood model for expressing API maturity. -Adopting this model for individual plugins allows the project to reach 1.0.0 while clearly communicating that specific plugins remain experimental. -The versioning scheme also enables controlled migration paths where plugin authors can ship both alpha and beta implementations in the same JAR, allowing users to migrate gradually. +Kubernetes-style API versioning (v1alpha1, v1beta1, v1) provides a well-understood model for expressing configuration schema maturity. +Adopting this model for individual plugin configurations allows the project to reach 1.0.0 while clearly communicating that specific plugin configurations remain experimental. +The versioning scheme also enables controlled migration paths where plugin authors can support multiple configuration schema versions in the same plugin implementation, allowing users to migrate gradually. -Version-based disambiguation additionally solves a practical problem for plugin authors who wish to maintain multiple implementations during API transitions. -Currently, two classes named RecordEncryption in different packages create an ambiguity that can only be resolved through fully qualified names. -Adding version as part of the plugin identity allows RecordEncryption/v1alpha1 and RecordEncryption/v1beta1 to coexist without requiring users to remember package structures. +This proposal establishes the versioning mechanism that PR #96 will build upon. +By making configuration version an explicit concept in 1.0.0, we ensure users understand that plugin configurations can evolve independently of the project version. +PR #96 will then add the multi-file layout, dependency graphs, and JSON Schema validation that make versioned configurations more powerful. ## Proposal ### Functional changes -Plugin authors will annotate their implementations with `@Version("v1alpha1")` or similar version identifiers following Kubernetes conventions. -Users will reference these versioned plugins in configuration using the format `PluginName/version`. -The proxy will validate at configuration parse time that the referenced version matches the plugin's declared version, failing fast with a clear error message if they diverge. -Unversioned plugins and unversioned references will continue to work to maintain backward compatibility with existing deployments. +Plugin authors will annotate their implementations with multiple `@Plugin` annotations, each specifying a different `configVersion` and corresponding `configType`. +The existing `@Plugin` annotation becomes `@Repeatable`, allowing a single implementation class to support multiple configuration schema versions simultaneously. +Users will specify which configuration schema version they are using via a separate `version` field in their YAML configuration. +The proxy will validate at configuration parse time that the referenced version matches one of the plugin's declared configuration versions, failing fast with a clear error message if no match is found. -For proxy users, configuration syntax extends from `type: RecordEncryption` to support `type: RecordEncryption/v1alpha1`. -When a version is present in the type string, the proxy uses it both for validation and for name resolution. -A plugin named RecordEncryption annotated with `@Version("v1alpha1")` can be referenced as either `RecordEncryption/v1alpha1` (explicit version) or `RecordEncryption` (implicit, triggers warning). -If multiple RecordEncryption implementations exist with different versions (v1alpha1 and v1beta1), users disambiguate by including the version rather than switching to fully qualified class names. +For proxy users, configuration syntax extends from: +```yaml +type: RecordEncryption +config: + # legacy config fields +``` + +to support an explicit `version` field: +```yaml +type: RecordEncryption +version: v1alpha1 +config: + # v1alpha1 config fields +``` + +When a version is present, the proxy uses it to select which `@Plugin(configVersion = "...", configType = ...)` annotation to use for deserialization. +When no version is specified, the proxy uses the legacy configuration (the `@Plugin` annotation with an empty `configVersion`). Version validation ensures that configuration matches plugin implementation. -If a user references `RecordEncryption/v1alpha1` but the plugin declares `@Version("v1beta1")`, the proxy rejects the configuration during parsing with an error message identifying the mismatch. -If a user references `RecordEncryption/v1alpha1` but the plugin has no `@Version` annotation, the proxy similarly rejects the configuration. +If a user specifies `version: v1alpha1` but the plugin has no `@Plugin(configVersion = "v1alpha1", ...)` annotation, the proxy rejects the configuration during parsing with an error message identifying the mismatch. This fail-fast behavior prevents runtime surprises from version mismatches. The enforcement policy evolves across Kroxylicious releases. -In versions before 1.0.0, version annotations are optional and version references in configuration are optional. -When a plugin declares a version but the configuration omits it, the proxy logs a warning but continues. -This allows plugin authors to begin adopting version annotations while maintaining compatibility with existing configurations. -In 1.0.0 and later, when a plugin declares a version, configurations must include that version. +In versions before 1.0.0, version annotations are optional and the `version` field in configuration is optional. +When a plugin declares versioned configurations but the user's configuration omits the `version` field, the proxy logs a warning but continues using the legacy configuration. +This allows plugin authors to begin adopting versioned configurations while maintaining compatibility with existing deployments. +In 1.0.0 and later, when a plugin declares versioned configurations, user configurations must include an explicit `version` field. The proxy rejects configurations that omit required versions. An environment variable `KROXYLICIOUS_REQUIRE_PLUGIN_VERSIONS` allows operators to opt into strict enforcement before 1.0.0 for testing purposes. -For filter authors and plugin developers, the `@Version` annotation becomes part of the public API contract. -Placing `@Version("v1alpha1")` on a filter implementation signals to users that the API is experimental and subject to breaking changes. -Transitioning from v1alpha1 to v1beta1 may involve incompatible configuration changes, but users explicitly opt into the new version by updating their configuration. -Plugin authors can ship both versions in the same JAR during migration periods, allowing gradual rollout. -When a plugin reaches stability, annotating it with `@Version("v1")` communicates that the API will remain backward compatible within the v1 series. +For filter authors and plugin developers, adding a versioned `@Plugin` annotation signals the configuration schema's maturity. +Annotating with `@Plugin(configVersion = "v1alpha1", configType = RecordEncryptionConfigV1Alpha1.class)` communicates that the configuration is experimental and subject to breaking changes. +Transitioning from v1alpha1 to v1beta1 involves adding another `@Plugin` annotation with a new config type, but users explicitly opt into the new version by updating their `version` field. +Plugin authors can support multiple versions in the same implementation during migration periods, allowing gradual rollout. +When a plugin's configuration reaches stability, annotating it with `@Plugin(configVersion = "v1", ...)` communicates that the configuration schema will remain backward compatible within the v1 series. -Version-based disambiguation enables new packaging strategies. -A plugin author maintaining both alpha and beta implementations can ship `io.kroxylicious.filter.encryption.alpha.RecordEncryption` annotated with `@Version("v1alpha1")` and `io.kroxylicious.filter.encryption.beta.RecordEncryption` annotated with `@Version("v1beta1")` in the same JAR. -Users running configurations with `type: RecordEncryption/v1alpha1` receive the alpha implementation while users with `type: RecordEncryption/v1beta1` receive the beta implementation. -This eliminates the need for separate JARs or complex classloader isolation to support parallel versions. +The instanceof dispatch pattern enables a single implementation class to handle multiple configuration versions. +A plugin's `initialize()` method receives the configuration object and uses `instanceof` pattern matching to determine which version was provided: -Nested plugin references inherit the same versioning behavior. -When a RecordEncryption filter configuration references a KmsService, that reference can include a version: `kms: VaultKmsService/v1alpha1`. -The same parsing and validation logic applies recursively to all plugin references in the configuration tree, ensuring consistency throughout. +```java +public SharedEncryptionContext initialize( + FilterFactoryContext context, Object config) { + var configuration = Plugins.requireConfig(this, config); + if (configuration instanceof RecordEncryptionConfigV1Alpha1 v1a1) { + return initializeV1Alpha1(context, v1a1); + } + else if (configuration instanceof RecordEncryptionConfig legacy) { + return initializeLegacy(context, legacy); + } + throw new PluginConfigurationException("Unsupported config version"); +} +``` + +This maintains full backward compatibility while allowing controlled evolution. + +Nested plugin references in the legacy format continue to work as they do today. +Versioned configuration schemas that need to reference other plugins will use the patterns introduced in PR #96 (`HasPluginReferences` interface). +This proposal does not change how nested plugins are referenced in the legacy single-file format. ### Public API changes -A new `@Version` annotation will be added to `io.kroxylicious.proxy.plugin` in the kroxylicious-api module: +The existing `@Plugin` annotation in `io.kroxylicious.proxy.plugin` will be extended with a `configVersion` attribute and made `@Repeatable`: ```java +@Repeatable(Plugins.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -public @interface Version { +public @interface Plugin { + /** + * The configuration type for this plugin. + */ + Class configType(); + /** - * The API version identifier for this plugin implementation. - * Should follow Kubernetes-style versioning: v1alpha1, v1beta1, v1, etc. + * The configuration schema version identifier. + * Empty string (default) indicates the legacy unversioned configuration. + * Non-empty values should follow Kubernetes-style versioning: v1alpha1, v1beta1, v1, etc. */ - String value(); + String configVersion() default ""; } ``` -A new `VersionMismatchException` will be added to `io.kroxylicious.proxy.plugin` in the kroxylicious-api module: +A container annotation is required for `@Repeatable`: ```java -public class VersionMismatchException extends RuntimeException { - public VersionMismatchException(String message) { - super(message); - } +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Plugins { + Plugin[] value(); } ``` -This exception is thrown when configuration version references do not match plugin version declarations. +Example usage for a plugin supporting multiple configuration versions: + +```java +@Plugin(configType = RecordEncryptionConfig.class) // legacy, configVersion = "" +@Plugin(configVersion = "v1alpha1", configType = RecordEncryptionConfigV1Alpha1.class) +public class RecordEncryption implements FilterFactory> { +``` + +**Design compromise: loss of compile-time type safety.** When a plugin supports multiple configuration versions, the type parameter for the plugin interface (e.g., `FilterFactory`) must be `Object` instead of a specific configuration type. This is unavoidable because multiple config versions form a union type which Java's generics cannot express directly. The cost is mitigated by the `Plugins.requireConfig()` helper (which validates the config type at runtime) and by `instanceof` pattern matching in `initialize()`. + +A new helper method will be added to the `Plugins` utility class in `io.kroxylicious.proxy.plugin`: + +```java +public final class Plugins { + /** + * Type-checks and casts a configuration object for a plugin that supports multiple + * configuration versions. Returns the configuration if it matches one of the plugin's + * declared config types, otherwise throws PluginConfigurationException. + * + * @param plugin the plugin instance + * @param config the configuration object to validate + * @return the configuration object, validated + * @throws PluginConfigurationException if config type doesn't match any declared config type + */ + public static Object requireConfig(Object plugin, Object config) { + // implementation validates against all @Plugin annotations on plugin.getClass() + } +} +``` -The `PluginFactory` interface in kroxylicious-runtime will gain a new method: +A new `VersionMismatchException` will be added to `io.kroxylicious.proxy.plugin`: ```java -@Nullable -String pluginVersion(String instanceName); +public class VersionMismatchException extends RuntimeException { + public VersionMismatchException(String message) { + super(message); + } +} ``` -This method returns the version from a plugin's `@Version` annotation or null if the plugin is not annotated. -It follows the same signature pattern as the existing `configType(String instanceName)` method and throws `UnknownPluginInstanceException` if the instance name cannot be resolved. +This exception is thrown when a configuration specifies a `version` that does not match any `@Plugin(configVersion = "...")` annotation on the plugin implementation. -No changes are required to existing filter APIs or configuration classes. -The `@PluginImplName` and `@PluginImplConfig` annotations already support arbitrary string values for the type field, so the slash-separated version syntax works without modification to the annotation processing infrastructure. +No changes are required to existing configuration classes that use `@PluginImplName` and `@PluginImplConfig`. +Those annotations continue to work as they do today for legacy configurations. ### Migration plan @@ -117,36 +182,76 @@ The migration proceeds in two phases aligned with Kroxylicious release versions. Phase 1 ships in version 0.22.0 or similar pre-1.0 releases and continues until 1.0.0. -- All version-related infrastructure is implemented: the `@Version` annotation, the parsing logic, the validation logic, and the enforcement policy. +- All version-related infrastructure is implemented: the `@Plugin(configVersion = "...")` attribute, `@Repeatable` support, the parsing logic for the `version` field, validation logic, and the enforcement policy. - Enforcement defaults to warning mode. -- Built-in plugins receive `@Version` annotations indicating their stability level (e.g., `@Version("v1alpha1")` for experimental plugins like Record Encryption, `@Version("v1beta1")` or `@Version("v1")` for more mature filters). -- Documentation and examples are updated to show versioned plugin references. -- When users reference a plugin without including the version, and the plugin has a `@Version` annotation, the proxy logs a warning indicating that explicit versions will be required in 1.0.0. -- Example warning: "Plugin 'RecordEncryption' declares version 'v1alpha1' but config does not specify it. Update config to: type: RecordEncryption/v1alpha1 (required in 1.0.0+)". -- Plugin authors can ship multiple API versions in the same JAR for migration purposes. -- Users are encouraged to update configurations to include explicit versions to prepare for 1.0.0. -- All existing configurations continue to work without modification. +- Built-in plugins receive versioned `@Plugin` annotations indicating their configuration schema stability level (e.g., `@Plugin(configVersion = "v1alpha1", configType = RecordEncryptionConfigV1Alpha1.class)` for experimental configurations). +- Documentation and examples are updated to show the `version` field in configuration. +- When users reference a plugin that declares versioned configurations but the configuration omits the `version` field, the proxy logs a warning indicating that explicit versions will be required in 1.0.0. +- Example warning: "Plugin 'RecordEncryption' declares versioned configurations but config does not specify version. Add 'version: v1alpha1' field (required in 1.0.0+)". +- Plugin authors can support multiple configuration versions via multiple `@Plugin` annotations with different `configVersion` values, allowing migration periods. +- Users are encouraged to add explicit `version` fields to prepare for 1.0.0. +- All existing configurations continue to work without modification (they use the legacy `@Plugin` annotation with `configVersion = ""`). - The `KROXYLICIOUS_REQUIRE_PLUGIN_VERSIONS` environment variable allows operators to opt into strict enforcement before 1.0.0 by setting it to `true`, treating missing versions as errors rather than warnings for testing purposes (particularly useful in CI pipelines and staging environments). #### Phase 2: Enforcement (1.0.0+) Phase 2 begins with the 1.0.0 release. -- The enforcement policy changes from warning to error when a plugin declares a version but the configuration omits it. +- The enforcement policy changes from warning to error when a plugin declares versioned configurations but the configuration omits the `version` field. - Configurations without explicit versions fail during parsing with `VersionMismatchException`. - This breaking change is acceptable at a major version boundary and has been signaled through warnings in all prior releases. - Users who updated their configurations in response to warnings experience no disruption. -- Users who ignored warnings must add version suffixes to plugin type references before upgrading to 1.0.0. -- The migration path is clear and mechanical: each warning message indicates exactly which version string to add. +- Users who ignored warnings must add `version` fields to their plugin configurations before upgrading to 1.0.0. +- The migration path is clear and mechanical: each warning message indicates exactly which version to specify. + +### Scope: What this proposal includes + +This proposal is tightly focused on establishing the versioning mechanism: + +1. **`@Plugin(configVersion = "...")` attribute**: Extending the existing annotation to support multiple configuration versions via `@Repeatable`. +2. **`version` field in YAML**: A separate field alongside `type` and `config` in the existing single-file format. +3. **instanceof dispatch pattern**: Plugin implementations use `instanceof` to determine which configuration version they received. +4. **Version validation**: Runtime checks that the `version` field matches a declared `@Plugin(configVersion = "...")`. +5. **Phased enforcement**: Warnings pre-1.0.0, errors in 1.0.0+, with `KROXYLICIOUS_REQUIRE_PLUGIN_VERSIONS` for early opt-in. +6. **Migration periods**: Supporting multiple configuration versions in one plugin implementation class. + +The `version` field works within the existing single-file configuration format. Name resolution for plugins remains unchanged (simple names when unambiguous, FQCNs when ambiguous). + +### Scope: What is deferred to PR #96 + +PR #96 (Config2: Multi-file plugin configuration) will build on this versioning foundation with: + +- Multi-file `plugins.d/` layout with one file per plugin instance +- `PluginReference` and `HasPluginReferences` for explicit dependency graphs +- Dependency graph validation (cycle detection, referential integrity) +- JSON Schema validation for versioned configurations +- `@Stateless` annotation and shared plugin instances +- `ResolvedPluginRegistry` for dependency injection +- `Snapshot` abstraction and change detection (generation numbers) +- Binary resource support (`@ResourceType`) and out-of-band passwords +- FQCN requirement for `type` field in multi-file format +- Migration tool from single-file to multi-file format + +All mechanisms in this proposal carry forward to PR #96 without changes: +- Same `@Plugin(configVersion = "...")` semantics +- Same `version` field name and position +- Same `instanceof` dispatch pattern +- Same validation logic +- Same enforcement timeline + +Users will transition from single-file configs with `version` fields (this proposal) to multi-file configs (PR #96) at their own pace. Both formats will coexist. ## Affected/not affected projects ### Affected The kroxylicious repository is affected. -Changes span the kroxylicious-api module (new annotation and exception types), the kroxylicious-runtime module (parsing, validation, and registration logic), and potentially filter implementation modules (adding `@Version` annotations). -The kroxylicious-kms module is affected as KMS provider plugins will need version annotations. -The kroxylicious-authorizer-api module is affected as authorizer plugins will need version annotations. +Changes span: +- **kroxylicious-api**: Extending `@Plugin` with `configVersion` attribute, making it `@Repeatable`, adding `Plugins` container annotation, adding `VersionMismatchException`, extending `Plugins` utility class with `requireConfig()`. +- **kroxylicious-runtime**: Parsing logic for `version` field in YAML, version validation logic, enforcement policy implementation. +- **Filter modules**: Optionally adding versioned `@Plugin` annotations to introduce versioned configuration schemas. Existing filters continue to work with only the legacy `@Plugin` annotation. +- **kroxylicious-kms**: KMS providers can optionally add versioned configurations. +- **kroxylicious-authorizer-api**: Authorizers can optionally add versioned configurations. ### Not affected @@ -157,25 +262,37 @@ The Kubernetes CRD schema already permits arbitrary strings in plugin type field ## Compatibility Before 1.0.0, all changes are backward compatible. -Existing configurations with unversioned plugin references continue to work. -Existing plugins without `@Version` annotations continue to work. -The only behavioral change is the addition of warning messages when versioned plugins are referenced without versions, and these warnings do not prevent startup or operation. +Existing configurations without `version` fields continue to work, using the legacy configuration schema (the `@Plugin` annotation with `configVersion = ""`). +Existing plugins with only a single `@Plugin` annotation continue to work unchanged. +The only behavioral change is the addition of warning messages when a plugin declares versioned configurations but the user's configuration omits the `version` field. These warnings do not prevent startup or operation. -At 1.0.0, the change becomes intentionally breaking for configurations that reference versioned plugins without including the version. +At 1.0.0, the change becomes intentionally breaking for configurations that reference plugins with versioned configurations but omit the `version` field. This is acceptable at a major version boundary and follows semantic versioning principles. The breakage is limited to configurations that ignored warnings in prior releases. -Configurations that never referenced versioned plugins (because all their plugins remained unversioned) continue to work without modification even at 1.0.0. - -For plugin developers, adding a `@Version` annotation is a one-way commitment. -Once a plugin is annotated with a version, removing the annotation in a future release would break any configurations that include the version string. -Plugin authors should carefully consider API stability before adding version annotations. -The conventional approach is to start with v1alpha1 for experimental APIs and progress through v1beta1 to v1 as the API stabilizes. +Configurations for plugins that never added versioned `@Plugin` annotations (remaining with only the legacy annotation) continue to work without modification even at 1.0.0. + +For plugin developers, adding a versioned `@Plugin` annotation is a commitment to support that configuration schema version. +Once a plugin declares `@Plugin(configVersion = "v1alpha1", ...)`, removing support for that version in a future release would break any configurations specifying `version: v1alpha1`. +The conventional approach is to: +1. Start with v1alpha1 for experimental configuration schemas +2. Progress through v1beta1 as the schema stabilizes +3. Reach v1 for stable schemas with backward compatibility guarantees +4. Support multiple versions during migration periods via multiple `@Plugin` annotations + +The Kubernetes CRD versioning approach provides precedent for version transitions: +- Moving from v1alpha1 to v1beta1 may involve breaking changes to configuration structure +- Moving from v1beta1 to v1 should avoid breaking changes where possible but may make incompatible changes if necessary +- Moving from v1 to v2 is a major version transition requiring careful migration planning + +During migration periods, plugin authors can support both old and new versions: +```java +@Plugin(configType = RecordEncryptionConfig.class) // legacy +@Plugin(configVersion = "v1alpha1", configType = RecordEncryptionConfigV1Alpha1.class) +@Plugin(configVersion = "v1beta1", configType = RecordEncryptionConfigV1Beta1.class) +public class RecordEncryption implements FilterFactory> +``` -The Kubernetes CRD versioning approach provides precedent for version transitions. -Moving from v1alpha1 to v1beta1 may involve breaking changes to configuration structure. -Moving from v1beta1 to v1 should avoid breaking changes where possible but may make incompatible changes if necessary. -Moving from v1 to v2 is a major version transition requiring careful migration planning. -Kroxylicious adopts these same conventions, relying on users to understand the stability implications of each version level. +Users migrate by adding the `version` field and updating their config structure to match the new schema. ## Rejected alternatives @@ -184,35 +301,55 @@ Kroxylicious adopts these same conventions, relying on users to understand the s Using semantic version strings (1.0.0, 1.1.0, 2.0.0) instead of Kubernetes-style versions was considered. Semantic versioning provides more granular version information and is familiar to Java developers. However, Kubernetes-style versioning has significant advantages. -The alpha/beta/stable progression clearly communicates API maturity in a way that semantic versions do not. -A plugin at version 0.5.0 might be quite stable or highly experimental; the version number alone does not convey this. -A plugin at version v1alpha1 unambiguously signals experimental status. +The alpha/beta/stable progression clearly communicates configuration schema maturity in a way that semantic versions do not. +A configuration schema at version 0.5.0 might be quite stable or highly experimental; the version number alone does not convey this. +A configuration schema at version v1alpha1 unambiguously signals experimental status. Kubernetes-style versioning is already used in the kroxylicious-kubernetes-api module for CRDs, so adopting it for plugin configuration maintains consistency across the project. -### Version as separate configuration field +### Composite version string (original proposal approach) + +The original version of this proposal used a composite type string like `type: RecordEncryption/v1alpha1` instead of separate `type` and `version` fields. +This was rejected to align with PR #96's approach, which uses separate fields to match Kubernetes resource conventions where `kind` and `apiVersion` are distinct. +The separate field approach also: +- Provides clearer separation between plugin identity and configuration version +- Simplifies parsing (no need to split on `/` and handle escaping) +- Enables better validation (version is always in a known location) +- Aligns with the multi-file format in PR #96 where metadata (name, type, version) is cleanly separated from config content + +### Separate @Version annotation (original proposal approach) + +The original version of this proposal introduced a new `@Version` annotation separate from `@Plugin`. +This was rejected in favor of extending the existing `@Plugin` annotation with a `configVersion` attribute because: +- It reduces API surface area (one annotation instead of two) +- It keeps version information co-located with the config type it describes +- It makes the relationship between version and config type explicit and type-safe +- It aligns with PR #96's multi-version design, where each version maps to a specific config type +- The `@Repeatable` mechanism naturally supports multiple versions + +### Version for disambiguation instead of configuration schema versioning -Representing version as a separate field in configuration rather than as part of the type string was considered. -The configuration might look like `type: RecordEncryption` and `version: v1alpha1` as sibling fields. -This approach provides clearer separation between plugin identity and version, potentially simplifying parsing. -However, it increases configuration verbosity, requiring two fields instead of one. -It also diverges from patterns like Kubernetes resource definitions where apiVersion is a single field combining API group and version (e.g., apps/v1). -The composite type string approach is more concise and aligns with Kubernetes conventions. +The original proposal treated version as a disambiguation mechanism when multiple plugin implementations share the same simple name. +This was rejected because: +- Version should identify the configuration schema, not the implementation +- Name disambiguation is already solved by FQCNs (and will be required in PR #96's multi-file format) +- Conflating version with implementation identity creates confusion about what version means +- Configuration schema version is the more important concept for users ### Plugin version compatibility checking Implementing version compatibility checking rather than exact version matching was considered. -The proxy might accept a configuration referencing v1alpha1 when the plugin implements v1alpha2 if the versions are compatible according to some policy. +The proxy might accept a configuration with `version: v1alpha1` when the plugin only declares `v1alpha2` if the versions are compatible according to some policy. This would provide more flexibility during upgrades. However, defining compatibility rules for alpha and beta versions is difficult because those version levels explicitly allow breaking changes. For stable v1 versions, semantic versioning compatibility rules could apply, but mixing compatibility rules across version levels creates complexity. The exact-match approach is simpler, more predictable, and encourages users to make deliberate version upgrade decisions. -When a plugin author releases v1alpha2, configurations must explicitly opt into the new version, ensuring users are aware of potential breaking changes. +When a plugin author releases v1alpha2, configurations must explicitly update to `version: v1alpha2`, ensuring users are aware of potential breaking changes. ### Automatic version migration Providing automatic migration from v1alpha1 to v1beta1 through configuration transformations was considered. -The proxy might detect a v1alpha1 configuration and automatically transform it to v1beta1 format according to plugin-provided migration rules. +The proxy might detect `version: v1alpha1` and automatically transform it to v1beta1 format according to plugin-provided migration rules. This would ease version transitions but adds significant complexity to the configuration system. Migration rules would need to be discoverable, machine-readable, and composable across multiple plugin upgrades. The approach also obscures what version is actually in use, potentially causing confusion. -Explicit version references in configuration make the actual plugin version transparent and keep migration responsibility with the configuration owner rather than embedding it in proxy logic. +Explicit version references in configuration make the actual configuration schema version transparent and keep migration responsibility with the configuration owner rather than embedding it in proxy logic.