diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/Experimental.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/Experimental.java new file mode 100644 index 0000000000..8b97475f54 --- /dev/null +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/Experimental.java @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.model.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a Data Prepper plugin as experimental. + *

+ * Experimental plugins do not have the same compatibility guarantees as other plugins and may be unstable. + * They may have breaking changes between minor versions and may even be removed. + *

+ * Data Prepper administrators must enable experimental plugins in order to use them. + * Otherwise, they are not available to use with pipelines. + * + * @since 2.11 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Experimental { +} diff --git a/data-prepper-core/src/integrationTest/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryIT.java b/data-prepper-core/src/integrationTest/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryIT.java index 27dfa97cef..9230aa7ff8 100644 --- a/data-prepper-core/src/integrationTest/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryIT.java +++ b/data-prepper-core/src/integrationTest/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryIT.java @@ -14,6 +14,7 @@ import org.opensearch.dataprepper.core.event.EventFactoryApplicationContextMarker; import org.opensearch.dataprepper.core.validation.LoggingPluginErrorsHandler; import org.opensearch.dataprepper.core.validation.PluginErrorCollector; +import org.opensearch.dataprepper.model.plugin.NoPluginFoundException; import org.opensearch.dataprepper.plugins.configtest.TestComponentWithConfigInject; import org.opensearch.dataprepper.plugins.configtest.TestDISourceWithConfig; import org.opensearch.dataprepper.validation.PluginErrorsHandler; @@ -27,6 +28,7 @@ import org.opensearch.dataprepper.plugins.test.TestPlugin; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -38,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; /** * Integration test of the plugin framework. These tests should not mock any portion @@ -49,6 +52,8 @@ class DefaultPluginFactoryIT { private PipelinesDataFlowModel pipelinesDataFlowModel; @Mock private ExtensionsConfiguration extensionsConfiguration; + @Mock + private ExperimentalConfigurationContainer experimentalConfigurationContainer; private String pluginName; private String objectPluginName; private String pipelineName; @@ -67,6 +72,8 @@ private DefaultPluginFactory createObjectUnderTest() { final AnnotationConfigApplicationContext coreContext = new AnnotationConfigApplicationContext(); coreContext.setParent(publicContext); + when(experimentalConfigurationContainer.getExperimental()).thenReturn(ExperimentalConfiguration.defaultConfiguration()); + coreContext.scan(EventFactoryApplicationContextMarker.class.getPackage().getName()); coreContext.scan(DefaultAcknowledgementSetManager.class.getPackage().getName()); coreContext.scan(DefaultPluginFactory.class.getPackage().getName()); @@ -75,6 +82,7 @@ private DefaultPluginFactory createObjectUnderTest() { coreContext.registerBean(PluginErrorsHandler.class, LoggingPluginErrorsHandler::new); coreContext.registerBean(ExtensionsConfiguration.class, () -> extensionsConfiguration); coreContext.registerBean(PipelinesDataFlowModel.class, () -> pipelinesDataFlowModel); + coreContext.registerBean(ExperimentalConfigurationContainer.class, () -> experimentalConfigurationContainer); coreContext.refresh(); return coreContext.getBean(DefaultPluginFactory.class); @@ -188,6 +196,20 @@ void loadPlugin_should_throw_when_a_plugin_configuration_is_invalid() { assertThat(actualException.getMessage(), equalTo("Plugin test_plugin in pipeline " + pipelineName + " is configured incorrectly: requiredString must not be null")); } + @Test + void loadPlugin_should_throw_when_a_plugin_is_experimental_by_default() { + pluginName = "test_experimental_plugin"; + final PluginSetting pluginSetting = createPluginSettings(Collections.emptyMap()); + + final DefaultPluginFactory objectUnderTest = createObjectUnderTest(); + + final NoPluginFoundException actualException = assertThrows(NoPluginFoundException.class, + () -> objectUnderTest.loadPlugin(TestPluggableInterface.class, pluginSetting)); + + assertThat(actualException.getMessage(), notNullValue()); + assertThat(actualException.getMessage(), equalTo("Unable to create experimental plugin test_experimental_plugin. You must enable experimental plugins in data-prepper-config.yaml in order to use them.")); + } + private PluginSetting createPluginSettings(final Map pluginSettingMap) { final PluginSetting pluginSetting = new PluginSetting(pluginName, pluginSettingMap); pluginSetting.setPipelineName(pipelineName); diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/core/parser/model/DataPrepperConfiguration.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/core/parser/model/DataPrepperConfiguration.java index 9212a9943b..046653dcd6 100644 --- a/data-prepper-core/src/main/java/org/opensearch/dataprepper/core/parser/model/DataPrepperConfiguration.java +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/core/parser/model/DataPrepperConfiguration.java @@ -18,6 +18,8 @@ import org.opensearch.dataprepper.core.pipeline.PipelineShutdownOption; import org.opensearch.dataprepper.model.configuration.PipelineExtensions; import org.opensearch.dataprepper.model.configuration.PluginModel; +import org.opensearch.dataprepper.plugin.ExperimentalConfiguration; +import org.opensearch.dataprepper.plugin.ExperimentalConfigurationContainer; import org.opensearch.dataprepper.plugin.ExtensionsConfiguration; import java.time.Duration; @@ -31,7 +33,7 @@ /** * Class to hold configuration for DataPrepper, including server port and Log4j settings */ -public class DataPrepperConfiguration implements ExtensionsConfiguration, EventConfigurationContainer { +public class DataPrepperConfiguration implements ExtensionsConfiguration, EventConfigurationContainer, ExperimentalConfigurationContainer { static final Duration DEFAULT_SHUTDOWN_DURATION = Duration.ofSeconds(30L); private static final String DEFAULT_SOURCE_COORDINATION_STORE = "in_memory"; @@ -55,6 +57,7 @@ public class DataPrepperConfiguration implements ExtensionsConfiguration, EventC private PeerForwarderConfiguration peerForwarderConfiguration; private Duration processorShutdownTimeout; private Duration sinkShutdownTimeout; + private ExperimentalConfiguration experimental; private PipelineExtensions pipelineExtensions; public static final DataPrepperConfiguration DEFAULT_CONFIG = new DataPrepperConfiguration(); @@ -96,6 +99,7 @@ public DataPrepperConfiguration( @JsonProperty("source_coordination") final SourceCoordinationConfig sourceCoordinationConfig, @JsonProperty("pipeline_shutdown") final PipelineShutdownOption pipelineShutdown, @JsonProperty("event") final EventConfiguration eventConfiguration, + @JsonProperty("experimental") final ExperimentalConfiguration experimental, @JsonProperty("extensions") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonSetter(nulls = Nulls.SKIP) @@ -126,6 +130,8 @@ public DataPrepperConfiguration( if (this.sinkShutdownTimeout.isNegative()) { throw new IllegalArgumentException("sinkShutdownTimeout must be non-negative."); } + this.experimental = experimental != null ? experimental : ExperimentalConfiguration.defaultConfiguration(); + this.pipelineExtensions = pipelineExtensions; } @@ -239,4 +245,9 @@ public EventConfiguration getEventConfiguration() { public PipelineExtensions getPipelineExtensions() { return pipelineExtensions; } + + @Override + public ExperimentalConfiguration getExperimental() { + return experimental; + } } diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/core/parser/PipelineTransformerTests.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/core/parser/PipelineTransformerTests.java index 300850d9c9..3f2f03a1d9 100644 --- a/data-prepper-core/src/test/java/org/opensearch/dataprepper/core/parser/PipelineTransformerTests.java +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/core/parser/PipelineTransformerTests.java @@ -140,6 +140,7 @@ void setUp() { @AfterEach void tearDown() { verify(dataPrepperConfiguration).getEventConfiguration(); + verify(dataPrepperConfiguration).getExperimental(); verifyNoMoreInteractions(dataPrepperConfiguration); } diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestExperimentalPlugin.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestExperimentalPlugin.java new file mode 100644 index 0000000000..36f334e526 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestExperimentalPlugin.java @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugins; + +import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; +import org.opensearch.dataprepper.model.annotations.Experimental; +import org.opensearch.dataprepper.plugin.TestPluggableInterface; + +@DataPrepperPlugin(name = "test_experimental_plugin", pluginType = TestPluggableInterface.class) +@Experimental +public class TestExperimentalPlugin { +} diff --git a/data-prepper-plugin-framework/build.gradle b/data-prepper-plugin-framework/build.gradle index 8eff39b1dc..894132b473 100644 --- a/data-prepper-plugin-framework/build.gradle +++ b/data-prepper-plugin-framework/build.gradle @@ -25,4 +25,5 @@ dependencies { implementation libs.reflections.core implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.apache.commons:commons-text:1.10.0' + testImplementation 'ch.qos.logback:logback-classic:1.5.16' } \ No newline at end of file diff --git a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java index 81fd1c2b5b..0ec3b5a953 100644 --- a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java +++ b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -41,6 +42,7 @@ public class DefaultPluginFactory implements PluginFactory { private final PluginBeanFactoryProvider pluginBeanFactoryProvider; private final PluginConfigurationObservableFactory pluginConfigurationObservableFactory; private final ApplicationContextToTypedSuppliers applicationContextToTypedSuppliers; + private final List>> definedPluginConsumers; @Inject DefaultPluginFactory( @@ -49,8 +51,10 @@ public class DefaultPluginFactory implements PluginFactory { final PluginConfigurationConverter pluginConfigurationConverter, final PluginBeanFactoryProvider pluginBeanFactoryProvider, final PluginConfigurationObservableFactory pluginConfigurationObservableFactory, - final ApplicationContextToTypedSuppliers applicationContextToTypedSuppliers) { + final ApplicationContextToTypedSuppliers applicationContextToTypedSuppliers, + final List>> definedPluginConsumers) { this.applicationContextToTypedSuppliers = applicationContextToTypedSuppliers; + this.definedPluginConsumers = definedPluginConsumers; Objects.requireNonNull(pluginProviderLoader); Objects.requireNonNull(pluginConfigurationObservableFactory); this.pluginCreator = Objects.requireNonNull(pluginCreator); @@ -140,15 +144,13 @@ private Class getPluginClass(final Class baseClass, final St .orElseThrow(() -> new NoPluginFoundException( "Unable to find a plugin named '" + pluginName + "'. Please ensure that plugin is annotated with appropriate values.")); - logDeprecatedPluginsNames(pluginClass, pluginName); + handleDefinedPlugins(pluginClass, pluginName); return pluginClass; } - private void logDeprecatedPluginsNames(final Class pluginClass, final String pluginName) { - final String deprecatedName = pluginClass.getAnnotation(DataPrepperPlugin.class).deprecatedName(); - final String name = pluginClass.getAnnotation(DataPrepperPlugin.class).name(); - if (deprecatedName.equals(pluginName)) { - LOG.warn("Plugin name '{}' is deprecated and will be removed in the next major release. Consider using the updated plugin name '{}'.", deprecatedName, name); - } + private void handleDefinedPlugins(final Class pluginClass, final String pluginName) { + final DefinedPlugin definedPlugin = new DefinedPlugin<>(pluginClass, pluginName); + + definedPluginConsumers.forEach(definedPluginConsumer -> definedPluginConsumer.accept(definedPlugin)); } } diff --git a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefinedPlugin.java b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefinedPlugin.java new file mode 100644 index 0000000000..7f0b550a89 --- /dev/null +++ b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefinedPlugin.java @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import java.util.Objects; + +class DefinedPlugin { + private final Class pluginClass; + private final String pluginName; + + public DefinedPlugin(final Class pluginClass, final String pluginName) { + this.pluginClass = Objects.requireNonNull(pluginClass); + this.pluginName = Objects.requireNonNull(pluginName); + } + + public Class getPluginClass() { + return pluginClass; + } + + public String getPluginName() { + return pluginName; + } +} diff --git a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetector.java b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetector.java new file mode 100644 index 0000000000..aab8a8120f --- /dev/null +++ b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetector.java @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Named; +import java.util.function.Consumer; + +@Named +class DeprecatedPluginDetector implements Consumer> { + private static final Logger LOG = LoggerFactory.getLogger(DeprecatedPluginDetector.class); + + @Override + public void accept(final DefinedPlugin definedPlugin) { + logDeprecatedPluginsNames(definedPlugin.getPluginClass(), definedPlugin.getPluginName()); + } + + private void logDeprecatedPluginsNames(final Class pluginClass, final String pluginName) { + final String deprecatedName = pluginClass.getAnnotation(DataPrepperPlugin.class).deprecatedName(); + final String name = pluginClass.getAnnotation(DataPrepperPlugin.class).name(); + if (deprecatedName.equals(pluginName)) { + LOG.warn("Plugin name '{}' is deprecated and will be removed in the next major release. Consider using the updated plugin name '{}'.", deprecatedName, name); + } + } +} diff --git a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfiguration.java b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfiguration.java new file mode 100644 index 0000000000..5a30e529a4 --- /dev/null +++ b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Data Prepper configurations for experimental features. + * + * @since 2.11 + */ +public class ExperimentalConfiguration { + @JsonProperty("enable_all") + private boolean enableAll = false; + + public static ExperimentalConfiguration defaultConfiguration() { + return new ExperimentalConfiguration(); + } + + /** + * Gets whether all experimental features are enabled. + * @return true if all experimental features are enabled, false otherwise + * @since 2.11 + */ + public boolean isEnableAll() { + return enableAll; + } +} diff --git a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationContainer.java b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationContainer.java new file mode 100644 index 0000000000..e2ba00657f --- /dev/null +++ b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationContainer.java @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +/** + * Interface to decouple how an experimental configuration is defined from + * usage of those configurations. + * + * @since 2.11 + */ +public interface ExperimentalConfigurationContainer { + /** + * Gets the experimental configuration. + * @return the experimental configuration + * @since 2.11 + */ + ExperimentalConfiguration getExperimental(); +} diff --git a/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidator.java b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidator.java new file mode 100644 index 0000000000..ffd835de41 --- /dev/null +++ b/data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidator.java @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.annotations.Experimental; +import org.opensearch.dataprepper.model.plugin.NoPluginFoundException; + +import javax.inject.Named; +import java.util.function.Consumer; + +@Named +class ExperimentalPluginValidator implements Consumer> { + private final ExperimentalConfiguration experimentalConfiguration; + + ExperimentalPluginValidator(final ExperimentalConfigurationContainer experimentalConfigurationContainer) { + this.experimentalConfiguration = experimentalConfigurationContainer.getExperimental(); + } + + @Override + public void accept(final DefinedPlugin definedPlugin) { + if(isPluginDisallowedAsExperimental(definedPlugin.getPluginClass())) { + throw new NoPluginFoundException("Unable to create experimental plugin " + definedPlugin.getPluginName() + + ". You must enable experimental plugins in data-prepper-config.yaml in order to use them."); + } + } + + private boolean isPluginDisallowedAsExperimental(final Class pluginClass) { + return pluginClass.isAnnotationPresent(Experimental.class) && !experimentalConfiguration.isEnableAll(); + } +} diff --git a/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java index 6f54a55c95..2c1bf9e0fa 100644 --- a/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java +++ b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import java.util.function.Supplier; import static org.hamcrest.CoreMatchers.equalTo; @@ -38,6 +39,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -64,6 +66,7 @@ class DefaultPluginFactoryTest { private PluginConfigurationObservableFactory pluginConfigurationObservableFactory; private PluginConfigObservable pluginConfigObservable; private ApplicationContextToTypedSuppliers applicationContextToTypedSuppliers; + private List>> definedPluginConsumers; @BeforeEach void setUp() { @@ -92,6 +95,8 @@ void setUp() { )).willReturn(pluginConfigObservable); applicationContextToTypedSuppliers = mock(ApplicationContextToTypedSuppliers.class); + + definedPluginConsumers = List.of(mock(Consumer.class), mock(Consumer.class)); } private DefaultPluginFactory createObjectUnderTest() { @@ -99,7 +104,8 @@ private DefaultPluginFactory createObjectUnderTest() { pluginProviderLoader, pluginCreator, pluginConfigurationConverter, beanFactoryProvider, pluginConfigurationObservableFactory, - applicationContextToTypedSuppliers); + applicationContextToTypedSuppliers, + definedPluginConsumers); } @Test @@ -230,6 +236,22 @@ void loadPlugin_should_create_a_new_instance_of_the_first_plugin_found() { verify(beanFactoryProvider).createPluginSpecificContext(new Class[]{}, convertedConfiguration); } + @Test + void loadPlugin_should_call_all_definedPluginConsumers() { + createObjectUnderTest().loadPlugin(baseClass, pluginSetting); + + assertThat("This test is not valid if there are no defined plugin consumers.", + definedPluginConsumers.size(), greaterThanOrEqualTo(2)); + for (final Consumer> definedPluginConsumer : definedPluginConsumers) { + final ArgumentCaptor> definedPluginArgumentCaptor = ArgumentCaptor.forClass(DefinedPlugin.class); + verify(definedPluginConsumer).accept(definedPluginArgumentCaptor.capture()); + + final DefinedPlugin actualDefinedPlugin = definedPluginArgumentCaptor.getValue(); + assertThat(actualDefinedPlugin.getPluginClass(), equalTo(expectedPluginClass)); + assertThat(actualDefinedPlugin.getPluginName(), equalTo(pluginName)); + } + } + @Test void loadPlugins_should_throw_for_null_number_of_instances() { @@ -322,6 +344,23 @@ void loadPlugin_with_varargs_should_return_a_single_instance_when_the_the_number assertThat(plugin, equalTo(expectedInstance)); } + @Test + void loadPlugin_with_varargs_should_call_all_definedPluginConsumers() { + final Object vararg1 = new Object(); + createObjectUnderTest().loadPlugin(baseClass, pluginSetting, vararg1); + + assertThat("This test is not valid if there are no defined plugin consumers.", + definedPluginConsumers.size(), greaterThanOrEqualTo(2)); + for (final Consumer> definedPluginConsumer : definedPluginConsumers) { + final ArgumentCaptor> definedPluginArgumentCaptor = ArgumentCaptor.forClass(DefinedPlugin.class); + verify(definedPluginConsumer).accept(definedPluginArgumentCaptor.capture()); + + final DefinedPlugin actualDefinedPlugin = definedPluginArgumentCaptor.getValue(); + assertThat(actualDefinedPlugin.getPluginClass(), equalTo(expectedPluginClass)); + assertThat(actualDefinedPlugin.getPluginName(), equalTo(pluginName)); + } + } + @Test void loadPlugins_should_return_an_instance_for_the_total_count() { final TestSink expectedInstance1 = mock(TestSink.class); diff --git a/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetectorTest.java b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetectorTest.java new file mode 100644 index 0000000000..f5325968a2 --- /dev/null +++ b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetectorTest.java @@ -0,0 +1,100 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; +import org.opensearch.dataprepper.model.processor.Processor; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DeprecatedPluginDetectorTest { + @Mock + private DefinedPlugin definedPlugin; + private TestLogAppender testAppender; + + @BeforeEach + void setUp() { + final Logger logger = (Logger) LoggerFactory.getLogger(DeprecatedPluginDetector.class); + + testAppender = new TestLogAppender(); + testAppender.start(); + logger.addAppender(testAppender); + } + + private DeprecatedPluginDetector createObjectUnderTest() { + return new DeprecatedPluginDetector(); + } + + @Test + void accept_on_plugin_without_deprecated_name_does_not_log() { + when(definedPlugin.getPluginClass()).thenReturn(PluginWithoutDeprecatedName.class); + createObjectUnderTest().accept(definedPlugin); + + assertThat(testAppender.getLoggedEvents(), empty()); + } + + @Test + void accept_on_plugin_with_deprecated_name_does_not_log_if_new_name_is_used() { + when(definedPlugin.getPluginClass()).thenReturn(PluginWithDeprecatedName.class); + when(definedPlugin.getPluginName()).thenReturn("test_for_deprecated_detection"); + createObjectUnderTest().accept(definedPlugin); + + assertThat(testAppender.getLoggedEvents(), empty()); + } + + @Test + void accept_on_plugin_with_deprecated_name_logs_if_deprecated_name_is_used() { + when(definedPlugin.getPluginClass()).thenReturn(PluginWithDeprecatedName.class); + when(definedPlugin.getPluginName()).thenReturn("test_for_deprecated_detection_deprecated_name"); + createObjectUnderTest().accept(definedPlugin); + + assertThat(testAppender.getLoggedEvents().stream() + .anyMatch(event -> event.getFormattedMessage().contains("Plugin name 'test_for_deprecated_detection_deprecated_name' is deprecated and will be removed in the next major release. Consider using the updated plugin name 'test_for_deprecated_detection'.")), + equalTo(true)); + } + + @DataPrepperPlugin(name = "test_for_deprecated_detection", pluginType = Processor.class) + private static class PluginWithoutDeprecatedName { + } + + @DataPrepperPlugin(name = "test_for_deprecated_detection", pluginType = Processor.class, deprecatedName = "test_for_deprecated_detection_deprecated_name") + private static class PluginWithDeprecatedName { + } + + public static class TestLogAppender extends AppenderBase { + private final List events = new ArrayList<>(); + + @Override + protected void append(final ILoggingEvent eventObject) { + events.add(eventObject); + } + + public List getLoggedEvents() { + return Collections.unmodifiableList(events); + } + } +} \ No newline at end of file diff --git a/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationTest.java b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationTest.java new file mode 100644 index 0000000000..0a3df6a892 --- /dev/null +++ b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationTest.java @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +class ExperimentalConfigurationTest { + @Test + void defaultConfiguration_should_return_config_with_isEnableAll_false() { + final ExperimentalConfiguration objectUnderTest = ExperimentalConfiguration.defaultConfiguration(); + assertThat(objectUnderTest, notNullValue()); + assertThat(objectUnderTest.isEnableAll(), equalTo(false)); + } +} \ No newline at end of file diff --git a/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidatorTest.java b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidatorTest.java new file mode 100644 index 0000000000..78d008b222 --- /dev/null +++ b/data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidatorTest.java @@ -0,0 +1,92 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.dataprepper.model.annotations.Experimental; +import org.opensearch.dataprepper.model.plugin.NoPluginFoundException; + +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExperimentalPluginValidatorTest { + + @Mock + private ExperimentalConfigurationContainer experimentalConfigurationContainer; + + @Mock + private ExperimentalConfiguration experimentalConfiguration; + + @Mock + private DefinedPlugin definedPlugin; + + @BeforeEach + void setUp() { + when(experimentalConfigurationContainer.getExperimental()).thenReturn(experimentalConfiguration); + } + + private ExperimentalPluginValidator createObjectUnderTest() { + return new ExperimentalPluginValidator(experimentalConfigurationContainer); + } + + @Test + void accept_with_non_Experimental_plugin_returns() { + when(definedPlugin.getPluginClass()).thenReturn(NonExperimentalPlugin.class); + + createObjectUnderTest().accept(definedPlugin); + } + + @Nested + class WithExperimentalPlugin { + @BeforeEach + void setUp() { + when(definedPlugin.getPluginClass()).thenReturn(ExperimentalPlugin.class); + } + + @Test + void accept_with_Experimental_plugin_throws_if_experimental_is_not_enabled() { + final String pluginName = UUID.randomUUID().toString(); + when(definedPlugin.getPluginName()).thenReturn(pluginName); + + final ExperimentalPluginValidator objectUnderTest = createObjectUnderTest(); + + final NoPluginFoundException actualException = assertThrows(NoPluginFoundException.class, () -> objectUnderTest.accept(definedPlugin)); + + assertThat(actualException.getMessage(), notNullValue()); + assertThat(actualException.getMessage(), containsString(pluginName)); + assertThat(actualException.getMessage(), containsString("experimental plugin")); + } + + @Test + void accept_with_Experimental_plugin_does_not_throw_if_experimental_is_enabled() { + when(experimentalConfiguration.isEnableAll()).thenReturn(true); + + createObjectUnderTest().accept(definedPlugin); + } + } + + private static class NonExperimentalPlugin { + } + + @Experimental + private static class ExperimentalPlugin { + } +} \ No newline at end of file