-
Notifications
You must be signed in to change notification settings - Fork 325
Add support for extensions to depend upon other extensions #6012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| 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; | ||
|
|
||
| @Documented | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Target({ElementType.TYPE}) | ||
| public @interface ExtensionDependsOn { | ||
| /** | ||
| * The list of classes that this extension depends on | ||
| * @return Array of Class objects representing the classes this extension depends on | ||
| */ | ||
| Class<?>[] dependentClasses() default {}; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| 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; | ||
|
|
||
| /** | ||
| * Annotation to specify that a class provides an extension and its dependencies. | ||
| * This annotation can be used to declare what extension points a class provides | ||
| * and what other extension points it depends on. | ||
| */ | ||
| @Documented | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Target({ElementType.TYPE}) | ||
| public @interface ExtensionProvides { | ||
| /** | ||
| * The list of classes that this extension provides | ||
| * @return Array of Class objects representing the classes this extension provides | ||
| */ | ||
| Class<?>[] providedClasses() default {}; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ | |
| package org.opensearch.dataprepper.plugin; | ||
|
|
||
| import org.opensearch.dataprepper.model.annotations.DataPrepperExtensionPlugin; | ||
| import org.opensearch.dataprepper.model.annotations.ExtensionDependsOn; | ||
| import org.opensearch.dataprepper.model.annotations.ExtensionProvides; | ||
| import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel; | ||
| import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; | ||
| import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException; | ||
|
|
@@ -27,10 +29,17 @@ public class ExtensionLoader { | |
| public class ExtensionPluginWithContext { | ||
| ExtensionPlugin extensionPlugin; | ||
| boolean configured; | ||
| Class<?>[] dependentClasses; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These can be |
||
| Class<?>[] providedClasses; | ||
|
|
||
| public ExtensionPluginWithContext(final ExtensionPlugin extensionPlugin, final boolean isConfigured) { | ||
| public ExtensionPluginWithContext(final ExtensionPlugin extensionPlugin, | ||
| final boolean isConfigured, | ||
| final Class<?>[] dependentClasses, | ||
| final Class<?>[] providedClasses) { | ||
| this.extensionPlugin = extensionPlugin; | ||
| this.configured = isConfigured; | ||
| this.dependentClasses = dependentClasses; | ||
| this.providedClasses = providedClasses; | ||
| } | ||
|
|
||
| public ExtensionPlugin getExtensionPlugin() { | ||
|
|
@@ -40,6 +49,14 @@ public ExtensionPlugin getExtensionPlugin() { | |
| public boolean isConfigured() { | ||
| return configured; | ||
| } | ||
|
|
||
| public Class<?>[] getDependentClasses() { | ||
| return dependentClasses; | ||
| } | ||
|
|
||
| public Class<?>[] getProvidedClasses() { | ||
| return providedClasses; | ||
| } | ||
| } | ||
|
|
||
| private Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator; | ||
|
|
@@ -73,9 +90,17 @@ public List<? extends ExtensionPlugin> loadExtensions() { | |
| final String pluginName = convertClassToName(extensionClass); | ||
| try { | ||
| final PluginArgumentsContext pluginArgumentsContext = getConstructionContext(extensionClass); | ||
| final ExtensionProvides extensionProvidesAnnotation = extensionClass.getAnnotation(ExtensionProvides.class); | ||
| final ExtensionDependsOn extensionDependsOnAnnotation = extensionClass.getAnnotation(ExtensionDependsOn.class); | ||
|
|
||
| final Class<?>[] providedClasses = extensionProvidesAnnotation != null ? | ||
| extensionProvidesAnnotation.providedClasses() : new Class<?>[]{}; | ||
| final Class<?>[] dependentClasses = extensionDependsOnAnnotation != null ? | ||
| extensionDependsOnAnnotation.dependentClasses() : new Class<?>[]{}; | ||
|
|
||
| final Object config = pluginArgumentsContext.getArgument(0); | ||
| return new ExtensionPluginWithContext(extensionPluginCreator.newPluginInstance( | ||
| extensionClass, pluginArgumentsContext, pluginName), (config != null)); | ||
| extensionClass, pluginArgumentsContext, pluginName), (config != null), dependentClasses, providedClasses); | ||
| } catch (Exception e) { | ||
| final PluginError pluginError = PluginError.builder() | ||
| .componentType(PipelinesDataFlowModel.EXTENSION_PLUGIN_TYPE) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| import javax.inject.Named; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Comparator; | ||
|
|
||
| @Named | ||
|
|
@@ -21,6 +22,35 @@ public PluginCreator pluginCreator( | |
|
|
||
| @Bean(name = "extensionsLoaderComparator") | ||
| public Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator() { | ||
| return Comparator.comparing(ExtensionLoader.ExtensionPluginWithContext::isConfigured).reversed(); | ||
| return (extensionOne, extensionTwo) -> { | ||
| // First, compare by configuration status (configured ones first) | ||
| int configCompare = Boolean.compare(extensionTwo.isConfigured(), extensionOne.isConfigured()); | ||
| if (configCompare != 0) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably not the final sorting. It may be the case a configured depends on a non-configured. In which case the dependency will not exist. But, we can handle this later. |
||
| return configCompare; | ||
| } | ||
|
|
||
| // Get the provided and dependent classes for both extensions | ||
| Class<?>[] extensionOneProvidedClasses = extensionOne.getProvidedClasses(); | ||
| Class<?>[] extensionTwoProvidedClasses = extensionTwo.getProvidedClasses(); | ||
| Class<?>[] extensionOneDependentClasses = extensionOne.getDependentClasses(); | ||
| Class<?>[] extensionTwoDependentClasses = extensionTwo.getDependentClasses(); | ||
|
|
||
| // If extensionOne provides any classes that extensionTwo depends on, extensionOne should go first | ||
| if (containsAnyExtensionDependencies(extensionOneProvidedClasses, extensionTwoDependentClasses)) { | ||
| return -1; | ||
| } | ||
|
|
||
| // If extensionTwo provides any classes that extensionOne depends on, extensionTwo should go first | ||
| if (containsAnyExtensionDependencies(extensionTwoProvidedClasses, extensionOneDependentClasses)) { | ||
| return 1; | ||
| } | ||
|
|
||
| return 0; | ||
| }; | ||
| } | ||
|
|
||
| private boolean containsAnyExtensionDependencies(final Class<?>[] provided, final Class<?>[] dependencies) { | ||
| return Arrays.stream(dependencies).anyMatch(dep -> | ||
| Arrays.asList(provided).contains(dep)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,10 @@ | |
|
|
||
| package org.opensearch.dataprepper.plugins.aws; | ||
|
|
||
| import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier; | ||
| import org.opensearch.dataprepper.model.annotations.DataPrepperExtensionPlugin; | ||
| import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor; | ||
| import org.opensearch.dataprepper.model.annotations.ExtensionProvides; | ||
| import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; | ||
| import org.opensearch.dataprepper.model.plugin.ExtensionPoints; | ||
|
|
||
|
|
@@ -15,6 +17,7 @@ | |
| * Data Prepper as an extension plugin. Everything starts from here. | ||
| */ | ||
| @DataPrepperExtensionPlugin(modelType = AwsPluginConfig.class, rootKeyJsonPath = "/aws/configurations") | ||
| @ExtensionProvides(providedClasses = {AwsCredentialsSupplier.class}) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| public class AwsPlugin implements ExtensionPlugin { | ||
| private final DefaultAwsCredentialsSupplier defaultAwsCredentialsSupplier; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We found that multiple calls to
refresh()will fail. So this solution only works with one dependency. This is fine for now, but we need to remember to fix this.