From 72bd07047c355830a4a87d38591bb7bad8c74eee Mon Sep 17 00:00:00 2001 From: Keith Wall Date: Tue, 28 Nov 2023 12:09:42 +0100 Subject: [PATCH] Tease out simple transform filters into their own module (#727) * Tease out kroxylicious-simple-transform to separate module moving the filters from io.kroxylicious.proxy.internal.filter to io.kroxylicious.proxy.filter.simpletransform; Signed-off-by: kwall --- CHANGELOG.md | 8 +- Dockerfile | 2 +- .../proxy/filter/FilterFactory.java | 2 +- kroxylicious-app/pom.xml | 17 ++ kroxylicious-bom/pom.xml | 6 + .../kroxylicious-simple-transform/pom.xml | 70 +++++++ .../ByteBufferTransformation.java | 2 +- .../ByteBufferTransformationFactory.java | 2 +- .../FetchResponseTransformationFilter.java | 32 ++-- ...chResponseTransformationFilterFactory.java | 4 +- .../ProduceRequestTransformationFilter.java | 27 +-- ...uceRequestTransformationFilterFactory.java | 4 +- .../filter/simpletransform}/UpperCasing.java | 2 +- ...io.kroxylicious.proxy.filter.FilterFactory | 2 + ...etransform.ByteBufferTransformationFactory | 7 + ...TransformationFilterFactoryFilterTest.java | 9 +- ...TransformationFilterFactoryFilterTest.java | 171 ++++++++++++++++++ kroxylicious-filters/pom.xml | 1 + kroxylicious-integration-test-support/pom.xml | 2 +- .../proxy/config/ConfigurationTest.java | 18 +- .../proxy/config/ExampleFilterFactory.java | 42 +++++ .../proxy/config/ExamplePluginFactory.java | 29 +++ .../proxy/config/ExamplePluginInstance.java | 23 +++ ...ylicious.proxy.config.ExamplePluginFactory | 2 +- ...io.kroxylicious.proxy.filter.FilterFactory | 6 + kroxylicious-integration-tests/pom.xml | 5 + .../java/io/kroxylicious/proxy/FilterIT.java | 4 +- .../proxy/TestDecoderFactory.java | 6 +- .../proxy/TestEncoderFactory.java | 4 +- ...transform.ByteBufferTransformationFactory} | 0 kroxylicious-runtime/pom.xml | 13 -- ...io.kroxylicious.proxy.filter.FilterFactory | 2 - .../io/kroxylicious/proxy/KafkaProxyTest.java | 27 ++- .../proxy/config/ConfigParserTest.java | 43 ++--- .../internal/filter/ExamplePluginFactory.java | 17 ++ .../filter/ExamplePluginInstance.java | 23 +++ .../proxy/internal/filter/FilterTest.java | 26 +-- .../filter/NestedPluginConfigFactory.java | 42 +++++ .../filter/OptionalConfigFactory.java | 2 + .../filter/RequiresConfigFactory.java | 2 + .../internal/filter/TestFilterFactory.java | 2 + ...io.kroxylicious.proxy.filter.FilterFactory | 8 +- ...proxy.internal.filter.ExamplePluginFactory | 6 + 43 files changed, 594 insertions(+), 128 deletions(-) create mode 100644 kroxylicious-filters/kroxylicious-simple-transform/pom.xml rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/ByteBufferTransformation.java (87%) rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/ByteBufferTransformationFactory.java (94%) rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/FetchResponseTransformationFilter.java (80%) rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/FetchResponseTransformationFilterFactory.java (91%) rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/ProduceRequestTransformationFilter.java (66%) rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/ProduceRequestTransformationFilterFactory.java (92%) rename {kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform}/UpperCasing.java (97%) create mode 100644 kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory create mode 100644 kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory rename {kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter => kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform}/FetchResponseTransformationFilterFactoryFilterTest.java (97%) create mode 100644 kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java create mode 100644 kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java create mode 100644 kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java create mode 100644 kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java rename kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory => kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory (75%) create mode 100644 kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory rename kroxylicious-integration-tests/src/test/resources/META-INF/services/{io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory => io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory} (100%) delete mode 100644 kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory create mode 100644 kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java create mode 100644 kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java create mode 100644 kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java create mode 100644 kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory diff --git a/CHANGELOG.md b/CHANGELOG.md index 774c08c400..9b0948f7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,19 @@ Please enumerate **all user-facing** changes using format ` { * This method may provide extra semantic validation of the config, * and returns some object (which may be the config, or some other object) which will be passed to {@link #createFilter(FilterFactoryContext, Object)} * - * @param context + * @param context context * @param config configuration * @throws PluginConfigurationException when the configuration is invalid */ diff --git a/kroxylicious-app/pom.xml b/kroxylicious-app/pom.xml index 2d47a2edd0..5647e79907 100644 --- a/kroxylicious-app/pom.xml +++ b/kroxylicious-app/pom.xml @@ -195,5 +195,22 @@ + + withAdditionalFilters + + + io.kroxylicious + kroxylicious-multitenant + + + io.kroxylicious + kroxylicious-record-validation + + + io.kroxylicious + kroxylicious-simple-transform + + + diff --git a/kroxylicious-bom/pom.xml b/kroxylicious-bom/pom.xml index 54d7cb9629..7f6ef5bcff 100644 --- a/kroxylicious-bom/pom.xml +++ b/kroxylicious-bom/pom.xml @@ -135,6 +135,12 @@ ${project.version} + + io.kroxylicious + kroxylicious-simple-transform + ${project.version} + + diff --git a/kroxylicious-filters/kroxylicious-simple-transform/pom.xml b/kroxylicious-filters/kroxylicious-simple-transform/pom.xml new file mode 100644 index 0000000000..0031cdceed --- /dev/null +++ b/kroxylicious-filters/kroxylicious-simple-transform/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + io.kroxylicious + kroxylicious-parent + 0.4.0-SNAPSHOT + ../../pom.xml + + + kroxylicious-simple-transform + Simple filters + Several simple filters that exist for test purposes. + + + + + io.kroxylicious + kroxylicious-api + + + + + io.kroxylicious + kroxylicious-filter-test-support + test + + + + + com.github.spotbugs + spotbugs-annotations + + + org.assertj + assertj-core + test + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + \ No newline at end of file diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformation.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformation.java similarity index 87% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformation.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformation.java index aa75e0d556..c7035b264c 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformation.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformation.java @@ -3,7 +3,7 @@ * * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; import java.nio.ByteBuffer; diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformationFactory.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformationFactory.java similarity index 94% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformationFactory.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformationFactory.java index cc8be6b4d3..f8b8de84f7 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ByteBufferTransformationFactory.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ByteBufferTransformationFactory.java @@ -4,7 +4,7 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; import io.kroxylicious.proxy.plugin.PluginConfigurationException; diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilter.java similarity index 80% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilter.java index 8b08f5c6b8..b593c76d80 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilter.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilter.java @@ -4,9 +4,8 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; @@ -24,6 +23,7 @@ import org.apache.kafka.common.record.MemoryRecordsBuilder; import org.apache.kafka.common.record.MutableRecordBatch; import org.apache.kafka.common.record.Record; +import org.apache.kafka.common.record.RecordBatch; import org.apache.kafka.common.record.TimestampType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,11 +31,12 @@ import io.kroxylicious.proxy.filter.FetchResponseFilter; import io.kroxylicious.proxy.filter.FilterContext; import io.kroxylicious.proxy.filter.ResponseFilterResult; -import io.kroxylicious.proxy.internal.util.MemoryRecordsHelper; /** * A filter for modifying the key/value/header/topic of {@link ApiKeys#FETCH} responses. - */ + *

+ * Not intended to production use. + *

*/ public class FetchResponseTransformationFilter implements FetchResponseFilter { // Version 12 was the first version that uses topic ids. @@ -93,18 +94,21 @@ public CompletionStage onFetchResponse(short apiVersion, R private void applyTransformation(FilterContext context, FetchResponseData responseData) { for (FetchResponseData.FetchableTopicResponse topicData : responseData.responses()) { for (FetchResponseData.PartitionData partitionData : topicData.partitions()) { - MemoryRecords records = (MemoryRecords) partitionData.records(); - MemoryRecordsBuilder newRecords = MemoryRecordsHelper.builder(context.createByteBufferOutputStream(records.sizeInBytes()), CompressionType.NONE, - TimestampType.CREATE_TIME, 0); - - for (MutableRecordBatch batch : records.batches()) { - for (Iterator batchRecords = batch.iterator(); batchRecords.hasNext();) { - Record batchRecord = batchRecords.next(); - newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.topic(), batchRecord.value())); + var records = (MemoryRecords) partitionData.records(); + var stream = context.createByteBufferOutputStream(records.sizeInBytes()); + try (var newRecords = new MemoryRecordsBuilder(stream, RecordBatch.CURRENT_MAGIC_VALUE, CompressionType.NONE, TimestampType.CREATE_TIME, 0, + System.currentTimeMillis(), RecordBatch.NO_PRODUCER_ID, RecordBatch.NO_PRODUCER_EPOCH, RecordBatch.NO_SEQUENCE, false, false, + RecordBatch.NO_PARTITION_LEADER_EPOCH, + stream.remaining())) { + + for (MutableRecordBatch batch : records.batches()) { + for (Record batchRecord : batch) { + newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.topic(), batchRecord.value())); + } } - } - partitionData.setRecords(newRecords.build()); + partitionData.setRecords(newRecords.build()); + } } } } diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactory.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactory.java similarity index 91% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactory.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactory.java index ffa16300b6..9269b122cd 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactory.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactory.java @@ -4,7 +4,7 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; import java.util.Objects; @@ -12,7 +12,7 @@ import io.kroxylicious.proxy.filter.FilterFactory; import io.kroxylicious.proxy.filter.FilterFactoryContext; -import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory.Config; +import io.kroxylicious.proxy.filter.simpletransform.FetchResponseTransformationFilterFactory.Config; import io.kroxylicious.proxy.plugin.Plugin; import io.kroxylicious.proxy.plugin.PluginImplConfig; import io.kroxylicious.proxy.plugin.PluginImplName; diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilter.java similarity index 66% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilter.java index 700df30c69..06604001af 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilter.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilter.java @@ -4,9 +4,8 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; -import java.util.Iterator; import java.util.concurrent.CompletionStage; import org.apache.kafka.common.message.ProduceRequestData; @@ -17,15 +16,18 @@ import org.apache.kafka.common.record.MemoryRecordsBuilder; import org.apache.kafka.common.record.MutableRecordBatch; import org.apache.kafka.common.record.Record; +import org.apache.kafka.common.record.RecordBatch; import org.apache.kafka.common.record.TimestampType; import io.kroxylicious.proxy.filter.FilterContext; import io.kroxylicious.proxy.filter.ProduceRequestFilter; import io.kroxylicious.proxy.filter.RequestFilterResult; -import io.kroxylicious.proxy.internal.util.MemoryRecordsHelper; /** * A filter for modifying the key/value/header/topic of {@link ApiKeys#PRODUCE} requests. + *

+ * Not intended to production use. + *

*/ public class ProduceRequestTransformationFilter implements ProduceRequestFilter { @@ -50,17 +52,20 @@ private void applyTransformation(FilterContext ctx, ProduceRequestData req) { req.topicData().forEach(topicData -> { for (ProduceRequestData.PartitionProduceData partitionData : topicData.partitionData()) { MemoryRecords records = (MemoryRecords) partitionData.records(); - MemoryRecordsBuilder newRecords = MemoryRecordsHelper.builder(ctx.createByteBufferOutputStream(records.sizeInBytes()), CompressionType.NONE, - TimestampType.CREATE_TIME, 0); + var stream = ctx.createByteBufferOutputStream(records.sizeInBytes()); + try (var newRecords = new MemoryRecordsBuilder(stream, RecordBatch.CURRENT_MAGIC_VALUE, CompressionType.NONE, TimestampType.CREATE_TIME, 0, + System.currentTimeMillis(), RecordBatch.NO_PRODUCER_ID, RecordBatch.NO_PRODUCER_EPOCH, RecordBatch.NO_SEQUENCE, false, false, + RecordBatch.NO_PARTITION_LEADER_EPOCH, + stream.remaining())) { - for (MutableRecordBatch batch : records.batches()) { - for (Iterator batchRecords = batch.iterator(); batchRecords.hasNext();) { - Record batchRecord = batchRecords.next(); - newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.name(), batchRecord.value())); + for (MutableRecordBatch batch : records.batches()) { + for (Record batchRecord : batch) { + newRecords.append(batchRecord.timestamp(), batchRecord.key(), valueTransformation.transform(topicData.name(), batchRecord.value())); + } } - } - partitionData.setRecords(newRecords.build()); + partitionData.setRecords(newRecords.build()); + } } }); } diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilterFactory.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactory.java similarity index 92% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilterFactory.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactory.java index 2dbcc5aeab..04fe598080 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/ProduceRequestTransformationFilterFactory.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactory.java @@ -4,13 +4,13 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; import com.fasterxml.jackson.annotation.JsonProperty; import io.kroxylicious.proxy.filter.FilterFactory; import io.kroxylicious.proxy.filter.FilterFactoryContext; -import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory.Config; +import io.kroxylicious.proxy.filter.simpletransform.ProduceRequestTransformationFilterFactory.Config; import io.kroxylicious.proxy.plugin.Plugin; import io.kroxylicious.proxy.plugin.PluginImplConfig; import io.kroxylicious.proxy.plugin.PluginImplName; diff --git a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/UpperCasing.java b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/UpperCasing.java similarity index 97% rename from kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/UpperCasing.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/UpperCasing.java index 51c393fe01..3ac04fadc8 100644 --- a/kroxylicious-runtime/src/main/java/io/kroxylicious/proxy/internal/filter/UpperCasing.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/java/io/kroxylicious/proxy/filter/simpletransform/UpperCasing.java @@ -4,7 +4,7 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; import java.nio.ByteBuffer; import java.nio.charset.Charset; diff --git a/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory new file mode 100644 index 0000000000..d4d60bb89c --- /dev/null +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory @@ -0,0 +1,2 @@ +io.kroxylicious.proxy.filter.simpletransform.ProduceRequestTransformationFilterFactory +io.kroxylicious.proxy.filter.simpletransform.FetchResponseTransformationFilterFactory diff --git a/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory new file mode 100644 index 0000000000..f69fc666ee --- /dev/null +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory @@ -0,0 +1,7 @@ +# +# Copyright Kroxylicious Authors. +# +# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 +# + +io.kroxylicious.proxy.filter.simpletransform.UpperCasing \ No newline at end of file diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactoryFilterTest.java b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactoryFilterTest.java similarity index 97% rename from kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactoryFilterTest.java rename to kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactoryFilterTest.java index 7958d43525..c4fa017dc3 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FetchResponseTransformationFilterFactoryFilterTest.java +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/FetchResponseTransformationFilterFactoryFilterTest.java @@ -4,7 +4,7 @@ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 */ -package io.kroxylicious.proxy.internal.filter; +package io.kroxylicious.proxy.filter.simpletransform; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -38,7 +38,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; @@ -47,7 +46,6 @@ import io.kroxylicious.proxy.filter.ResponseFilterResult; import io.kroxylicious.proxy.filter.ResponseFilterResultBuilder; import io.kroxylicious.proxy.filter.filterresultbuilder.CloseOrTerminalStage; -import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory.Config; import io.kroxylicious.proxy.plugin.PluginConfigurationException; import edu.umd.cs.findbugs.annotations.NonNull; @@ -57,6 +55,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -116,9 +115,9 @@ void testFactory() { FetchResponseTransformationFilterFactory factory = new FetchResponseTransformationFilterFactory(); assertThatThrownBy(() -> factory.initialize(null, null)).isInstanceOf(PluginConfigurationException.class) .hasMessage(FetchResponseTransformationFilterFactory.class.getSimpleName() + " requires configuration, but config object is null"); - FilterFactoryContext constructContext = Mockito.mock(FilterFactoryContext.class); + FilterFactoryContext constructContext = mock(FilterFactoryContext.class); doReturn(new UpperCasing()).when(constructContext).pluginInstance(any(), any()); - Config config = new Config(UpperCasing.class.getName(), + FetchResponseTransformationFilterFactory.Config config = new FetchResponseTransformationFilterFactory.Config(UpperCasing.class.getName(), new UpperCasing.Config("UTF-8")); assertThat(factory.createFilter(constructContext, config)).isInstanceOf(FetchResponseTransformationFilter.class); } diff --git a/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java new file mode 100644 index 0000000000..11c3e6e123 --- /dev/null +++ b/kroxylicious-filters/kroxylicious-simple-transform/src/test/java/io/kroxylicious/proxy/filter/simpletransform/ProduceRequestTransformationFilterFactoryFilterTest.java @@ -0,0 +1,171 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.filter.simpletransform; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.apache.kafka.common.message.ProduceRequestData; +import org.apache.kafka.common.message.ProduceRequestData.PartitionProduceData; +import org.apache.kafka.common.message.ProduceRequestData.TopicProduceData; +import org.apache.kafka.common.message.RequestHeaderData; +import org.apache.kafka.common.protocol.ApiMessage; +import org.apache.kafka.common.record.CompressionType; +import org.apache.kafka.common.record.MemoryRecords; +import org.apache.kafka.common.record.MemoryRecordsBuilder; +import org.apache.kafka.common.record.Record; +import org.apache.kafka.common.record.RecordBatch; +import org.apache.kafka.common.record.Records; +import org.apache.kafka.common.record.TimestampType; +import org.apache.kafka.common.utils.ByteBufferOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import io.kroxylicious.proxy.filter.FilterContext; +import io.kroxylicious.proxy.filter.FilterFactoryContext; +import io.kroxylicious.proxy.filter.RequestFilterResult; +import io.kroxylicious.proxy.plugin.PluginConfigurationException; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ProduceRequestTransformationFilterFactoryFilterTest { + + private static final String TOPIC_NAME = "mytopic"; + private static final String ORIGINAL_RECORD_VALUE = "lowercasevalue"; + private static final String EXPECTED_TRANSFORMED_RECORD_VALUE = ORIGINAL_RECORD_VALUE.toUpperCase(Locale.ROOT); + private static final String RECORD_KEY = "key"; + private ProduceRequestTransformationFilter filter; + @Mock(strictness = Mock.Strictness.LENIENT) + FilterContext context; + + @Mock(strictness = Mock.Strictness.LENIENT) + private RequestFilterResult responseFilterResult; + + @Captor + private ArgumentCaptor bufferInitialCapacity; + + @Captor + private ArgumentCaptor requestHeaderDataCaptor; + + @Captor + private ArgumentCaptor apiMessageCaptor; + + @BeforeEach + void setUp() { + filter = new ProduceRequestTransformationFilter(new UpperCasing.Transformation( + new UpperCasing.Config("UTF-8"))); + + when(context.forwardRequest(requestHeaderDataCaptor.capture(), apiMessageCaptor.capture())).thenAnswer( + invocation -> CompletableFuture.completedStage(responseFilterResult)); + + when(responseFilterResult.message()).thenAnswer(invocation -> apiMessageCaptor.getValue()); + when(responseFilterResult.header()).thenAnswer(invocation -> requestHeaderDataCaptor.getValue()); + + when(context.createByteBufferOutputStream(bufferInitialCapacity.capture())).thenAnswer( + (Answer) invocation -> { + Object[] args = invocation.getArguments(); + Integer size = (Integer) args[0]; + return new ByteBufferOutputStream(size); + }); + } + + @Test + void testFactory() { + var factory = new ProduceRequestTransformationFilterFactory(); + assertThatThrownBy(() -> factory.initialize(null, null)).isInstanceOf(PluginConfigurationException.class) + .hasMessage(ProduceRequestTransformationFilterFactory.class.getSimpleName() + " requires configuration, but config object is null"); + FilterFactoryContext constructContext = mock(FilterFactoryContext.class); + doReturn(new UpperCasing()).when(constructContext).pluginInstance(any(), any()); + var config = new ProduceRequestTransformationFilterFactory.Config(UpperCasing.class.getName(), + new UpperCasing.Config("UTF-8")); + assertThat(factory.createFilter(constructContext, config)).isInstanceOf(ProduceRequestTransformationFilter.class); + } + + @Test + void filterProduceRequest() throws Exception { + + var produceRequest = new ProduceRequestData(); + produceRequest.topicData().add(createTopicProduceDataWithOneRecord(RECORD_KEY, ORIGINAL_RECORD_VALUE).setName(TOPIC_NAME)); + + var stage = filter.onProduceRequest(produceRequest.apiKey(), new RequestHeaderData(), produceRequest, context); + assertThat(stage).isCompleted(); + + var filteredRequest = (ProduceRequestData) stage.toCompletableFuture().get().message(); + + // verify that the response now has the topic name + assertThat(filteredRequest.topicData()) + .withFailMessage("expected same number of topics in the request") + .hasSameSizeAs(produceRequest.topicData()) + .withFailMessage("expected topic request to have been augmented with topic name") + .anyMatch(ftr -> Objects.equals(ftr.name(), TOPIC_NAME)); + + var filteredRecords = requestToRecordStream(filteredRequest).toList(); + assertThat(filteredRecords) + .withFailMessage("unexpected number of records in the filtered request") + .hasSize(1); + + var filteredRecord = filteredRecords.get(0); + assertThat(decodeUtf8Value(filteredRecord)) + .withFailMessage("expected record value to have been transformed") + .isEqualTo(EXPECTED_TRANSFORMED_RECORD_VALUE); + } + + private Stream requestToRecordStream(ProduceRequestData filteredResponse) { + return Stream.of(filteredResponse.topicData()) + .flatMap(Collection::stream) + .map(TopicProduceData::partitionData) + .flatMap(Collection::stream) + .map(PartitionProduceData::records) + .map(Records.class::cast) + .map(Records::records) + .map(Iterable::spliterator) + .flatMap(si -> StreamSupport.stream(si, false)); + } + + @NonNull + private static TopicProduceData createTopicProduceDataWithOneRecord(String key, String value) { + var topicProduceData = new TopicProduceData(); + var partitionData = new PartitionProduceData(); + partitionData.setRecords(buildOneRecord(key, value)); + topicProduceData.partitionData().add(partitionData); + return topicProduceData; + } + + private static MemoryRecords buildOneRecord(String key, String value) { + try (MemoryRecordsBuilder builder = MemoryRecords.builder(ByteBuffer.allocate(1024), RecordBatch.CURRENT_MAGIC_VALUE, + CompressionType.NONE, TimestampType.CREATE_TIME, 0L, System.currentTimeMillis())) { + builder.append(0L, key.getBytes(), value.getBytes()); + return builder.build(); + } + } + + @NonNull + private String decodeUtf8Value(Record record) { + return StandardCharsets.UTF_8.decode(record.value()).toString(); + } + +} diff --git a/kroxylicious-filters/pom.xml b/kroxylicious-filters/pom.xml index 751c8741ca..6f3121e90f 100644 --- a/kroxylicious-filters/pom.xml +++ b/kroxylicious-filters/pom.xml @@ -25,6 +25,7 @@ kroxylicious-multitenant kroxylicious-record-validation + kroxylicious-simple-transform diff --git a/kroxylicious-integration-test-support/pom.xml b/kroxylicious-integration-test-support/pom.xml index f77e7a461c..3c0c7b6569 100644 --- a/kroxylicious-integration-test-support/pom.xml +++ b/kroxylicious-integration-test-support/pom.xml @@ -21,7 +21,7 @@ kroxylicious-integration-test-support Integration test support - Support code to simplfy writing integration tests for filters + Support code to simplify writing integration tests for filters diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java index 68acbaff71..b86038411b 100644 --- a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java +++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java @@ -6,6 +6,7 @@ package io.kroxylicious.proxy.config; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; @@ -17,9 +18,6 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.flipkart.zjsonpatch.JsonDiff; -import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory; -import io.kroxylicious.proxy.internal.filter.UpperCasing; - import static org.assertj.core.api.Assertions.assertThat; class ConfigurationTest { @@ -34,18 +32,18 @@ public static Stream fluentApiConfigYamlFidelity() { useIoUring: true"""), Arguments.of("With filter", new ConfigurationBuilder() - .addToFilters(new FilterDefinitionBuilder(ProduceRequestTransformationFilterFactory.class.getSimpleName()) - .withConfig("transformation", UpperCasing.class.getName(), - "transformationConfig", new UpperCasing.Config("UTF-8")) + .addToFilters(new FilterDefinitionBuilder(ExampleFilterFactory.class.getSimpleName()) + .withConfig("examplePlugin", "ExamplePluginInstance", + "examplePluginConfig", Map.of("pluginKey", "pluginValue")) .build()) .build(), """ filters: - - type: ProduceRequestTransformationFilterFactory + - type: ExampleFilterFactory config: - transformation: "io.kroxylicious.proxy.internal.filter.UpperCasing" - transformationConfig: - charset: UTF-8 + examplePlugin: ExamplePluginInstance + examplePluginConfig: + pluginKey: pluginValue """), Arguments.of("With Virtual Cluster", new ConfigurationBuilder() diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java new file mode 100644 index 0000000000..abc7753792 --- /dev/null +++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExampleFilterFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.config; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.kroxylicious.proxy.filter.Filter; +import io.kroxylicious.proxy.filter.FilterFactory; +import io.kroxylicious.proxy.filter.FilterFactoryContext; +import io.kroxylicious.proxy.plugin.Plugin; +import io.kroxylicious.proxy.plugin.PluginImplConfig; +import io.kroxylicious.proxy.plugin.PluginImplName; +import io.kroxylicious.proxy.plugin.Plugins; + +import static org.junit.jupiter.api.Assertions.fail; + +@Plugin(configType = ExampleFilterFactory.Config.class) +public class ExampleFilterFactory implements FilterFactory { + + public record Config(@PluginImplName(ExamplePluginFactory.class) @JsonProperty(required = true) String examplePlugin, + @PluginImplConfig(implNameProperty = "examplePlugin") Object examplePluginConfig) {} + + @Override + public Config initialize(FilterFactoryContext context, Config config) { + var factory = context.pluginInstance(ExampleFilterFactory.class, config.examplePlugin()); + Objects.requireNonNull(factory, "Violated contract of FilterCreationContext"); + return Plugins.requireConfig(this, config); + } + + @Override + public Filter createFilter(FilterFactoryContext context, Config configuration) { + fail("unexpected call"); + return null; + } + +} diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java new file mode 100644 index 0000000000..c7b252ddba --- /dev/null +++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.config; + +import io.kroxylicious.proxy.plugin.PluginConfigurationException; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface ExamplePluginFactory { + + @NonNull + default C requireConfig(C config) { + if (config == null) { + throw new PluginConfigurationException(this.getClass().getSimpleName() + " requires configuration, but config object is null"); + } + return config; + } + + ExamplePlugin createExamplePlugin(C configuration); + + @FunctionalInterface + interface ExamplePlugin { + void foo(); + } +} diff --git a/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java new file mode 100644 index 0000000000..ee74bccffc --- /dev/null +++ b/kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ExamplePluginInstance.java @@ -0,0 +1,23 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.config; + +import io.kroxylicious.proxy.plugin.Plugin; + +import static org.junit.jupiter.api.Assertions.fail; + +@Plugin(configType = ExamplePluginInstance.Config.class) +public class ExamplePluginInstance implements ExamplePluginFactory { + + @Override + public ExamplePlugin createExamplePlugin(Config configuration) { + fail("unexpected call"); + return null; + } + + public record Config(String pluginKey) {} +} diff --git a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory similarity index 75% rename from kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory rename to kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory index 50e4a84360..3e18ac60a6 100644 --- a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory +++ b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.config.ExamplePluginFactory @@ -4,4 +4,4 @@ # Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 # -io.kroxylicious.proxy.internal.filter.UpperCasing \ No newline at end of file +io.kroxylicious.proxy.config.ExamplePluginInstance \ No newline at end of file diff --git a/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory new file mode 100644 index 0000000000..dd952e2fd9 --- /dev/null +++ b/kroxylicious-integration-test-support/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory @@ -0,0 +1,6 @@ +# +# Copyright Kroxylicious Authors. +# +# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 +# +io.kroxylicious.proxy.config.ExampleFilterFactory \ No newline at end of file diff --git a/kroxylicious-integration-tests/pom.xml b/kroxylicious-integration-tests/pom.xml index 98284cfa0f..1121f57f37 100644 --- a/kroxylicious-integration-tests/pom.xml +++ b/kroxylicious-integration-tests/pom.xml @@ -48,6 +48,11 @@ kroxylicious-record-validation test
+ + io.kroxylicious + kroxylicious-simple-transform + test + io.kroxylicious.testing testing-api diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java index 1c801e9693..2a8390a917 100644 --- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java +++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/FilterIT.java @@ -56,8 +56,8 @@ import io.kroxylicious.proxy.filter.RejectingCreateTopicFilterFactory; import io.kroxylicious.proxy.filter.RequestResponseMarkingFilter; import io.kroxylicious.proxy.filter.RequestResponseMarkingFilterFactory; -import io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory; -import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory; +import io.kroxylicious.proxy.filter.simpletransform.FetchResponseTransformationFilterFactory; +import io.kroxylicious.proxy.filter.simpletransform.ProduceRequestTransformationFilterFactory; import io.kroxylicious.test.Request; import io.kroxylicious.test.Response; import io.kroxylicious.test.ResponsePayload; diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java index fadea1a678..7dfe11822b 100644 --- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java +++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestDecoderFactory.java @@ -8,8 +8,8 @@ import java.nio.ByteBuffer; -import io.kroxylicious.proxy.internal.filter.ByteBufferTransformation; -import io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory; +import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformation; +import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory; import io.kroxylicious.proxy.plugin.Plugin; import io.kroxylicious.proxy.plugin.PluginConfigurationException; @@ -26,7 +26,7 @@ public TestDecoder createTransformation(Void configuration) { return new TestDecoder(); } - public class TestDecoder implements ByteBufferTransformation { + public static class TestDecoder implements ByteBufferTransformation { @Override public ByteBuffer transform(String topicName, ByteBuffer in) { diff --git a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java index 2f3f7feeaa..266b21af93 100644 --- a/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java +++ b/kroxylicious-integration-tests/src/test/java/io/kroxylicious/proxy/TestEncoderFactory.java @@ -8,8 +8,8 @@ import java.nio.ByteBuffer; -import io.kroxylicious.proxy.internal.filter.ByteBufferTransformation; -import io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory; +import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformation; +import io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory; import io.kroxylicious.proxy.plugin.Plugin; import io.kroxylicious.proxy.plugin.PluginConfigurationException; diff --git a/kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory b/kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory similarity index 100% rename from kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory rename to kroxylicious-integration-tests/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.simpletransform.ByteBufferTransformationFactory diff --git a/kroxylicious-runtime/pom.xml b/kroxylicious-runtime/pom.xml index 381b4f630e..772aea5ff0 100644 --- a/kroxylicious-runtime/pom.xml +++ b/kroxylicious-runtime/pom.xml @@ -278,19 +278,6 @@ - - withAdditionalFilters - - - io.kroxylicious - kroxylicious-multitenant - - - io.kroxylicious - kroxylicious-record-validation - - - debug diff --git a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory deleted file mode 100644 index c6fca29e83..0000000000 --- a/kroxylicious-runtime/src/main/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory +++ /dev/null @@ -1,2 +0,0 @@ -io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory -io.kroxylicious.proxy.internal.filter.FetchResponseTransformationFilterFactory diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java index 8f91333303..5f45842087 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/KafkaProxyTest.java @@ -27,24 +27,23 @@ class KafkaProxyTest { @Test void shouldFailToStartIfRequireFilterConfigIsMissing() throws Exception { var config = """ - virtualClusters: - demo1: - targetCluster: - bootstrap_servers: kafka.example:1234 - clusterNetworkAddressConfigProvider: - type: PortPerBrokerClusterNetworkAddressConfigProvider - config: - bootstrapAddress: localhost:9192 - brokerStartPort: 9193 - numberOfBrokerPorts: 2 - filters: - - type: ProduceRequestTransformationFilterFactory + virtualClusters: + demo1: + targetCluster: + bootstrap_servers: kafka.example:1234 + clusterNetworkAddressConfigProvider: + type: PortPerBrokerClusterNetworkAddressConfigProvider + config: + bootstrapAddress: localhost:9192 + numberOfBrokerPorts: 1 + filters: + - type: RequiresConfigFactory """; - ConfigParser configParser = new ConfigParser(); + var configParser = new ConfigParser(); try (var kafkaProxy = new KafkaProxy(configParser, configParser.parseConfiguration(config))) { assertThatThrownBy(kafkaProxy::startup).isInstanceOf(PluginConfigurationException.class) .hasMessage( - "Exception initializing filter factory ProduceRequestTransformationFilterFactory with config null: ProduceRequestTransformationFilterFactory requires configuration, but config object is null"); + "Exception initializing filter factory RequiresConfigFactory with config null: RequiresConfigFactory requires configuration, but config object is null"); } } diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java index 2f623ecdbf..48d46c4454 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/config/ConfigParserTest.java @@ -26,14 +26,13 @@ import io.kroxylicious.proxy.config.admin.AdminHttpConfiguration; import io.kroxylicious.proxy.filter.FilterFactory; import io.kroxylicious.proxy.internal.clusternetworkaddressconfigprovider.PortPerBrokerClusterNetworkAddressConfigProvider.PortPerBrokerClusterNetworkAddressConfigProviderConfig; -import io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory; import io.kroxylicious.proxy.internal.filter.ConstructorInjectionConfig; +import io.kroxylicious.proxy.internal.filter.ExamplePluginFactory; import io.kroxylicious.proxy.internal.filter.FactoryMethodConfig; import io.kroxylicious.proxy.internal.filter.FieldInjectionConfig; -import io.kroxylicious.proxy.internal.filter.ProduceRequestTransformationFilterFactory; +import io.kroxylicious.proxy.internal.filter.NestedPluginConfigFactory; import io.kroxylicious.proxy.internal.filter.RecordConfig; import io.kroxylicious.proxy.internal.filter.SetterInjectionConfig; -import io.kroxylicious.proxy.internal.filter.UpperCasing; import io.kroxylicious.proxy.plugin.UnknownPluginInstanceException; import io.kroxylicious.proxy.service.HostPort; @@ -47,7 +46,7 @@ class ConfigParserTest { private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); // Given - private ConfigParser configParser = new ConfigParser(); + private final ConfigParser configParser = new ConfigParser(); public static Stream yamlDeserializeSerializeFidelity() { return Stream.of(Arguments.of("Top level flags", """ @@ -103,11 +102,7 @@ public static Stream yamlDeserializeSerializeFidelity() { Arguments.of("Filters", """ filters: - - type: ProduceRequestTransformationFilterFactory - config: - transformation: io.kroxylicious.proxy.internal.filter.UpperCasing - transformationConfig: - charset: UTF-8 + - type: TestFilterFactory """), Arguments.of("Admin", """ adminHttp: @@ -249,25 +244,22 @@ void testNestedPlugins() { ConfigParser cp = new ConfigParser(); var config = cp.parseConfiguration(""" filters: - - type: ProduceRequestTransformationFilterFactory + - type: NestedPluginConfigFactory config: - transformation: UpperCasing - transformationConfig: - charset: UTF-8 + examplePlugin: ExamplePluginInstance """); assertThat(config.filters()).hasSize(1); FilterDefinition fd = config.filters().get(0); - assertEquals("ProduceRequestTransformationFilterFactory", fd.type()); + assertThat(fd.type()).isEqualTo("NestedPluginConfigFactory"); FilterFactory ff = cp.pluginFactory(FilterFactory.class).pluginInstance(fd.type()); assertThat(ff).isNotNull(); - assertThat(fd.config()).isInstanceOf(ProduceRequestTransformationFilterFactory.Config.class); + assertThat(fd.config()).isInstanceOf(NestedPluginConfigFactory.NestedPluginConfig.class); - var prtc = (ProduceRequestTransformationFilterFactory.Config) fd.config(); - assertThat(prtc.transformationConfig()).isInstanceOf(UpperCasing.Config.class); - assertEquals("UpperCasing", prtc.transformation()); - ByteBufferTransformationFactory tm = cp.pluginFactory(ByteBufferTransformationFactory.class).pluginInstance(prtc.transformation()); - assertThat(tm).isNotNull(); + var npc = (NestedPluginConfigFactory.NestedPluginConfig) fd.config(); + assertThat(npc.examplePlugin()).isEqualTo("ExamplePluginInstance"); + var ep = cp.pluginFactory(ExamplePluginFactory.class).pluginInstance(npc.examplePlugin()); + assertThat(ep).isNotNull(); } @Test @@ -275,17 +267,16 @@ void testUnknownPlugin() { ConfigParser cp = new ConfigParser(); var iae = assertThrows(IllegalArgumentException.class, () -> cp.parseConfiguration(""" filters: - - type: ProduceRequestTransformationFilterFactory + - type: NestedPluginConfigFactory config: - transformation: NotAKnownPlugin - transformationConfig: - charset: UTF-8 + examplePlugin: NotAKnownPlugin + """)); var vie = assertInstanceOf(ValueInstantiationException.class, iae.getCause()); var upie = assertInstanceOf(UnknownPluginInstanceException.class, vie.getCause()); - assertEquals("Unknown io.kroxylicious.proxy.internal.filter.ByteBufferTransformationFactory plugin instance for " + assertEquals("Unknown io.kroxylicious.proxy.internal.filter.ExamplePluginFactory plugin instance for " + "name 'NotAKnownPlugin'. " - + "Known plugin instances are [UpperCasing, io.kroxylicious.proxy.internal.filter.UpperCasing]. " + + "Known plugin instances are [ExamplePluginInstance, io.kroxylicious.proxy.internal.filter.ExamplePluginInstance]. " + "Plugins must be loadable by java.util.ServiceLoader and annotated with " + "@Plugin.", upie.getMessage()); diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java new file mode 100644 index 0000000000..bcf32e7a67 --- /dev/null +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginFactory.java @@ -0,0 +1,17 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.internal.filter; + +public interface ExamplePluginFactory { + + ExamplePlugin createExamplePlugin(C configuration); + + @FunctionalInterface + interface ExamplePlugin { + void foo(); + } +} diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java new file mode 100644 index 0000000000..c1ef0d3acf --- /dev/null +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/ExamplePluginInstance.java @@ -0,0 +1,23 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.internal.filter; + +import io.kroxylicious.proxy.plugin.Plugin; + +import static org.junit.jupiter.api.Assertions.fail; + +@Plugin(configType = ExamplePluginInstance.Config.class) +public class ExamplePluginInstance implements ExamplePluginFactory { + public record Config(String myConfig) {} + + @Override + public ExamplePlugin createExamplePlugin(Config configuration) { + fail("unexpected call"); + return null; + } + +} diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java index 9c896bc57d..300607645a 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/FilterTest.java @@ -7,28 +7,28 @@ package io.kroxylicious.proxy.internal.filter; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.mockito.Mock; import io.kroxylicious.proxy.filter.FilterFactoryContext; import io.kroxylicious.proxy.plugin.PluginConfigurationException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; class FilterTest { + private final RequiresConfigFactory factory = new RequiresConfigFactory(); + @Mock + FilterFactoryContext constructContext; + @Test - void testFactory() { - ProduceRequestTransformationFilterFactory factory = new ProduceRequestTransformationFilterFactory(); + void detectsMissingConfig() { assertThatThrownBy(() -> factory.initialize(null, null)).isInstanceOf(PluginConfigurationException.class) - .hasMessage(ProduceRequestTransformationFilterFactory.class.getSimpleName() + " requires configuration, but config object is null"); - FilterFactoryContext constructContext = Mockito.mock(FilterFactoryContext.class); - doReturn(new UpperCasing()).when(constructContext).pluginInstance(eq(ByteBufferTransformationFactory.class), any()); - ProduceRequestTransformationFilterFactory.Config config = new ProduceRequestTransformationFilterFactory.Config( - UpperCasing.class.getName(), - new UpperCasing.Config("UTF-8")); - assertThat(factory.createFilter(constructContext, config)).isInstanceOf(ProduceRequestTransformationFilter.class); + .hasMessage(RequiresConfigFactory.class.getSimpleName() + " requires configuration, but config object is null"); + } + + @Test + void createFilter() { + var config = new ExampleConfig(); + assertThat(factory.createFilter(constructContext, config)).isInstanceOf(RequiresConfigFactory.Filter.class); } } diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java new file mode 100644 index 0000000000..3cdb33350e --- /dev/null +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/NestedPluginConfigFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright Kroxylicious Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ + +package io.kroxylicious.proxy.internal.filter; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.kroxylicious.proxy.filter.Filter; +import io.kroxylicious.proxy.filter.FilterFactory; +import io.kroxylicious.proxy.filter.FilterFactoryContext; +import io.kroxylicious.proxy.plugin.Plugin; +import io.kroxylicious.proxy.plugin.PluginImplConfig; +import io.kroxylicious.proxy.plugin.PluginImplName; +import io.kroxylicious.proxy.plugin.Plugins; + +import static org.junit.jupiter.api.Assertions.fail; + +@Plugin(configType = NestedPluginConfigFactory.NestedPluginConfig.class) +public class NestedPluginConfigFactory implements FilterFactory { + + @Override + public NestedPluginConfig initialize(FilterFactoryContext context, NestedPluginConfig config) { + var factory = context.pluginInstance(ExamplePluginFactory.class, config.examplePlugin()); + Objects.requireNonNull(factory, "Violated contract of FilterCreationContext"); + return Plugins.requireConfig(this, config); + } + + @Override + public Filter createFilter(FilterFactoryContext context, NestedPluginConfig configuration) { + fail("unexpected call"); + return null; + } + + public record NestedPluginConfig(@PluginImplName(ExamplePluginFactory.class) @JsonProperty(required = true) String examplePlugin, + @PluginImplConfig(implNameProperty = "examplePlugin") Object examplePluginConfig) {} + +} diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java index 91f000e01f..4d6618b1a7 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/OptionalConfigFactory.java @@ -17,7 +17,9 @@ import io.kroxylicious.proxy.filter.FilterFactoryContext; import io.kroxylicious.proxy.filter.RequestFilter; import io.kroxylicious.proxy.filter.RequestFilterResult; +import io.kroxylicious.proxy.plugin.Plugin; +@Plugin(configType = ExampleConfig.class) public class OptionalConfigFactory implements FilterFactory { @Override diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java index f83e8a86b2..7739c9f531 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/RequiresConfigFactory.java @@ -17,8 +17,10 @@ import io.kroxylicious.proxy.filter.FilterFactoryContext; import io.kroxylicious.proxy.filter.RequestFilter; import io.kroxylicious.proxy.filter.RequestFilterResult; +import io.kroxylicious.proxy.plugin.Plugin; import io.kroxylicious.proxy.plugin.Plugins; +@Plugin(configType = ExampleConfig.class) public class RequiresConfigFactory implements FilterFactory { @Override diff --git a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java index c0fcd9ac9f..7cdd0c08fa 100644 --- a/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java +++ b/kroxylicious-runtime/src/test/java/io/kroxylicious/proxy/internal/filter/TestFilterFactory.java @@ -17,8 +17,10 @@ import io.kroxylicious.proxy.filter.FilterFactoryContext; import io.kroxylicious.proxy.filter.RequestFilter; import io.kroxylicious.proxy.filter.RequestFilterResult; +import io.kroxylicious.proxy.plugin.Plugin; import io.kroxylicious.proxy.plugin.Plugins; +@Plugin(configType = ExampleConfig.class) public class TestFilterFactory implements FilterFactory { @Override diff --git a/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory index f009d352cf..091ef372d7 100644 --- a/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory +++ b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.filter.FilterFactory @@ -1,3 +1,8 @@ +# +# Copyright Kroxylicious Authors. +# +# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 +# io.kroxylicious.proxy.internal.filter.TestFilterFactory io.kroxylicious.proxy.internal.filter.OptionalConfigFactory io.kroxylicious.proxy.internal.filter.RequiresConfigFactory @@ -6,4 +11,5 @@ io.kroxylicious.proxy.internal.filter.FieldInjection io.kroxylicious.proxy.internal.filter.Record io.kroxylicious.proxy.internal.filter.SetterInjection io.kroxylicious.proxy.internal.filter.FactoryMethod -io.kroxylicious.proxy.internal.filter.MissingPluginImplName \ No newline at end of file +io.kroxylicious.proxy.internal.filter.MissingPluginImplName +io.kroxylicious.proxy.internal.filter.NestedPluginConfigFactory diff --git a/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory new file mode 100644 index 0000000000..3bb0a53c89 --- /dev/null +++ b/kroxylicious-runtime/src/test/resources/META-INF/services/io.kroxylicious.proxy.internal.filter.ExamplePluginFactory @@ -0,0 +1,6 @@ +# +# Copyright Kroxylicious Authors. +# +# Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 +# +io.kroxylicious.proxy.internal.filter.ExamplePluginInstance \ No newline at end of file