diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/DiffCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/DiffCommand.java index 528a3ea6d..60d383f65 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/DiffCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/DiffCommand.java @@ -30,11 +30,11 @@ import picocli.CommandLine.Option; @Command(name = "diff", - header = "Show resource changes required by the current resource definitions.", - description = """ - Generates a speculative reconciliation plan, showing the resource changes Jikkou would apply to reconcile the resource definitions. - This command does not actually perform the reconciliation actions. - """) + header = "Show resource changes required by the current resource definitions.", + description = """ + Generates a speculative reconciliation plan, showing the resource changes Jikkou would apply to reconcile the resource definitions. + This command does not actually perform the reconciliation actions. + """) @Singleton public class DiffCommand extends CLIBaseCommand implements Callable { @@ -47,22 +47,24 @@ public class DiffCommand extends CLIBaseCommand implements Callable { FormatOptionsMixin formatOptions; @Mixin ConfigOptionsMixin configOptionsMixin; + @Mixin + ProviderOptionMixin providerOptionMixin; @Option(names = {"--" + SimpleResourceChangeFilter.FILTER_RESOURCE_OPS_NAME}, - split = ",", - description = "Filter out all resources except those corresponding to given operations. Valid values: ${COMPLETION-CANDIDATES}." + split = ",", + description = "Filter out all resources except those corresponding to given operations. Valid values: ${COMPLETION-CANDIDATES}." ) Set filterOutAllResourcesExcept = new HashSet<>(); @Option(names = {"--" + SimpleResourceChangeFilter.FILTER_CHANGE_OP_NAME}, - split = ",", - description = "Filter out all state-changes except those corresponding to given operations. Valid values: ${COMPLETION-CANDIDATES}." + split = ",", + description = "Filter out all state-changes except those corresponding to given operations. Valid values: ${COMPLETION-CANDIDATES}." ) Set filterOutAllChangesExcept = new HashSet<>(); @Option(names = {"--list"}, - defaultValue = "false", - description = "Get resources as ApiResourceChangeList (default: ${DEFAULT-VALUE})." + defaultValue = "false", + description = "Get resources as ApiResourceChangeList (default: ${DEFAULT-VALUE})." ) private boolean list; @@ -82,11 +84,11 @@ public Integer call() throws IOException { try { ApiResourceChangeList result = api.getDiff( - getResources(), - new SimpleResourceChangeFilter() - .filterOutAllResourcesExcept(filterOutAllResourcesExcept) - .filterOutAllChangesExcept(filterOutAllChangesExcept), - getReconciliationContext() + getResources(), + new SimpleResourceChangeFilter() + .filterOutAllResourcesExcept(filterOutAllResourcesExcept) + .filterOutAllChangesExcept(filterOutAllChangesExcept), + getReconciliationContext() ); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { if (list) { @@ -111,12 +113,13 @@ private HasItems getResources() { @NotNull private ReconciliationContext getReconciliationContext() { return ReconciliationContext - .builder() - .dryRun(true) - .configuration(configOptionsMixin.getConfiguration()) - .selector(selectorOptions.getResourceSelector()) - .labels(fileOptions.getLabels()) - .annotations(fileOptions.getAnnotations()) - .build(); + .builder() + .dryRun(true) + .configuration(configOptionsMixin.getConfiguration()) + .selector(selectorOptions.getResourceSelector()) + .labels(fileOptions.getLabels()) + .annotations(fileOptions.getAnnotations()) + .providerName(providerOptionMixin.getProvider()) + .build(); } } diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/PrepareCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/PrepareCommand.java index a49f9c924..b3bbed2b1 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/PrepareCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/PrepareCommand.java @@ -24,10 +24,10 @@ import picocli.CommandLine.Mixin; @Command(name = "prepare", - header = "Prepare the resource definition files for validation.", - description = """ - Prepare the resource definition files specified through the command line arguments for validation. - """ + header = "Prepare the resource definition files for validation.", + description = """ + Prepare the resource definition files specified through the command line arguments for validation. + """ ) @Singleton public class PrepareCommand extends CLIBaseCommand implements Callable { @@ -41,7 +41,8 @@ public class PrepareCommand extends CLIBaseCommand implements Callable FormatOptionsMixin formatOptions; @Mixin ConfigOptionsMixin configOptionsMixin; - + @Mixin + ProviderOptionMixin providerOptionMixin; // API @Inject JikkouApi api; @@ -58,8 +59,8 @@ public Integer call() throws IOException { try { HasItems result = api.prepare( - getResources(), - getReconciliationContext() + getResources(), + getReconciliationContext() ); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { writer.write(formatOptions.format, result.getItems(), baos); @@ -80,12 +81,13 @@ private HasItems getResources() { @NotNull private ReconciliationContext getReconciliationContext() { return ReconciliationContext - .builder() - .dryRun(true) - .configuration(configOptionsMixin.getConfiguration()) - .selector(selectorOptions.getResourceSelector()) - .labels(fileOptions.getLabels()) - .annotations(fileOptions.getAnnotations()) - .build(); + .builder() + .dryRun(true) + .configuration(configOptionsMixin.getConfiguration()) + .selector(selectorOptions.getResourceSelector()) + .labels(fileOptions.getLabels()) + .annotations(fileOptions.getAnnotations()) + .providerName(providerOptionMixin.getProvider()) + .build(); } } diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/ProviderOptionMixin.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/ProviderOptionMixin.java new file mode 100644 index 000000000..1c308125f --- /dev/null +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/ProviderOptionMixin.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.client.command; + +import picocli.CommandLine.Option; + +/** + * Mixin class for the --provider CLI option. + * Use this mixin to add provider selection support to commands that need + * to target a specific provider instance when multiple providers of the + * same type are configured. + */ +public final class ProviderOptionMixin { + + @Option( + names = {"--provider"}, + description = "Select a specific provider instance when multiple providers of the same type are configured (e.g., kafka-prod, kafka-dev)" + ) + public String provider; + + /** + * Gets the provider name. + * + * @return the provider name, or null if not specified. + */ + public String getProvider() { + return provider; + } +} \ No newline at end of file diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/action/ExecuteActionCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/action/ExecuteActionCommand.java index b728a1c3c..901bd058d 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/action/ExecuteActionCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/action/ExecuteActionCommand.java @@ -8,6 +8,7 @@ import io.micronaut.context.annotation.Prototype; import io.streamthoughts.jikkou.client.command.AbstractApiCommand; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.core.JikkouApi; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.io.Jackson; @@ -41,9 +42,11 @@ enum Format { defaultValue = "YAML", description = "Prints the output in the specified format. Allowed values: ${COMPLETION-CANDIDATES} (default YAML)." ) - private Format format; + @CommandLine.Mixin + ProviderOptionMixin providerMixin; + // Picocli require an empty constructor to generate the completion file public ExecuteActionCommand() { } @@ -53,7 +56,7 @@ public ExecuteActionCommand() { **/ @Override public Integer call() throws Exception { - ApiActionResultSet results = api.execute(name, Configuration.from(options())); + ApiActionResultSet results = api.execute(name, Configuration.from(options()), providerMixin.getProvider()); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { switch (format) { case JSON -> Jackson.JSON_OBJECT_MAPPER diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/config/SetContextCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/config/SetContextCommand.java index 428d068a7..ab50a7b25 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/config/SetContextCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/config/SetContextCommand.java @@ -59,7 +59,7 @@ public Integer call() throws IOException { if (this.clientConfigPropertiesFile != null) { for (final String propertiesFile : clientConfigPropertiesFile) { - if (!Strings.isBlank(propertiesFile)) { + if (!Strings.isNullOrEmpty(propertiesFile)) { final String expandedPath = Paths.get(propertiesFile).toAbsolutePath().toString(); try (var reader = new FileReader(expandedPath)) { clientConfigProps.load(reader); diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/extension/ListExtensionCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/extension/ListExtensionCommand.java index b4094e94d..c46bf99c2 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/extension/ListExtensionCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/extension/ListExtensionCommand.java @@ -52,7 +52,7 @@ public class ListExtensionCommand extends CLIBaseCommand implements Runnable { @Override public void run() { - ApiExtensionList apiExtensions = Strings.isBlank(kind) ? + ApiExtensionList apiExtensions = Strings.isNullOrEmpty(kind) ? api.getApiExtensions() : api.getApiExtensions(kind); Predicate predicate = Stream.>of( diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/get/GetResourceCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/get/GetResourceCommand.java index a5677be8b..af442ae4f 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/get/GetResourceCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/get/GetResourceCommand.java @@ -9,9 +9,12 @@ import io.micronaut.context.annotation.Prototype; import io.streamthoughts.jikkou.client.command.AbstractApiCommand; import io.streamthoughts.jikkou.client.command.FormatOptionsMixin; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin; import io.streamthoughts.jikkou.common.utils.Strings; +import io.streamthoughts.jikkou.core.GetContext; import io.streamthoughts.jikkou.core.JikkouApi; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.io.writer.ResourceWriter; import io.streamthoughts.jikkou.core.models.HasMetadata; @@ -36,6 +39,8 @@ public class GetResourceCommand extends AbstractApiCommand { SelectorOptionsMixin selectorOptions; @Mixin FormatOptionsMixin formatOptions; + @Mixin + ProviderOptionMixin providerOptions; @Option(names = {"--list"}, defaultValue = "false", description = "Get resources as ResourceListObject (default: ${DEFAULT-VALUE})." @@ -68,18 +73,19 @@ public GetResourceCommand() { @Override public Integer call() throws Exception { ResourceList resources; - if (Strings.isBlank(name)) { - resources = api.listResources( - type, - selectorOptions.getResourceSelector(), - Configuration.from(options()) - ); + if (Strings.isNullOrEmpty(name)) { + ListContext context = ListContext.builder() + .selector(selectorOptions.getResourceSelector()) + .configuration(Configuration.from(options())) + .providerName(providerOptions.getProvider()) + .build(); + resources = api.listResources(type, context); } else { - HasMetadata resource = api.getResource( - type, - name, - Configuration.from(options()) - ); + GetContext getContext = GetContext.builder() + .configuration(Configuration.from(options())) + .providerName(providerOptions.getProvider()) + .build(); + HasMetadata resource = api.getResource(type, name, getContext); resources = ResourceList.of(resource); } try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/health/GetHealthCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/health/GetHealthCommand.java index cd43b9709..575c263db 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/health/GetHealthCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/health/GetHealthCommand.java @@ -7,6 +7,7 @@ package io.streamthoughts.jikkou.client.command.health; import io.streamthoughts.jikkou.client.command.CLIBaseCommand; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.core.JikkouApi; import io.streamthoughts.jikkou.core.health.HealthStatus; import io.streamthoughts.jikkou.core.io.Jackson; @@ -44,6 +45,9 @@ enum Formats { JSON, YAML } description = "Timeout in milliseconds for retrieving health indicators (default: ${DEFAULT-VALUE}).") long timeoutMs; + @CommandLine.Mixin + ProviderOptionMixin providerMixin; + @Parameters( paramLabel = "HEALTH_INDICATOR", description = "Name of the health indicator (use 'all' to get all indicators).") @@ -60,9 +64,9 @@ public Integer call() throws IOException { Duration timeout = Duration.ofMillis(timeoutMs); ApiHealthResult result; if (indicator.equalsIgnoreCase(HEALTH_INDICATOR_ALL)) { - result = api.getApiHealth(timeout); + result = api.getApiHealth(timeout, providerMixin.getProvider()); } else { - result = api.getApiHealth(indicator, timeout); + result = api.getApiHealth(indicator, timeout, providerMixin.getProvider()); } if (result != null) { diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/BaseResourceCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/BaseResourceCommand.java index 3b89cfdac..a131eff6d 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/BaseResourceCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/BaseResourceCommand.java @@ -11,6 +11,7 @@ import io.streamthoughts.jikkou.client.command.ConfigOptionsMixin; import io.streamthoughts.jikkou.client.command.ExecOptionsMixin; import io.streamthoughts.jikkou.client.command.FileOptionsMixin; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin; import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter; import io.streamthoughts.jikkou.client.printer.Printers; @@ -39,7 +40,8 @@ public abstract class BaseResourceCommand extends CLIBaseCommand implements Call SelectorOptionsMixin selectorOptions; @Mixin ConfigOptionsMixin configOptionsMixin; - + @Mixin + ProviderOptionMixin providerOptionMixin; // SERVICES @Inject JikkouApi api; @@ -73,6 +75,7 @@ public Integer call() throws IOException { .selector(selectorOptions.getResourceSelector()) .labels(fileOptions.getLabels()) .annotations(fileOptions.getAnnotations()) + .providerName(providerOptionMixin.getProvider()) .build(); } diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/PatchResourceCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/PatchResourceCommand.java index d8f60871d..b9ce61dc3 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/PatchResourceCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/PatchResourceCommand.java @@ -11,6 +11,7 @@ import io.streamthoughts.jikkou.client.command.ConfigOptionsMixin; import io.streamthoughts.jikkou.client.command.ExecOptionsMixin; import io.streamthoughts.jikkou.client.command.FileOptionsMixin; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin; import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter; import io.streamthoughts.jikkou.core.JikkouApi; @@ -46,6 +47,8 @@ public final class PatchResourceCommand extends CLIBaseCommand implements Callab SelectorOptionsMixin selectorOptions; @Mixin ConfigOptionsMixin configOptionsMixin; + @Mixin + ProviderOptionMixin providerOptionMixin; @Option(names = {"--mode"}, required = true, @@ -86,6 +89,7 @@ public Integer call() throws IOException { .selector(selectorOptions.getResourceSelector()) .labels(fileOptions.getLabels()) .annotations(fileOptions.getAnnotations()) + .providerName(providerOptionMixin.getProvider()) .build(); } diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/ReplaceResourceCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/ReplaceResourceCommand.java index c80f72644..a36457ffe 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/ReplaceResourceCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/reconcile/ReplaceResourceCommand.java @@ -11,6 +11,7 @@ import io.streamthoughts.jikkou.client.command.ConfigOptionsMixin; import io.streamthoughts.jikkou.client.command.ExecOptionsMixin; import io.streamthoughts.jikkou.client.command.FileOptionsMixin; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin; import io.streamthoughts.jikkou.client.command.validate.ValidationErrorsWriter; import io.streamthoughts.jikkou.core.JikkouApi; @@ -45,7 +46,8 @@ public final class ReplaceResourceCommand extends CLIBaseCommand implements Call SelectorOptionsMixin selectorOptions; @Mixin ConfigOptionsMixin configOptionsMixin; - + @Mixin + ProviderOptionMixin providerOptionMixin; // SERVICES @Inject JikkouApi api; @@ -76,6 +78,7 @@ public Integer call() throws IOException { .selector(selectorOptions.getResourceSelector()) .labels(fileOptions.getLabels()) .annotations(fileOptions.getAnnotations()) + .providerName(providerOptionMixin.getProvider()) .build(); } diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/resources/ListApiResourcesCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/resources/ListApiResourcesCommand.java index 2ce249090..4a7f6d1f5 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/resources/ListApiResourcesCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/resources/ListApiResourcesCommand.java @@ -53,7 +53,7 @@ public void setVerbs(List verbs) { **/ @Override public void run() { - List apiResourceLists = Strings.isBlank(group) ? + List apiResourceLists = Strings.isNullOrEmpty(group) ? api.listApiResources() : api.listApiResources(group); diff --git a/cli/src/main/java/io/streamthoughts/jikkou/client/command/validate/ValidateCommand.java b/cli/src/main/java/io/streamthoughts/jikkou/client/command/validate/ValidateCommand.java index 57046da37..25fb7576d 100644 --- a/cli/src/main/java/io/streamthoughts/jikkou/client/command/validate/ValidateCommand.java +++ b/cli/src/main/java/io/streamthoughts/jikkou/client/command/validate/ValidateCommand.java @@ -10,6 +10,7 @@ import io.streamthoughts.jikkou.client.command.ConfigOptionsMixin; import io.streamthoughts.jikkou.client.command.FileOptionsMixin; import io.streamthoughts.jikkou.client.command.FormatOptionsMixin; +import io.streamthoughts.jikkou.client.command.ProviderOptionMixin; import io.streamthoughts.jikkou.client.command.SelectorOptionsMixin; import io.streamthoughts.jikkou.core.JikkouApi; import io.streamthoughts.jikkou.core.ReconciliationContext; @@ -30,13 +31,13 @@ import picocli.CommandLine.Mixin; @Command(name = "validate", - header = "Check whether the resources definitions meet all validation requirements.", - description = """ - Validate the resource definition files specified through the command line arguments. - - Validate runs all the user-defined validation requirements after performing any relevant resource transformations. - Validation rules are applied only to resources matching the selectors passed through the command line arguments. - """ + header = "Check whether the resources definitions meet all validation requirements.", + description = """ + Validate the resource definition files specified through the command line arguments. + + Validate runs all the user-defined validation requirements after performing any relevant resource transformations. + Validation rules are applied only to resources matching the selectors passed through the command line arguments. + """ ) @Singleton public class ValidateCommand extends CLIBaseCommand implements Callable { @@ -50,7 +51,8 @@ public class ValidateCommand extends CLIBaseCommand implements Callable FormatOptionsMixin formatOptions; @Mixin ConfigOptionsMixin configOptionsMixin; - + @Mixin + ProviderOptionMixin providerOptionMixin; // SERVICES @Inject JikkouApi api; @@ -85,11 +87,12 @@ private HasItems getResources() { @NotNull private ReconciliationContext getReconciliationContext() { return ReconciliationContext.builder() - .dryRun(true) - .configuration(configOptionsMixin.getConfiguration()) - .selector(selectorOptions.getResourceSelector()) - .labels(fileOptions.getLabels()) - .annotations(fileOptions.getAnnotations()) - .build(); + .dryRun(true) + .configuration(configOptionsMixin.getConfiguration()) + .selector(selectorOptions.getResourceSelector()) + .labels(fileOptions.getLabels()) + .annotations(fileOptions.getAnnotations()) + .providerName(providerOptionMixin.getProvider()) + .build(); } } diff --git a/cli/src/test/java/io/streamthoughts/jikkou/client/JikkouTest.java b/cli/src/test/java/io/streamthoughts/jikkou/client/JikkouTest.java index ac213d501..18618c0f0 100644 --- a/cli/src/test/java/io/streamthoughts/jikkou/client/JikkouTest.java +++ b/cli/src/test/java/io/streamthoughts/jikkou/client/JikkouTest.java @@ -22,8 +22,8 @@ class JikkouTest { URL resource = JikkouTest.class.getResource("/test-jikkou-config.json"); String path = resource.getPath(); GlobalConfigurationContext.setConfigurationContext(new ConfigurationContext( - new File(path), - new ObjectMapper() + new File(path), + new ObjectMapper() )); } @@ -50,7 +50,7 @@ void shouldPrintUsageForGetCommand() { int execute = Jikkou.execute(new String[]{"get"}); Assertions.assertEquals(CommandLine.ExitCode.USAGE, execute); } - + @Test void testCommandHealthGetIndicators() { int execute = Jikkou.execute(new String[]{"health", "get-indicators"}); diff --git a/core/src/main/java/io/streamthoughts/jikkou/common/utils/Strings.java b/core/src/main/java/io/streamthoughts/jikkou/common/utils/Strings.java index 14b9e0123..96176716b 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/common/utils/Strings.java +++ b/core/src/main/java/io/streamthoughts/jikkou/common/utils/Strings.java @@ -18,17 +18,12 @@ public final class Strings { private Strings() { } - public static boolean isNullOrEmpty(String value) { - return value == null || value.trim().isEmpty(); - } - - public static boolean isBlank(final String string) { + public static boolean isNullOrEmpty(final String string) { return string == null || string.trim().isEmpty(); - } public static boolean isNotBlank(final String string) { - return !isBlank(string); + return !isNullOrEmpty(string); } public static Properties toProperties(final String string) { diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/BaseApi.java b/core/src/main/java/io/streamthoughts/jikkou/core/BaseApi.java index 2d1e739c3..696a0d5db 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/BaseApi.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/BaseApi.java @@ -7,14 +7,17 @@ package io.streamthoughts.jikkou.core; import io.streamthoughts.jikkou.common.utils.Pair; +import io.streamthoughts.jikkou.common.utils.Strings; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.converter.Converter; import io.streamthoughts.jikkou.core.converter.ConverterChain; import io.streamthoughts.jikkou.core.exceptions.JikkouRuntimeException; +import io.streamthoughts.jikkou.core.extension.DefaultProviderConfigurationRegistry; import io.streamthoughts.jikkou.core.extension.Extension; import io.streamthoughts.jikkou.core.extension.ExtensionDescriptorModifier; import io.streamthoughts.jikkou.core.extension.ExtensionFactory; import io.streamthoughts.jikkou.core.extension.ExtensionProviderAwareRegistry; +import io.streamthoughts.jikkou.core.extension.ProviderConfigurationRegistry; import io.streamthoughts.jikkou.core.extension.qualifier.Qualifiers; import io.streamthoughts.jikkou.core.models.ApiGroup; import io.streamthoughts.jikkou.core.models.ApiResourceList; @@ -58,6 +61,7 @@ public abstract class BaseApi implements JikkouApi { private static final Logger LOG = LoggerFactory.getLogger(BaseApi.class); protected final ExtensionFactory extensionFactory; + protected final ProviderConfigurationRegistry providerConfigurationRegistry; /** * An abstract base implementation of the {@link BaseBuilder} object instance. @@ -66,14 +70,25 @@ public static abstract class BaseBuilder B register(@NotNull Class type, /** * Creates a new {@link BaseApi} instance. * - * @param extensionFactory The ExtensionFactory. + * @param extensionFactory The ExtensionFactory. + * @param providerConfigurationRegistry The ProviderConfigurationRegistry. */ - public BaseApi(@NotNull ExtensionFactory extensionFactory) { + public BaseApi(@NotNull ExtensionFactory extensionFactory, + @NotNull ProviderConfigurationRegistry providerConfigurationRegistry) { this.extensionFactory = Objects.requireNonNull(extensionFactory, "extensionFactory cannot be null"); + this.providerConfigurationRegistry = Objects.requireNonNull(providerConfigurationRegistry, "providerConfigurationRegistry cannot be null"); } /** @@ -187,16 +220,16 @@ public HasItems prepare(final @NotNull HasItems resources, @NotNull protected ResourceList doPrepare(@NotNull HasItems resources, @NotNull ReconciliationContext context) { return Stream.of(resources) - .map(this::convert) + .map(it -> convert(it, context)) .map(items -> transform(items, context)) .map(items -> select(items, context)) .findAny() .get(); } - private ResourceList convert(final @NotNull HasItems resources) { + private ResourceList convert(final @NotNull HasItems resources, @NotNull ReconciliationContext context) { - ConverterChain converter = getConverterChain(); + ConverterChain converter = newConverterChain(createProviderContext(context)); List converted = resources.getItems() .stream() .map(resource -> { @@ -224,7 +257,7 @@ private ResourceList convert(final @NotNull HasItems resources) { private ResourceList transform(final @NotNull HasItems items, final @NotNull ReconciliationContext context) { - TransformationChain transformationChain = newResourceTransformationChain(); + TransformationChain transformationChain = newResourceTransformationChain(createProviderContext(context)); List transformed = new LinkedList<>(); for (Map.Entry> entry : items.groupByType().entrySet()) { @@ -253,29 +286,25 @@ private ResourceList select(final @NotNull HasItems resources, .filter(Predicate.not(resource -> Resource.isTransient(resource.getClass()))) .filter(context.selector()::apply) .toList(); - return ResourceList.of((List)filtered); + return ResourceList.of((List) filtered); } @SuppressWarnings({"unchecked", "rawtypes"}) - protected ValidationChain newResourceValidationChain() { - return new ValidationChain((List)extensionFactory.getAllExtensions(Validation.class, Qualifiers.enabled())); + protected ValidationChain newResourceValidationChain(ProviderSelectionContext context) { + return new ValidationChain((List) extensionFactory.getAllExtensions(Validation.class, Qualifiers.enabled(), context)); } @SuppressWarnings({"unchecked", "rawtypes"}) - protected TransformationChain newResourceTransformationChain() { - return new TransformationChain((List)extensionFactory.getAllExtensions(Transformation.class, Qualifiers.enabled())); + protected TransformationChain newResourceTransformationChain(ProviderSelectionContext context) { + return new TransformationChain((List) extensionFactory.getAllExtensions(Transformation.class, Qualifiers.enabled(), context)); } - protected CombineChangeReporter newCombineReporter() { - return new CombineChangeReporter(extensionFactory.getAllExtensions(ChangeReporter.class, Qualifiers.enabled())); + protected CombineChangeReporter newCombineReporter(ProviderSelectionContext context) { + return new CombineChangeReporter(extensionFactory.getAllExtensions(ChangeReporter.class, Qualifiers.enabled(), context)); } - protected ConverterChain getConverterChain() { - @SuppressWarnings("rawtypes") - List converters = extensionFactory.getAllExtensions( - Converter.class, - Qualifiers.enabled()); - return new ConverterChain(converters); + protected ConverterChain newConverterChain(ProviderSelectionContext context) { + return new ConverterChain(extensionFactory.getAllExtensions(Converter.class, Qualifiers.enabled(), context)); } protected ResourceList addAllResourcesFromRepositories(HasItems resources) { @@ -288,15 +317,36 @@ protected ResourceList addAllResourcesFromRepositories(HasItems res return ResourceList.of(Stream.concat(stream, resources.getItems().stream()).toList()); } + /** + * Creates a ProviderSelectionContext from a ReconciliationContext. + * + * @param context the reconciliation context. + * @return the provider selection context, or null if no provider name is specified. + */ + protected ProviderSelectionContext createProviderContext(@NotNull ReconciliationContext context) { + return createProviderContext(context.providerName()); + } + + /** + * Creates a ProviderSelectionContext from a provider name. + * + * @param providerName the provider name. + * @return the provider selection context, or null if no provider name is specified. + */ + protected ProviderSelectionContext createProviderContext(String providerName) { + return Strings.isNullOrEmpty(providerName) ? null : ProviderSelectionContext.of(providerName, providerConfigurationRegistry); + } + @SuppressWarnings("unchecked") - protected Collector getMatchingCollector(@NotNull ResourceType resource) { + protected Collector getMatchingCollector(@NotNull ResourceType resource, + ProviderSelectionContext providerContext) { LOG.info("Looking for a collector accepting resource type: group={}, version={} and kind={}", resource.group(), resource.apiVersion(), resource.kind() ); return extensionFactory. - findExtension(Collector.class, Qualifiers.bySupportedResource(resource)) + findExtension(Collector.class, Qualifiers.bySupportedResource(resource), providerContext) .orElseThrow(() -> new JikkouRuntimeException(String.format( "Cannot find collector for resource type: group='%s', apiVersion='%s' and kind='%s", resource.group(), @@ -306,14 +356,15 @@ protected Collector getMatchingCollector(@NotNull Res } @SuppressWarnings("unchecked") - protected Controller getMatchingController(@NotNull ResourceType resource) { + protected Controller getMatchingController(@NotNull ResourceType resource, + ProviderSelectionContext providerContext) { LOG.info("Looking for a controller accepting resource type: group={}, apiVersion={} and kind={}", resource.group(), resource.apiVersion(), resource.kind() ); return extensionFactory - .findExtension(Controller.class, Qualifiers.bySupportedResource(resource)) + .findExtension(Controller.class, Qualifiers.bySupportedResource(resource), providerContext) .orElseThrow(() -> new JikkouRuntimeException(String.format( "Cannot find controller for resource type: group='%s', apiVersion='%s' and kind='%s", resource.group(), diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/DefaultApi.java b/core/src/main/java/io/streamthoughts/jikkou/core/DefaultApi.java index 9cb7e7e91..dc60c6e69 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/DefaultApi.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/DefaultApi.java @@ -14,6 +14,7 @@ import io.streamthoughts.jikkou.core.extension.ExtensionCategory; import io.streamthoughts.jikkou.core.extension.ExtensionDescriptor; import io.streamthoughts.jikkou.core.extension.ExtensionFactory; +import io.streamthoughts.jikkou.core.extension.ProviderConfigurationRegistry; import io.streamthoughts.jikkou.core.extension.exceptions.NoSuchExtensionException; import io.streamthoughts.jikkou.core.extension.qualifier.Qualifiers; import io.streamthoughts.jikkou.core.health.Health; @@ -125,7 +126,7 @@ public Builder(@NotNull ExtensionFactory extensionFactory, **/ @Override public DefaultApi build() { - return new DefaultApi(extensionFactory, resourceRegistry); + return new DefaultApi(extensionFactory, resourceRegistry, providerConfigurationRegistry); } } @@ -136,8 +137,9 @@ public DefaultApi build() { * Creates a new {@link DefaultApi} instance. */ private DefaultApi(@NotNull final ExtensionFactory extensionFactory, - @NotNull final ResourceRegistry resourceRegistry) { - super(extensionFactory); + @NotNull final ResourceRegistry resourceRegistry, + @NotNull final ProviderConfigurationRegistry providerConfigurationRegistry) { + super(extensionFactory, providerConfigurationRegistry); this.resourceRegistry = Objects.requireNonNull(resourceRegistry, "resourceRegistry must not be null"); } @@ -246,8 +248,9 @@ public ApiHealthIndicatorList getApiHealthIndicators() { **/ @Override public ApiHealthResult getApiHealth(@NotNull String name, - @NotNull Duration timeout) { - Health health = getHealth(name, timeout); + @NotNull Duration timeout, + String providerName) { + Health health = getHealth(name, timeout, providerName); return ApiHealthResult.from(health); } @@ -255,10 +258,10 @@ public ApiHealthResult getApiHealth(@NotNull String name, * {@inheritDoc} **/ @Override - public ApiHealthResult getApiHealth(@NotNull Duration timeout) { + public ApiHealthResult getApiHealth(@NotNull Duration timeout, String providerName) { ApiHealthIndicatorList list = getApiHealthIndicators(); List health = list.indicators().stream() - .map(indicator -> getHealth(indicator.name(), timeout)) + .map(indicator -> getHealth(indicator.name(), timeout, providerName)) .toList(); HealthAggregator aggregator = new HealthAggregator(); @@ -267,10 +270,12 @@ public ApiHealthResult getApiHealth(@NotNull Duration timeout) { } - private Health getHealth(@NotNull String name, @NotNull Duration timeout) { + private Health getHealth(@NotNull String name, @NotNull Duration timeout, String providerName) { + ProviderSelectionContext providerContext = createProviderContext(providerName); HealthIndicator extension = extensionFactory.getExtension( HealthIndicator.class, - Qualifiers.byName(name) + Qualifiers.byName(name), + providerContext ); Health health; try { @@ -370,7 +375,6 @@ private static ApiExtensionList getApiExtensionList(List all = addAllResourcesFromRepositories(resources); @@ -391,7 +395,6 @@ public ApiChangeResultList reconcile(@NotNull final HasItems resources, public ApiChangeResultList patch(@NotNull HasItems resources, @NotNull ReconciliationMode mode, @NotNull ReconciliationContext context) { - // Load resources from repositories ResourceList all = addAllResourcesFromRepositories(resources); @@ -453,17 +456,18 @@ private ApiChangeResultList doPatch(List policies, List reportable = results.stream() .filter(t -> !CoreAnnotations.isAnnotatedWithNoReport(t.change())) .collect(Collectors.toList()); - CombineChangeReporter reporter = newCombineReporter(); + CombineChangeReporter reporter = newCombineReporter(createProviderContext(context)); reporter.report(reportable); } return new ApiChangeResultList(context.isDryRun(), new ObjectMeta(), results); } private List applyPatchesAndGetResults(@NotNull ReconciliationMode mode, @NotNull ReconciliationContext context, List changes) { + ProviderSelectionContext providerContext = createProviderContext(context); Map> changesGroupByResourceType = ResourceList.of(changes).groupBy(ResourceType::of); return changesGroupByResourceType.entrySet().stream().flatMap(entry -> { ResourceType type = entry.getKey(); - Controller controller = getMatchingController(type); + Controller controller = getMatchingController(type, providerContext); List items = entry.getValue(); LOG.info("Applying {} changes for group={}, apiVersion={} and kind={} using controller: '{}' (mode: {}, dryRun: {}).", items.size(), @@ -495,15 +499,17 @@ private List applyPatchesAndGetResults(@NotNull ReconciliationMode @Override @SuppressWarnings("unchecked") public ApiActionResultSet execute(@NotNull String name, - @NotNull Configuration configuration) { + @NotNull Configuration configuration, + String providerName) { Objects.requireNonNull(name, "name cannot be null"); Objects.requireNonNull(configuration, "configuration cannot be null"); - Action action = getMatchingAction(name); + Action action = getMatchingAction(name, providerName); if (LOG.isDebugEnabled()) { LOG.debug( - "Executing action '{}' with configuration: {}", + "Executing action '{}' with configuration: {}{}", name, - configuration.asMap() + configuration.asMap(), + providerName != null ? " (provider: " + providerName + ")" : "" ); } ExecutionResultSet resultSet = action.execute(configuration); @@ -516,14 +522,18 @@ public ApiActionResultSet execute(@NotNull String nam } @SuppressWarnings("unchecked") - private Action getMatchingAction(@NotNull String action) { - LOG.info("Looking for an action named '{}'", action); + private Action getMatchingAction(@NotNull String action, String providerName) { + LOG.info("Looking for an action named '{}'{}", + action, + providerName != null ? " (provider: " + providerName + ")" : "" + ); + ProviderSelectionContext providerContext = createProviderContext(providerName); return extensionFactory.findExtension( Action.class, Qualifiers.byQualifiers( Qualifiers.byName(action) - ) - + ), + providerContext ).orElseThrow(() -> new NoSuchExtensionException(String.format("Cannot find action '%s'", action))); } @@ -543,7 +553,7 @@ public ApiValidationResult validate(final @NotNull HasItems resourc @NotNull private ApiValidationResult doValidate(@NotNull HasItems resources, @NotNull ReconciliationContext context) { List items = doPrepare(resources, context).getItems(); - ValidationResult validationChainResult = this.newResourceValidationChain().validate(items); + ValidationResult validationChainResult = this.newResourceValidationChain(createProviderContext(context)).validate(items); // Get and apply all policies List policies = getResourcePoliciesFrom(resources); @@ -575,10 +585,12 @@ private List getResourcePoliciesFrom(@NotNull HasItems resources @Override public T getResource(@NotNull ResourceType type, @NotNull String name, - @NotNull Configuration configuration) { - Collector collector = getMatchingCollector(type); + @NotNull GetContext context) { + // Create provider context from GetContext + ProviderSelectionContext providerContext = createProviderContext(context.providerName()); + Collector collector = getMatchingCollector(type, providerContext); final OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC); - return collector.get(name, configuration) + return collector.get(name, context.configuration()) .map(item -> addBuiltInAnnotations(item, timestamp)) .orElseThrow(() -> new ResourceNotFoundException( String.format( @@ -618,12 +630,15 @@ private ApiResourceChangeList doDiff(@NotNull HasItems resources, // Validate resources. Map> resourcesByType = doValidate(filtered, context).get().groupByType(); + // Create provider context + ProviderSelectionContext providerContext = createProviderContext(context); + // Diff List results = resourcesByType.entrySet() .stream() .map(e -> { final ResourceType type = e.getKey(); - Controller controller = getMatchingController(type); + Controller controller = getMatchingController(type, providerContext); List items = e.getValue(); LOG.info("Planning changes of {} resources for group={}, apiVersion={} and kind={} using controller: '{}'.", items.size(), @@ -646,9 +661,13 @@ private ApiResourceChangeList doDiff(@NotNull HasItems resources, @Override @SuppressWarnings("unchecked") public ResourceList listResources(final @NotNull ResourceType type, - final @NotNull Selector selector, - final @NotNull Configuration configuration) { - final Collector collector = getMatchingCollector(type); + final @NotNull ListContext context) { + // Create provider context from ListContext + ProviderSelectionContext providerContext = createProviderContext(context.providerName()); + final Collector collector = getMatchingCollector(type, providerContext); + + Selector selector = context.selector(); + Configuration configuration = context.configuration(); ResourceList result = collector.listAll(configuration, selector); diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/GetContext.java b/core/src/main/java/io/streamthoughts/jikkou/core/GetContext.java new file mode 100644 index 000000000..4e7bff5af --- /dev/null +++ b/core/src/main/java/io/streamthoughts/jikkou/core/GetContext.java @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core; + +import io.streamthoughts.jikkou.common.annotation.InterfaceStability; +import io.streamthoughts.jikkou.core.config.Configuration; +import org.jetbrains.annotations.NotNull; + +/** + * Represents the context of a get resource operation. + * + * @see JikkouApi + */ +@InterfaceStability.Evolving +public interface GetContext { + + /** + * Returns the {@link Configuration} used for executing a specific resource get operation. + * + * @return the options to be used for getting a resource. + */ + @NotNull Configuration configuration(); + + /** + * Returns the selected provider name for this get operation. + * + * @return the provider name, or null if not specified. + */ + String providerName(); + + /** + * Gets a new GetContext builder. + * + * @return a new {@link Builder} instance. + */ + static Builder builder() { + return new Builder(); + } + + /** + * An immutable class for building a new GetContext. + */ + class Builder { + + private final GetContext internal; + + private Builder() { + this(Default.EMPTY); + } + + private Builder(GetContext context) { + this.internal = context; + } + + /** + * Returns a new builder with the given configuration. + * + * @param configuration the configuration + * @return a new {@link Builder} + */ + public Builder configuration(Configuration configuration) { + return new Builder(new Default( + configuration, + internal.providerName() + )); + } + + /** + * Returns a new builder with the given provider name. + * + * @param providerName the provider name + * @return a new {@link Builder} + */ + public Builder providerName(String providerName) { + return new Builder(new Default( + internal.configuration(), + providerName + )); + } + + /** + * Builds a new {@link GetContext} instance. + * + * @return {@link GetContext} instance. + */ + public GetContext build() { + return internal; + } + } + + /** + * A default {@link GetContext} implementation. + * + * @param configuration The configuration for getting a resource. + * @param providerName The selected provider name for this get operation. + */ + record Default(Configuration configuration, + String providerName) + implements GetContext { + + public static Default EMPTY = new Default( + Configuration.empty(), + null + ); + } +} diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/JikkouApi.java b/core/src/main/java/io/streamthoughts/jikkou/core/JikkouApi.java index bc066d950..1ba856d5d 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/JikkouApi.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/JikkouApi.java @@ -14,7 +14,6 @@ import io.streamthoughts.jikkou.core.extension.ExtensionCategory; import io.streamthoughts.jikkou.core.extension.ExtensionDescriptorModifier; import io.streamthoughts.jikkou.core.extension.exceptions.ConflictingExtensionDefinitionException; -import io.streamthoughts.jikkou.core.health.HealthIndicator; import io.streamthoughts.jikkou.core.models.ApiActionResultSet; import io.streamthoughts.jikkou.core.models.ApiChangeResultList; import io.streamthoughts.jikkou.core.models.ApiExtension; @@ -33,7 +32,6 @@ import io.streamthoughts.jikkou.core.reconciler.Collector; import io.streamthoughts.jikkou.core.reconciler.ResourceChangeFilter; import io.streamthoughts.jikkou.core.selector.Selector; -import io.streamthoughts.jikkou.core.selector.Selectors; import io.streamthoughts.jikkou.spi.ExtensionProvider; import java.time.Duration; import java.util.List; @@ -67,6 +65,18 @@ default B register(@NotNull ExtensionProvider provider) { return register(provider, Configuration.empty()); } + /** + * Registers a provider configuration. + * + * @param providerName the name of the provider instance (e.g., "kafka-prod", "kafka-dev") + * @param providerType the fully qualified class name of the provider type + * @param configuration the configuration for this provider instance + * @param isDefault whether this is the default provider when multiple instances exist + */ + void registerProviderConfiguration(@NotNull String providerName, + @NotNull String providerType, + @NotNull Configuration configuration, + boolean isDefault); /** * Registers an extension provider with the given configuration. @@ -160,22 +170,50 @@ ApiResourceList listApiResources(@NotNull String group, * Gets the health details for the specified health indicator name. * * @param name the health indicator name. - * @return a new {@link HealthIndicator} instance. + * @param timeout the timeout duration. + * @return a new {@link ApiHealthResult} instance. + */ + default ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout) { + return getApiHealth(name, timeout, null); + } + + /** + * Gets the health details for the specified health indicator name with provider selection. + * + * @param name the health indicator name. + * @param timeout the timeout duration. + * @param providerName the provider name for selecting specific provider instance. + * @return a new {@link ApiHealthResult} instance. + * @since 0.37.0 */ - ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout); + ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout, String providerName); /** * Gets the health details for all supported health indicators. * - * @return a new {@link HealthIndicator} instance. + * @param timeout the timeout duration. + * @return a new {@link ApiHealthResult} instance. + */ + default ApiHealthResult getApiHealth(@NotNull Duration timeout) { + return getApiHealth(timeout, null); + } + + /** + * Gets the health details for all supported health indicators with provider selection. + * + * @param timeout the timeout duration. + * @param providerName the provider name for selecting specific provider instance. + * @return a new {@link ApiHealthResult} instance. + * @since 0.37.0 */ - ApiHealthResult getApiHealth(@NotNull Duration timeout); + ApiHealthResult getApiHealth(@NotNull Duration timeout, String providerName); /** * Gets the health details for the specified {@link ApiHealthIndicator}. * * @param indicator the {@link ApiHealthIndicator}. - * @return a new {@link HealthIndicator} instance. + * @param timeout the timeout duration. + * @return a new {@link ApiHealthResult} instance. */ default ApiHealthResult getApiHealth(@NotNull ApiHealthIndicator indicator, @NotNull Duration timeout) { @@ -277,7 +315,23 @@ ApiChangeResultList patch(@NotNull HasItems resources, * @param configuration The configuration. * @return The ApiExecutionResult. */ - ApiActionResultSet execute(@NotNull String action, @NotNull Configuration configuration); + default ApiActionResultSet execute(@NotNull String action, + @NotNull Configuration configuration) { + return execute(action, configuration, null); + } + + /** + * Executes the specified action for the specified resource type with provider selection. + * + * @param action The name of the action. + * @param configuration The configuration. + * @param providerName The provider name for selecting specific provider instance. + * @return The ApiExecutionResult. + * @since 0.37.0 + */ + ApiActionResultSet execute(@NotNull String action, + @NotNull Configuration configuration, + String providerName); /** * Execute validations on the given resources. @@ -318,14 +372,19 @@ HasItems prepare(final @NotNull HasItems resources, * * @param type The class of the resource to be described. * @param name The name of the resource. + * @param configuration The configuration. * @return the {@link HasMetadata}. * @throws JikkouApiException if no {@link Collector} can be found for the specified type, * or more than one descriptor match the type. + * @deprecated Use {@link #getResource(ResourceType, String, GetContext)} instead. */ + @Deprecated(since = "0.37.0", forRemoval = true) default T getResource(@NotNull Class type, @NotNull String name, @NotNull Configuration configuration) { - return getResource(ResourceType.of(type), name, configuration); + return getResource(ResourceType.of(type), name, GetContext.builder() + .configuration(configuration) + .build()); } /** @@ -333,14 +392,37 @@ default T getResource(@NotNull Class T getResource(@NotNull ResourceType type, + @NotNull String name, + @NotNull Configuration configuration) { + return getResource(type, name, GetContext.builder() + .configuration(configuration) + .build()); + } + + /** + * Get the resource associated for the specified type. + * + * @param type The type of the resource to be described. + * @param name The name of the resource. + * @param context The get context containing configuration and provider. + * @return the {@link HasMetadata}. + * @throws JikkouApiException if no {@link Collector} can be found for the specified type, + * or more than one descriptor match the type. + * @throws ResourceNotFoundException if no resource can be found for the given name. + * @since 0.37.0 */ T getResource(@NotNull ResourceType type, @NotNull String name, - @NotNull Configuration configuration); + @NotNull GetContext context); /** * Get all the changes for the given resources. @@ -372,35 +454,42 @@ ApiResourceChangeList getDiff(@NotNull HasItems resources, * or more than one descriptor match the type. */ default ResourceList listResources(@NotNull Class resourceClass) { - return listResources(resourceClass, Selectors.NO_SELECTOR, Configuration.empty()); + return listResources(ResourceType.of(resourceClass), ListContext.Default.EMPTY); } /** * List the resources associated for the specified type. * * @param resourceClass the class of the resource to be described. + * @param selector the selector to filter resources. * @return the {@link HasMetadata}. * @throws JikkouApiException if no {@link Collector} can be found for the specified type, * or more than one descriptor match the type. */ default ResourceList listResources(@NotNull Class resourceClass, @NotNull Selector selector) { - return listResources(resourceClass, selector, Configuration.empty()); + return listResources(ResourceType.of(resourceClass), ListContext.builder().selector(selector).build()); } /** * List the resources associated for the specified type. * * @param type the class of the resource to be described. + * @param selector the selector to filter resources. * @param configuration the configuration to be used for describing the resource-type. * @return the {@link HasMetadata}. * @throws JikkouApiException if no {@link Collector} can be found for the specified type, * or more than one descriptor match the type. + * @deprecated Use {@link #listResources(ResourceType, ListContext)} instead. */ + @Deprecated(since = "0.37.0", forRemoval = true) default ResourceList listResources(@NotNull Class type, @NotNull Selector selector, @NotNull Configuration configuration) { - return listResources(ResourceType.of(type), selector, configuration); + return listResources(ResourceType.of(type), ListContext.builder() + .selector(selector) + .configuration(configuration) + .build()); } /** @@ -410,30 +499,50 @@ default ResourceList listResources(@NotNull Class ResourceList listResources(@NotNull ResourceType resourceType) { - return listResources(resourceType, Selectors.NO_SELECTOR, Configuration.empty()); + return listResources(resourceType, ListContext.Default.EMPTY); } /** * List the resources associated for the specified type. * * @param resourceType the type of the resource to be described. + * @param selector the selector to filter resources. * @return the {@link HasMetadata}. */ default ResourceList listResources(@NotNull ResourceType resourceType, @NotNull Selector selector) { - return listResources(resourceType, selector, Configuration.empty()); + return listResources(resourceType, ListContext.builder().selector(selector).build()); } /** * List the resources associated for the specified type. * * @param resourceType the type of the resource to be described. + * @param selector the selector to filter resources. * @param configuration the option to be used for describing the resource-type. * @return the {@link HasMetadata}. + * @deprecated Use {@link #listResources(ResourceType, ListContext)} instead. + */ + @Deprecated(since = "0.37.0", forRemoval = true) + default ResourceList listResources(@NotNull ResourceType resourceType, + @NotNull Selector selector, + @NotNull Configuration configuration) { + return listResources(resourceType, ListContext.builder() + .selector(selector) + .configuration(configuration) + .build()); + } + + /** + * List the resources associated for the specified type. + * + * @param resourceType the type of the resource to be described. + * @param context the list context containing selector, configuration and provider. + * @return the {@link HasMetadata}. + * @since 0.37.0 */ ResourceList listResources(@NotNull ResourceType resourceType, - @NotNull Selector selector, - @NotNull Configuration configuration); + @NotNull ListContext context); @SuppressWarnings("rawtypes") ApiBuilder toBuilder(); diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/ListContext.java b/core/src/main/java/io/streamthoughts/jikkou/core/ListContext.java new file mode 100644 index 000000000..9de7357d4 --- /dev/null +++ b/core/src/main/java/io/streamthoughts/jikkou/core/ListContext.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core; + +import io.streamthoughts.jikkou.common.annotation.InterfaceStability; +import io.streamthoughts.jikkou.core.config.Configuration; +import io.streamthoughts.jikkou.core.selector.Selector; +import io.streamthoughts.jikkou.core.selector.Selectors; +import org.jetbrains.annotations.NotNull; + +/** + * Represents the context of a list operation. + * + * @see JikkouApi + */ +@InterfaceStability.Evolving +public interface ListContext { + + /** + * Returns the {@link Selector} used for filtering the resources. + * + * @return the {@link Selector}. + */ + @NotNull Selector selector(); + + /** + * Returns the {@link Configuration} used for executing a specific resource list operation. + * + * @return the options to be used for listing resources. + */ + @NotNull Configuration configuration(); + + /** + * Returns the selected provider name for this list operation. + * + * @return the provider name, or null if not specified. + */ + String providerName(); + + /** + * Gets a new ListContext builder. + * + * @return a new {@link Builder} instance. + */ + static Builder builder() { + return new Builder(); + } + + /** + * An immutable class for building a new ListContext. + */ + class Builder { + + private final ListContext internal; + + private Builder() { + this(Default.EMPTY); + } + + private Builder(ListContext context) { + this.internal = context; + } + + /** + * Returns a new builder with the given configuration. + * + * @param configuration the configuration + * @return a new {@link Builder} + */ + public Builder configuration(Configuration configuration) { + return new Builder(new Default( + internal.selector(), + configuration, + internal.providerName() + )); + } + + /** + * Returns a new builder with the given selector. + * + * @param selector The selector + * @return a new {@link Builder} + */ + public Builder selector(Selector selector) { + return new Builder(new Default( + selector, + internal.configuration(), + internal.providerName() + )); + } + + /** + * Returns a new builder with the given provider name. + * + * @param providerName the provider name + * @return a new {@link Builder} + */ + public Builder providerName(String providerName) { + return new Builder(new Default( + internal.selector(), + internal.configuration(), + providerName + )); + } + + /** + * Builds a new {@link ListContext} instance. + * + * @return {@link ListContext} instance. + */ + public ListContext build() { + return internal; + } + } + + /** + * A default {@link ListContext} implementation. + * + * @param selector The selector to filter resources. + * @param configuration The configuration for listing resources. + * @param providerName The selected provider name for this list operation. + */ + record Default(Selector selector, + Configuration configuration, + String providerName) + implements ListContext { + + public static Default EMPTY = new Default( + Selectors.NO_SELECTOR, + Configuration.empty(), + null + ); + } +} \ No newline at end of file diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/ProviderSelectionContext.java b/core/src/main/java/io/streamthoughts/jikkou/core/ProviderSelectionContext.java new file mode 100644 index 000000000..bf81d9f26 --- /dev/null +++ b/core/src/main/java/io/streamthoughts/jikkou/core/ProviderSelectionContext.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core; + +import io.streamthoughts.jikkou.common.annotation.InterfaceStability; +import io.streamthoughts.jikkou.common.utils.Strings; +import io.streamthoughts.jikkou.core.extension.ProviderConfigurationRegistry; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Context for provider selection during extension creation. + * This context flows through the extension system to enable provider-specific configuration selection. + * + * @since 0.37.0 + */ +@InterfaceStability.Evolving +public record ProviderSelectionContext( + @Nullable String selectedProvider, + @NotNull ProviderConfigurationRegistry registry) { + + /** + * Creates an empty provider selection context with no specific provider selected. + * + * @param registry the provider configuration registry + * @return an empty ProviderSelectionContext + */ + public static ProviderSelectionContext empty(@NotNull ProviderConfigurationRegistry registry) { + return new ProviderSelectionContext(null, registry); + } + + /** + * Creates a provider selection context with a specific provider selected. + * Validates that the provider exists in the registry. + * + * @param providerName the selected provider name + * @param registry the provider configuration registry + * @return a ProviderSelectionContext with the selected provider + * @throws IllegalArgumentException if the provider doesn't exist + */ + public static ProviderSelectionContext of(String providerName, + @NotNull ProviderConfigurationRegistry registry) { + if (Strings.isNullOrEmpty(providerName)) { + return empty(registry); + } + + // Validate provider exists + Set availableProviders = registry.getAllProviderNames(); + if (!availableProviders.contains(providerName)) { + throw new IllegalArgumentException(String.format( + "Provider '%s' not found. Available providers: %s.", + providerName, + availableProviders + )); + } + + return new ProviderSelectionContext(providerName, registry); + } + + /** + * Checks if a specific provider is selected. + * + * @return true if a provider is selected, false otherwise + */ + public boolean hasSelectedProvider() { + return !Strings.isNullOrEmpty(selectedProvider); + } +} diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/ReconciliationContext.java b/core/src/main/java/io/streamthoughts/jikkou/core/ReconciliationContext.java index 9cfb7cdcf..9b39da22d 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/ReconciliationContext.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/ReconciliationContext.java @@ -58,6 +58,13 @@ public interface ReconciliationContext { */ boolean isDryRun(); + /** + * Returns the selected provider name for this reconciliation operation. + * + * @return the provider name, or null if not specified. + */ + String providerName(); + /** * Gets a new ReconciliationContext builder. * @@ -94,7 +101,8 @@ public Builder configuration(Configuration configuration) { configuration, internal.isDryRun(), internal.labels(), - internal.annotations() + internal.annotations(), + internal.providerName() )); } @@ -110,7 +118,8 @@ public Builder dryRun(boolean dryRun) { internal.configuration(), dryRun, internal.labels(), - internal.annotations() + internal.annotations(), + internal.providerName() )); } @@ -126,7 +135,8 @@ public Builder selector(Selector selector) { internal.configuration(), internal.isDryRun(), internal.labels(), - internal.annotations() + internal.annotations(), + internal.providerName() )); } @@ -142,7 +152,8 @@ public Builder labels(Iterable labels) { internal.configuration(), internal.isDryRun(), NamedValueSet.setOf(labels), - internal.annotations() + internal.annotations(), + internal.providerName() )); } @@ -158,7 +169,8 @@ public Builder label(NamedValue label) { internal.configuration(), internal.isDryRun(), NamedValueSet.setOf(internal.labels()).with(label), - internal.annotations() + internal.annotations(), + internal.providerName() )); } @@ -173,7 +185,8 @@ public Builder annotations(Iterable annotations) { internal.configuration(), internal.isDryRun(), internal.labels(), - NamedValueSet.setOf(annotations) + NamedValueSet.setOf(annotations), + internal.providerName() )); } @@ -188,7 +201,25 @@ public Builder annotation(NamedValue annotation) { internal.configuration(), internal.isDryRun(), internal.labels(), - NamedValueSet.setOf(internal.annotations()).with(annotation) + NamedValueSet.setOf(internal.annotations()).with(annotation), + internal.providerName() + )); + } + + /** + * Returns a new builder with the given provider name. + * + * @param providerName the provider name + * @return a new {@link Builder} + */ + public Builder providerName(String providerName) { + return new Builder(new Default( + internal.selector(), + internal.configuration(), + internal.isDryRun(), + internal.labels(), + internal.annotations(), + providerName )); } @@ -209,12 +240,14 @@ public ReconciliationContext build() { * @param selector The selector to filter resources to be included in the reconciliation. * @param configuration The config for computing resource changes. * @param isDryRun Specify if the reconciliation should be run in dry-run. + * @param providerName The selected provider name for this reconciliation operation. */ record Default(Selector selector, Configuration configuration, boolean isDryRun, NamedValueSet labels, - NamedValueSet annotations) + NamedValueSet annotations, + String providerName) implements ReconciliationContext { public static Default EMPTY = new Default( @@ -222,7 +255,8 @@ record Default(Selector selector, Configuration.empty(), true, NamedValueSet.emptySet(), - NamedValueSet.emptySet() + NamedValueSet.emptySet(), + null ); } } diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionContext.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionContext.java index a3638b29a..49f10954e 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionContext.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionContext.java @@ -6,13 +6,14 @@ */ package io.streamthoughts.jikkou.core.extension; +import io.streamthoughts.jikkou.core.ProviderSelectionContext; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.extension.exceptions.NoSuchExtensionException; import io.streamthoughts.jikkou.spi.ExtensionProvider; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; -import java.util.function.Supplier; +import org.jetbrains.annotations.Nullable; /** * Default {@link ExtensionContext}. @@ -21,16 +22,21 @@ public final class DefaultExtensionContext implements ExtensionContext { private final ExtensionFactory factory; private final ExtensionDescriptor descriptor; + private final ProviderSelectionContext providerContext; /** * Creates a new {@link DefaultExtensionContext} instance. * - * @param descriptor The ExtensionDescriptor + * @param factory The ExtensionFactory + * @param descriptor The ExtensionDescriptor + * @param providerContext The ProviderSelectionContext (optional) */ public DefaultExtensionContext(final ExtensionFactory factory, - final ExtensionDescriptor descriptor) { + final ExtensionDescriptor descriptor, + @Nullable final ProviderSelectionContext providerContext) { this.factory = factory; this.descriptor = Objects.requireNonNull(descriptor, "descriptor cannot be null"); + this.providerContext = providerContext; } /** @@ -56,7 +62,7 @@ public Configuration configuration() { public ExtensionContext contextForExtension(Class extension) { if (factory == null) throw new IllegalStateException("No factory configured"); return factory.findDescriptorByClass(extension) - .map(descriptor -> new DefaultExtensionContext(factory, descriptor)) + .map(descriptor -> new DefaultExtensionContext(factory, descriptor, providerContext)) .orElseThrow(() -> new NoSuchExtensionException("No extension registered for type: " + extension.getName())); } @@ -67,7 +73,7 @@ public ExtensionContext contextForExtension(Class extension @SuppressWarnings("unchecked") public T provider() { return (T) Optional.ofNullable(descriptor.providerSupplier()) - .map(Supplier::get) + .map(supplier -> supplier.get(providerContext)) .orElseThrow(() -> new NoSuchElementException("No provider registered for extension: " + descriptor.name())); } diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionDescriptor.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionDescriptor.java index 6d66a6388..ddcb2531f 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionDescriptor.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionDescriptor.java @@ -32,7 +32,7 @@ public class DefaultExtensionDescriptor implements ExtensionDescriptor { private final ExtensionCategory category; private final List properties; private final Class provider; - private final Supplier providerSupplier; + private final ProviderSupplier providerSupplier; private final Class type; private final Supplier supplier; private final boolean isEnabled; @@ -60,7 +60,7 @@ public DefaultExtensionDescriptor(final String name, final ExtensionCategory category, final List properties, final Class provider, - final Supplier providerSupplier, + final ProviderSupplier providerSupplier, final Class type, final ClassLoader classLoader, final Supplier supplier, @@ -179,7 +179,7 @@ public Class provider() { * {@inheritDoc} */ @Override - public Supplier providerSupplier() { + public ProviderSupplier providerSupplier() { return providerSupplier; } diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionFactory.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionFactory.java index 89630d8d6..0d30f889d 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionFactory.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionFactory.java @@ -6,6 +6,7 @@ */ package io.streamthoughts.jikkou.core.extension; +import io.streamthoughts.jikkou.core.ProviderSelectionContext; import io.streamthoughts.jikkou.core.extension.exceptions.NoSuchExtensionException; import java.util.Collection; import java.util.Comparator; @@ -77,6 +78,16 @@ public T getExtension(@NotNull Class type) { @Override public T getExtension(@NotNull Class type, @Nullable Qualifier qualifier) { + return getExtension(type, qualifier, null); + } + + /** + * {@inheritDoc} + **/ + @Override + public T getExtension(@NotNull Class type, + @Nullable Qualifier qualifier, + @Nullable ProviderSelectionContext providerContext) { Optional> optional = registry.findDescriptorByClass(type, qualifier); if (optional.isEmpty()) { String error = qualifier != null ? @@ -85,7 +96,7 @@ public T getExtension(@NotNull Class type, throw new NoSuchExtensionException(error); } ExtensionDescriptor descriptor = optional.get(); - return registry.getExtensionSupplier(descriptor).get(this); + return registry.getExtensionSupplier(descriptor).get(this, providerContext); } /** @@ -93,9 +104,19 @@ public T getExtension(@NotNull Class type, **/ @Override public Optional findExtension(@NotNull Class type, @Nullable Qualifier qualifier) { + return findExtension(type, qualifier, null); + } + + /** + * {@inheritDoc} + **/ + @Override + public Optional findExtension(@NotNull Class type, + @Nullable Qualifier qualifier, + @Nullable ProviderSelectionContext providerContext) { return registry.findDescriptorByClass(type, qualifier) .map(registry::getExtensionSupplier) - .map(supplier -> supplier.get(this)); + .map(supplier -> supplier.get(this, providerContext)); } /** @@ -115,7 +136,7 @@ public T getExtension(@NotNull String type, Qualifier qualifier) { if (optional.isEmpty()) throw new NoSuchExtensionException("No extension registered for type '" + type + "'"); ExtensionDescriptor descriptor = optional.get(); - return registry.getExtensionSupplier(descriptor).get(this); + return registry.getExtensionSupplier(descriptor).get(this, null); } /** @@ -136,7 +157,7 @@ public Collection getAllExtensions(@NotNull String type, .stream() .sorted(Comparator.comparing(ExtensionDescriptor::priority)) .map(this.registry::getExtensionSupplier) - .map(supplier -> supplier.get(this)) + .map(supplier -> supplier.get(this, null)) .toList(); } @@ -154,10 +175,20 @@ public List getAllExtensions(@NotNull Class type) { @Override public List getAllExtensions(@NotNull Class type, @Nullable Qualifier qualifier) { + return getAllExtensions(type, qualifier, null); + } + + /** + * {@inheritDoc} + **/ + @Override + public List getAllExtensions(@NotNull Class type, + @Nullable Qualifier qualifier, + @Nullable ProviderSelectionContext providerContext) { return this.registry.findAllDescriptorsByClass(type, qualifier) .stream() .map(this.registry::getExtensionSupplier) - .map(supplier -> supplier.get(this)) + .map(supplier -> supplier.get(this, providerContext)) .toList(); } diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplier.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplier.java index ee145ee94..b1feff37a 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplier.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplier.java @@ -6,11 +6,12 @@ */ package io.streamthoughts.jikkou.core.extension; - +import io.streamthoughts.jikkou.core.ProviderSelectionContext; import io.streamthoughts.jikkou.core.extension.exceptions.ExtensionCreationException; import java.util.Objects; import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +34,7 @@ public DefaultExtensionSupplier(final @NotNull ExtensionDescriptor descriptor * {@inheritDoc} **/ @Override - public T get(ExtensionFactory factory) { + public T get(ExtensionFactory factory, @Nullable ProviderSelectionContext providerContext) { Supplier supplier = descriptor.supplier(); try { T t = supplier.get(); @@ -47,7 +48,7 @@ public T get(ExtensionFactory factory) { LOG.info("Initializing extension '{}'", extension.getName()); } - extension.init(new DefaultExtensionContext(factory, descriptor)); + extension.init(new DefaultExtensionContext(factory, descriptor, providerContext)); } return t; } catch (Exception e) { diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultProviderConfigurationRegistry.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultProviderConfigurationRegistry.java new file mode 100644 index 000000000..43a3c8035 --- /dev/null +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/DefaultProviderConfigurationRegistry.java @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core.extension; + +import io.streamthoughts.jikkou.common.utils.Strings; +import io.streamthoughts.jikkou.core.config.Configuration; +import io.streamthoughts.jikkou.core.exceptions.JikkouRuntimeException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default implementation of {@link ProviderConfigurationRegistry}. + * + * @since 0.37.0 + */ +public class DefaultProviderConfigurationRegistry implements ProviderConfigurationRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultProviderConfigurationRegistry.class); + + private final Map providerByName = new ConcurrentHashMap<>(); + private final Map> providerByType = new ConcurrentHashMap<>(); + private final Map defaultProviderByType = new ConcurrentHashMap<>(); + + @Override + public void registerProviderConfiguration(@NotNull String providerName, + @NotNull String providerType, + @NotNull Configuration configuration, + boolean isDefault) { + LOG.debug("Registering provider configuration: name={}, type={}, isDefault={}", providerName, providerType, isDefault); + + if (providerByName.containsKey(providerName)) { + throw new IllegalArgumentException("Configuration is already registered for provider name: " + providerName); + } + + providerByName.put(providerName, new ProviderEntry(providerType, configuration)); + providerByType.computeIfAbsent(providerType, unsued -> new ArrayList<>()).add(new ProviderEntry(providerType, configuration)); + + if (isDefault) { + String existing = defaultProviderByType.put(providerType, providerName); + if (existing != null && !existing.equals(providerName)) { + LOG.warn("Replacing default provider for type '{}': old='{}', new='{}'", + providerType, existing, providerName); + } + } + } + + @Override + public @NotNull Optional getProviderConfiguration(@NotNull String providerName) { + ProviderEntry entry = providerByName.get(providerName); + if (entry == null) { + LOG.debug("Provider configuration not found: name={}", providerName); + return Optional.empty(); + } + return Optional.of(entry.configuration()); + } + + @Override + public @NotNull Optional getDefaultConfiguration(@NotNull String providerType) { + String defaultProvider = defaultProviderByType.get(providerType); + if (defaultProvider == null) { + List providers = providerByType.get(providerType); + if (providers != null && providers.size() == 1) { + return Optional.of(providers.getFirst().configuration()); + } + LOG.debug("No default provider configured for type: {}", providerType); + return Optional.empty(); + } + return getProviderConfiguration(defaultProvider); + } + + @Override + public @NotNull Configuration getConfiguration(@NotNull String providerType, + @Nullable String providerName) { + Objects.requireNonNull(providerType, "providerType cannot be null"); + LOG.debug("Getting provider configuration: type={}, name={}", providerType, providerName); + if (Strings.isNullOrEmpty(providerName)) { + return getDefaultConfiguration(providerType) + .or(() -> getProviderNamesByType(providerType).isEmpty() ? Optional.of(Configuration.empty()) : Optional.empty()) + .orElseThrow(() -> new JikkouRuntimeException("No default configuration defined, and multiple configurations found for provider type: '%s'".formatted(providerType))); + } + return getProviderConfiguration(providerName) + .orElseThrow(() -> new JikkouRuntimeException("No provider configured for name: '%s'".formatted(providerName))); + } + + @Override + public @NotNull Set getAllProviderNames() { + return providerByName.keySet(); + } + + @Override + public @NotNull Set getProviderNamesByType(@NotNull String providerType) { + return providerByName.entrySet().stream() + .filter(entry -> entry.getValue().providerType().equals(providerType)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + /** + * Internal record to store provider configuration entries. + */ + private record ProviderEntry(String providerType, Configuration configuration) { + } +} diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/Extension.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/Extension.java index 561da623a..4c8801d89 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/Extension.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/Extension.java @@ -20,7 +20,7 @@ import org.jetbrains.annotations.NotNull; /** - * The top-level interface for extension. + * The top-level interface for extensions. * * @see Action * @see Resource diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptor.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptor.java index 01638810a..1a3f4cc1f 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptor.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptor.java @@ -101,7 +101,7 @@ public interface ExtensionDescriptor extends Comparable providerSupplier(); + ProviderSupplier providerSupplier(); /** * Adds new aliases to reference the described extension. diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiers.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiers.java index b3fa68dab..3e2cd26da 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiers.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiers.java @@ -9,7 +9,6 @@ import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.extension.builder.ExtensionDescriptorBuilder; import io.streamthoughts.jikkou.spi.ExtensionProvider; -import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; /** @@ -55,7 +54,7 @@ public ExtensionDescriptor apply(final ExtensionDescriptor descriptor) * @return a new {@link ExtensionDescriptorModifier} instance. */ public static ExtensionDescriptorModifier withProvider(@NotNull final Class provider, - @NotNull final Supplier supplier) { + @NotNull final ProviderSupplier supplier) { return new ExtensionDescriptorModifier() { @Override public ExtensionDescriptor apply(final ExtensionDescriptor descriptor) { diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionFactory.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionFactory.java index 0e3146f42..b7e67e6d7 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionFactory.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionFactory.java @@ -6,6 +6,7 @@ */ package io.streamthoughts.jikkou.core.extension; +import io.streamthoughts.jikkou.core.ProviderSelectionContext; import io.streamthoughts.jikkou.core.extension.exceptions.NoSuchExtensionException; import io.streamthoughts.jikkou.core.extension.exceptions.NoUniqueExtensionException; import java.util.Collection; @@ -156,6 +157,49 @@ Collection getAllExtensions(@NotNull final String type, List getAllExtensions(@NotNull final Class type, @Nullable final Qualifier qualifier); + // Provider context-aware methods + + /** + * Gets a new extension instance for the specified type with provider context. + * + * @param type the extension class. + * @param qualifier the options used to qualify the extension. + * @param providerContext the provider selection context. + * @param the type of the extension. + * @return the instance of type {@link T}. + * @throws NoUniqueExtensionException if more than one extension is registered for the given type. + * @throws NoSuchExtensionException if no extension is registered for the given type. + */ + T getExtension(@NotNull final Class type, + @Nullable final Qualifier qualifier, + @Nullable final ProviderSelectionContext providerContext); + + /** + * Finds a new extension instance for the specified type with provider context. + * + * @param type the extension class. + * @param qualifier the options used to qualify the extension. + * @param providerContext the provider selection context. + * @param the type of the extension. + * @return an optional instance of type {@link T}. + * @throws NoUniqueExtensionException if more than one extension is registered for the given type. + */ + Optional findExtension(@NotNull final Class type, + @Nullable final Qualifier qualifier, + @Nullable final ProviderSelectionContext providerContext); + + /** + * Gets all instances for the specified type and qualifier with provider context. + * + * @param type the extension class. + * @param qualifier the options used to qualify the extension. + * @param providerContext the provider selection context. + * @param the type of the extension. + * @return all instances of type {@link T}. + */ + List getAllExtensions(@NotNull final Class type, + @Nullable final Qualifier qualifier, + @Nullable final ProviderSelectionContext providerContext); /** * Duplicates this factory. diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionProviderAwareRegistry.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionProviderAwareRegistry.java index 58168ae01..8edf28ea3 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionProviderAwareRegistry.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionProviderAwareRegistry.java @@ -7,12 +7,14 @@ package io.streamthoughts.jikkou.core.extension; import io.streamthoughts.jikkou.common.utils.Classes; +import io.streamthoughts.jikkou.core.ProviderSelectionContext; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.spi.ExtensionProvider; import java.util.Arrays; import java.util.Objects; import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * ExtensionRegistry used to set extension provider. @@ -22,19 +24,24 @@ public final class ExtensionProviderAwareRegistry implements ExtensionRegistry { private final ExtensionRegistry delegate; private final Class provider; private final Configuration configuration; + private final ProviderConfigurationRegistry providerConfigurationRegistry; /** - * Creates a new {@link ExtensionProviderAwareRegistry} instance. + * Creates a new {@link ExtensionProviderAwareRegistry} instance with provider configuration registry. * * @param delegate The ExtensionRegistry to delegate to. * @param provider The extension group name. + * @param configuration The default configuration. + * @param providerConfigurationRegistry The provider configuration registry (optional). */ public ExtensionProviderAwareRegistry(@NotNull ExtensionRegistry delegate, @NotNull Class provider, - @NotNull Configuration configuration) { + @NotNull Configuration configuration, + @Nullable ProviderConfigurationRegistry providerConfigurationRegistry) { this.delegate = Objects.requireNonNull(delegate, "delegate cannot be null"); this.provider = Objects.requireNonNull(provider, "provider cannot be null"); this.configuration = Objects.requireNonNull(configuration, "configuration cannot be null"); + this.providerConfigurationRegistry = providerConfigurationRegistry; } /** @@ -60,27 +67,70 @@ public void register(@NotNull Class type, @NotNull private ExtensionDescriptorModifier newProviderModifier() { - return ExtensionDescriptorModifiers.withProvider(provider, new InternalExtensionProviderSupplier(provider, configuration)); + return ExtensionDescriptorModifiers.withProvider( + provider, + new InternalExtensionProviderSupplier(provider, configuration, providerConfigurationRegistry) + ); } - private static final class InternalExtensionProviderSupplier implements Supplier { + /** + * Internal supplier for creating ExtensionProvider instances with context-aware configuration selection. + */ + private static final class InternalExtensionProviderSupplier implements ProviderSupplier { private final Class clazz; - private final Configuration configuration; + private final Configuration defaultConfiguration; + private final ProviderConfigurationRegistry providerConfigurationRegistry; - public InternalExtensionProviderSupplier(Class clazz, Configuration configuration) { + public InternalExtensionProviderSupplier(Class clazz, + Configuration defaultConfiguration, + @Nullable ProviderConfigurationRegistry providerConfigurationRegistry) { this.clazz = clazz; - this.configuration = configuration; + this.defaultConfiguration = defaultConfiguration; + this.providerConfigurationRegistry = providerConfigurationRegistry; } /** - * {@inheritDoc} + * Gets a configured ExtensionProvider instance, optionally using a ProviderSelectionContext + * to select a specific provider configuration. + * + * @param providerContext the provider selection context (optional) + * @return a configured ExtensionProvider instance */ @Override - public ExtensionProvider get() { + public ExtensionProvider get(@Nullable ProviderSelectionContext providerContext) { + Configuration config = selectConfiguration(providerContext); ExtensionProvider provider = Classes.newInstance(clazz, clazz.getClassLoader()); - provider.configure(configuration); + provider.configure(config); return provider; } + + /** + * Selects the appropriate configuration based on the provider selection context. + * + * @param providerContext the provider selection context (optional) + * @return the selected configuration + */ + private Configuration selectConfiguration(@Nullable ProviderSelectionContext providerContext) { + // If no provider configuration registry is available, use default + if (providerConfigurationRegistry == null) { + return defaultConfiguration; + } + + // Get provider type name + String providerType = clazz.getName(); + + // Try to get selected provider from context + String selectedProvider = null; + if (providerContext != null && providerContext.hasSelectedProvider()) { + selectedProvider = providerContext.selectedProvider(); + } + + // Get configuration from registry + Configuration config = providerConfigurationRegistry.getConfiguration(providerType, selectedProvider); + + // Fallback to default configuration if not found in registry + return config.withFallback(defaultConfiguration); + } } } diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionSupplier.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionSupplier.java index 5742f7ea3..bd7d3a304 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionSupplier.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ExtensionSupplier.java @@ -6,7 +6,9 @@ */ package io.streamthoughts.jikkou.core.extension; +import io.streamthoughts.jikkou.core.ProviderSelectionContext; import io.streamthoughts.jikkou.core.extension.exceptions.ExtensionCreationException; +import org.jetbrains.annotations.Nullable; /** * Class for supplying extension instance. @@ -16,12 +18,14 @@ public interface ExtensionSupplier { /** - * Create a new extension instance. + * Create a new extension instance with provider context support. * + * @param factory the extension factory + * @param providerContext the provider selection context (optional) * @return a new instance of {@link T}. * @throws ExtensionCreationException if the extension cannot be created or configured. */ - T get(ExtensionFactory factor); + T get(ExtensionFactory factory, @Nullable ProviderSelectionContext providerContext); /** * Gets the descriptor for the extension supplied by this class. diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ProviderConfigurationRegistry.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ProviderConfigurationRegistry.java new file mode 100644 index 000000000..1f57e01e4 --- /dev/null +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ProviderConfigurationRegistry.java @@ -0,0 +1,81 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core.extension; + +import io.streamthoughts.jikkou.core.config.Configuration; +import io.streamthoughts.jikkou.core.exceptions.JikkouRuntimeException; +import java.util.Optional; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Registry for managing multiple provider configurations. + * Allows registering and retrieving configurations for different provider instances. + * + * @since 0.37.0 + */ +public interface ProviderConfigurationRegistry { + + /** + * Registers a provider configuration. + * + * @param providerName the name of the provider instance (e.g., "kafka-prod", "kafka-dev") + * @param providerType the fully qualified class name of the provider type + * @param configuration the configuration for this provider instance + * @param isDefault whether this is the default provider when multiple instances exist + */ + void registerProviderConfiguration(@NotNull String providerName, + @NotNull String providerType, + @NotNull Configuration configuration, + boolean isDefault); + + /** + * Gets the configuration for a specific provider instance. + * + * @param providerName the name of the provider instance + * @return the configuration if found + */ + @NotNull Optional getProviderConfiguration(@NotNull String providerName); + + /** + * Gets the default configuration for a provider type. + * + * @param providerType the fully qualified class name of the provider type + * @return the default configuration if found + */ + @NotNull Optional getDefaultConfiguration(@NotNull String providerType); + + /** + * Gets the configuration for a provider, selecting based on context. + * If {@code providerName} is specified, returns that configuration. + * Otherwise, returns the default configuration for the provider type. + * + * @param providerType the fully qualified class name of the provider type + * @param providerName the selected provider name (optional) + * @return the selected configuration + * + * @throws JikkouRuntimeException if no configuration is found for the given provider type and name. + */ + @NotNull Configuration getConfiguration(@NotNull String providerType, + @Nullable String providerName); + + /** + * Gets all registered provider names. + * + * @return set of all provider names + */ + @NotNull Set getAllProviderNames(); + + /** + * Gets all provider names for a specific provider type. + * + * @param providerType the fully qualified class name of the provider type + * @return set of provider names for this type + */ + @NotNull Set getProviderNamesByType(@NotNull String providerType); +} diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/ProviderSupplier.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ProviderSupplier.java new file mode 100644 index 000000000..b0f928382 --- /dev/null +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/ProviderSupplier.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core.extension; + +import io.streamthoughts.jikkou.core.ProviderSelectionContext; +import io.streamthoughts.jikkou.spi.ExtensionProvider; +import org.jetbrains.annotations.Nullable; + +/** + * Interface for supplying extension instance. + */ +public interface ProviderSupplier { + + /** + * Gets a configured ExtensionProvider instance, optionally using a ProviderSelectionContext + * to select a specific provider configuration. + * + * @param providerContext the provider selection context (optional) + * @return a configured ExtensionProvider instance + */ + ExtensionProvider get(@Nullable ProviderSelectionContext providerContext); +} diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilder.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilder.java index a0d07c3af..4fca7fd43 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilder.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilder.java @@ -13,6 +13,7 @@ import io.streamthoughts.jikkou.core.extension.ExtensionCategory; import io.streamthoughts.jikkou.core.extension.ExtensionDescriptor; import io.streamthoughts.jikkou.core.extension.ExtensionMetadata; +import io.streamthoughts.jikkou.core.extension.ProviderSupplier; import io.streamthoughts.jikkou.spi.ExtensionProvider; import java.util.HashSet; import java.util.List; @@ -34,7 +35,7 @@ public final class ExtensionDescriptorBuilder implements ExtensionDescriptor< private List properties; private ExtensionCategory category; private Class provider; - private Supplier providerSupplier; + private ProviderSupplier providerSupplier; private ExtensionMetadata metadata; private Class type; private boolean isEnabled; @@ -240,12 +241,12 @@ public ExtensionDescriptorBuilder category(ExtensionCategory category) { * {@inheritDoc} */ @Override - public Supplier providerSupplier() { + public ProviderSupplier providerSupplier() { return providerSupplier; } public ExtensionDescriptorBuilder provider(Class provider, - Supplier supplier) { + ProviderSupplier supplier) { this.provider = provider; this.providerSupplier = supplier; return this; diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/extension/qualifier/SupportedResourceQualifier.java b/core/src/main/java/io/streamthoughts/jikkou/core/extension/qualifier/SupportedResourceQualifier.java index 2a1bc722d..ccc103412 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/extension/qualifier/SupportedResourceQualifier.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/extension/qualifier/SupportedResourceQualifier.java @@ -72,10 +72,10 @@ private boolean matches(final ExtensionAttribute attribute) { if (type != HasMetadata.class) { resourceType = ResourceType.of(type); } - else if (!Strings.isBlank(apiVersion)) { + else if (!Strings.isNullOrEmpty(apiVersion)) { resourceType = ResourceType.of(kind, apiVersion); } - else if (!Strings.isBlank(kind)) { + else if (!Strings.isNullOrEmpty(kind)) { resourceType = ResourceType.of(kind); } return equals == (resourceType != null && this.type.canAccept(resourceType)); diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/models/HasMetadataAcceptable.java b/core/src/main/java/io/streamthoughts/jikkou/core/models/HasMetadataAcceptable.java index 917966a0e..b34f9b6d9 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/models/HasMetadataAcceptable.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/models/HasMetadataAcceptable.java @@ -43,11 +43,11 @@ static List getSupportedResources(final Class clazz) { if (accept.type() != HasMetadata.class) { return ResourceType.of(accept.type()); } - if (!Strings.isBlank(accept.apiVersion())) { + if (!Strings.isNullOrEmpty(accept.apiVersion())) { return ResourceType.of(accept.kind(), accept.apiVersion()); } - if (!Strings.isBlank(accept.kind())) { + if (!Strings.isNullOrEmpty(accept.kind())) { return ResourceType.of(accept.kind()); } diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/models/ObjectMeta.java b/core/src/main/java/io/streamthoughts/jikkou/core/models/ObjectMeta.java index 22d291464..98f8f9f60 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/models/ObjectMeta.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/models/ObjectMeta.java @@ -144,7 +144,7 @@ public Optional findLabelByKey(final String key) { * @return {@code true} if the name is present, otherwise {@code false}. */ public boolean hasName() { - return !Strings.isBlank(getName()); + return !Strings.isNullOrEmpty(getName()); } /** diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/models/ResourceType.java b/core/src/main/java/io/streamthoughts/jikkou/core/models/ResourceType.java index 43879e1d7..18ae848bd 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/models/ResourceType.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/models/ResourceType.java @@ -188,7 +188,7 @@ public static ResourceType of(@NotNull final String kind, @Nullable final String apiVersion, final boolean isTransient) { Objects.requireNonNull(kind, "'kind' should not be null"); - if (Strings.isBlank(apiVersion)) { + if (Strings.isNullOrEmpty(apiVersion)) { return new ResourceType(kind, null, null, isTransient); } else { String[] versionParts = new String[]{apiVersion, null}; diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/resource/ResourceDescriptorFactory.java b/core/src/main/java/io/streamthoughts/jikkou/core/resource/ResourceDescriptorFactory.java index 6337de107..1bceb73bd 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/resource/ResourceDescriptorFactory.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/resource/ResourceDescriptorFactory.java @@ -48,8 +48,8 @@ public ResourceDescriptor make(@NotNull final ResourceType type, type, description, resource, - Strings.isBlank(names.singular()) ? null : names.singular(), - Strings.isBlank(names.plural()) ? null : names.plural(), + Strings.isNullOrEmpty(names.singular()) ? null : names.singular(), + Strings.isNullOrEmpty(names.plural()) ? null : names.plural(), new TreeSet<>(Arrays.asList(names.shortNames())), extractVerbs(resource), Resource.isTransient(resource) diff --git a/core/src/main/java/io/streamthoughts/jikkou/core/selector/DefaultSelectorParser.java b/core/src/main/java/io/streamthoughts/jikkou/core/selector/DefaultSelectorParser.java index 29d92c850..dadf6aa6c 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/core/selector/DefaultSelectorParser.java +++ b/core/src/main/java/io/streamthoughts/jikkou/core/selector/DefaultSelectorParser.java @@ -43,7 +43,7 @@ public DefaultSelectorParser(final Function functi @Override public List parseExpression(@NotNull String expression) { - if (Strings.isBlank(expression)) { + if (Strings.isNullOrEmpty(expression)) { throw new InvalidSelectorException("Cannot parse empty or blank expression string"); } diff --git a/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ExtensionConfigEntry.java b/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ExtensionConfigEntry.java index c59aa4a72..be8c2fc7d 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ExtensionConfigEntry.java +++ b/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ExtensionConfigEntry.java @@ -26,6 +26,7 @@ public record ExtensionConfigEntry(String name, String type, Integer priority, Boolean enabled, + Boolean isDefault, Configuration config) { public static final ConfigProperty NAME_CONFIG = ConfigProperty @@ -53,6 +54,11 @@ public record ExtensionConfigEntry(String name, .description("The configuration of the extension") .defaultValue(Configuration.empty()); + public static final ConfigProperty DEFAULT_CONFIG = ConfigProperty + .ofBoolean("default") + .description("The configuration of the extension") + .defaultValue(false); + public static ExtensionConfigEntry of(final @NotNull Configuration config) { return of(config, null); } @@ -64,6 +70,7 @@ public static ExtensionConfigEntry of(final @NotNull Configuration config, Strin TYPE_CONFIG.get(config), PRIORITY_CONFIG.get(config), ENABLED_CONFIG.get(config), + DEFAULT_CONFIG.get(config), CONFIGURATION_CONFIG.get(config) ); } diff --git a/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfigurator.java b/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfigurator.java index 6308047d1..fe26a7362 100644 --- a/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfigurator.java +++ b/core/src/main/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfigurator.java @@ -6,8 +6,6 @@ */ package io.streamthoughts.jikkou.runtime.configurator; -import static io.streamthoughts.jikkou.runtime.JikkouConfigProperties.EXTENSIONS_PROVIDER_DEFAULT_ENABLED; -import static io.streamthoughts.jikkou.runtime.JikkouConfigProperties.EXTENSION_PROVIDER_CONFIG_PREFIX; import static io.streamthoughts.jikkou.runtime.JikkouConfigProperties.PROVIDER_CONFIG; import io.streamthoughts.jikkou.common.utils.ServiceLoaders; @@ -24,11 +22,9 @@ import java.nio.file.Paths; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -64,45 +60,42 @@ protected > B configur .filter(ExtensionConfigEntry::enabled) .toList(); - final Map providerByName = providers.stream() - .collect(Collectors.toMap(ExtensionConfigEntry::name, Function.identity())); + final Map> providerConfigByType = providers.stream() + .filter(entry -> entry.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); // Load all ExtensionProviders for (ExtensionProvider provider : ServiceLoaders.loadAllServices(ExtensionProvider.class, cls)) { final String providerName = provider.getName(); - if (providerByName.containsKey(providerName)) { - Configuration config = providerByName.get(providerName).config(); - // Looking for direct provider config override - Optional override = configuration().findConfig(providerName); - if (override.isPresent()) { - config = override.get().withFallback(config); - } - builder = builder.register(provider, config); - } else { - // backward-compatibility - Boolean extensionEnabledByDefault = EXTENSIONS_PROVIDER_DEFAULT_ENABLED.get(configuration()); - if (legacyIsExtensionProviderEnabled(configuration(), providerName, extensionEnabledByDefault)) { - Optional legacyConfiguration = configuration().findConfig(providerName); - LOG.warn("Deprecated provider configuration `jikkou.{}` was detected. Please, you should consider using the new `jikkou.providers`.", providerName); - builder = builder.register(provider, legacyConfiguration.orElse(Configuration.empty())); - } else { - LOG.debug( - "Provider '{}' was found but will be ignored. This provider is either not configured or disabled through.", - providerName + final String providerType = provider.getClass().getName(); + + if (providerConfigByType.containsKey(providerType)) { + // Register provider + builder.register(provider); + + // Register provider configurations + providerConfigByType.get(providerType).forEach(extensionConfigEntry -> { + + // Looking for direct provider config override + Configuration config = extensionConfigEntry.config(); + Optional override = configuration().findConfig(extensionConfigEntry.name()); + if (override.isPresent()) { + config = override.get().withFallback(config); + } + + // Register configuration + builder.registerProviderConfiguration( + extensionConfigEntry.name(), + extensionConfigEntry.type(), + config, + extensionConfigEntry.isDefault() ); - } + }); } } return builder; } - private static boolean legacyIsExtensionProviderEnabled(@NotNull Configuration configuration, - @NotNull String name, - boolean defaultValue) { - String property = String.format(EXTENSION_PROVIDER_CONFIG_PREFIX + ".%s.enabled", name.toLowerCase(Locale.ROOT)); - return configuration.findBoolean(property).orElse(defaultValue); - } - /** * @return the set of known {@link ClassLoader}. */ diff --git a/core/src/test/java/io/streamthoughts/jikkou/common/utils/StringsTest.java b/core/src/test/java/io/streamthoughts/jikkou/common/utils/StringsTest.java index 5e8cf6bad..9248739c6 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/common/utils/StringsTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/common/utils/StringsTest.java @@ -14,22 +14,22 @@ class StringsTest { @Test void shouldReturnTrueForNullString() { - Assertions.assertTrue(Strings.isBlank(null)); + Assertions.assertTrue(Strings.isNullOrEmpty(null)); } @Test void shouldReturnTrueForEmptyString() { - Assertions.assertTrue(Strings.isBlank("")); + Assertions.assertTrue(Strings.isNullOrEmpty("")); } @Test void shouldReturnTrueForBlankString() { - Assertions.assertTrue(Strings.isBlank(" ")); + Assertions.assertTrue(Strings.isNullOrEmpty(" ")); } @Test void shouldReturnFalseForNonEmptyString() { - Assertions.assertFalse(Strings.isBlank("dummy")); + Assertions.assertFalse(Strings.isNullOrEmpty("dummy")); } @Test diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/GetContextTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/GetContextTest.java new file mode 100644 index 000000000..8d9f0c3dc --- /dev/null +++ b/core/src/test/java/io/streamthoughts/jikkou/core/GetContextTest.java @@ -0,0 +1,150 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core; + +import io.streamthoughts.jikkou.core.config.Configuration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class GetContextTest { + + @Test + void shouldBuildGetContextWithAllFields() { + // Given + Configuration configuration = Configuration.of("key", "value"); + String providerName = "test-provider"; + + // When + GetContext context = GetContext.builder() + .configuration(configuration) + .providerName(providerName) + .build(); + + // Then + Assertions.assertEquals(configuration, context.configuration()); + Assertions.assertEquals(providerName, context.providerName()); + } + + @Test + void shouldBuildGetContextWithDefaultValues() { + // When + GetContext context = GetContext.builder().build(); + + // Then + Assertions.assertEquals(Configuration.empty(), context.configuration()); + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldBuildGetContextWithOnlyConfiguration() { + // Given + Configuration configuration = Configuration.of("key", "value"); + + // When + GetContext context = GetContext.builder() + .configuration(configuration) + .build(); + + // Then + Assertions.assertEquals(configuration, context.configuration()); + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldBuildGetContextWithOnlyProviderName() { + // Given + String providerName = "test-provider"; + + // When + GetContext context = GetContext.builder() + .providerName(providerName) + .build(); + + // Then + Assertions.assertEquals(Configuration.empty(), context.configuration()); + Assertions.assertEquals(providerName, context.providerName()); + } + + @Test + void shouldBuildGetContextUsingChainedBuilderCalls() { + // Given + Configuration config1 = Configuration.of("key1", "value1"); + Configuration config2 = Configuration.of("key2", "value2"); + + // When - each builder call returns a new builder + GetContext.Builder builder1 = GetContext.builder(); + GetContext.Builder builder2 = builder1.configuration(config1); + GetContext.Builder builder3 = builder2.configuration(config2); + GetContext.Builder builder4 = builder3.providerName("provider1"); + GetContext.Builder builder5 = builder4.providerName("provider2"); + + GetContext context = builder5.build(); + + // Then - last values should win + Assertions.assertEquals(config2, context.configuration()); + Assertions.assertEquals("provider2", context.providerName()); + } + + @Test + void shouldHaveEmptyDefaultInstance() { + // When + GetContext emptyContext = GetContext.Default.EMPTY; + + // Then + Assertions.assertEquals(Configuration.empty(), emptyContext.configuration()); + Assertions.assertNull(emptyContext.providerName()); + } + + @Test + void shouldCreateGetContextRecord() { + // Given + Configuration configuration = Configuration.of("key", "value"); + String providerName = "test-provider"; + + // When + GetContext.Default context = new GetContext.Default( + configuration, + providerName + ); + + // Then + Assertions.assertEquals(configuration, context.configuration()); + Assertions.assertEquals(providerName, context.providerName()); + } + + @Test + void shouldSupportNullProviderName() { + // When + GetContext context = GetContext.builder() + .providerName(null) + .build(); + + // Then + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldSupportComplexConfiguration() { + // Given + Configuration configuration = Configuration.from(java.util.Map.of( + "bootstrap.servers", "localhost:9092", + "security.protocol", "SASL_SSL", + "nested", java.util.Map.of("key1", "value1", "key2", "value2") + )); + + // When + GetContext context = GetContext.builder() + .configuration(configuration) + .providerName("kafka-prod") + .build(); + + // Then + Assertions.assertEquals("localhost:9092", context.configuration().getString("bootstrap.servers")); + Assertions.assertEquals("SASL_SSL", context.configuration().getString("security.protocol")); + Assertions.assertEquals("kafka-prod", context.providerName()); + } +} diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/ListContextTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/ListContextTest.java new file mode 100644 index 000000000..96c071f51 --- /dev/null +++ b/core/src/test/java/io/streamthoughts/jikkou/core/ListContextTest.java @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core; + +import io.streamthoughts.jikkou.core.config.Configuration; +import io.streamthoughts.jikkou.core.selector.Selectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ListContextTest { + + @Test + void shouldBuildListContextWithAllFields() { + // Given + Configuration configuration = Configuration.of("key", "value"); + String providerName = "test-provider"; + + // When + ListContext context = ListContext.builder() + .selector(Selectors.NO_SELECTOR) + .configuration(configuration) + .providerName(providerName) + .build(); + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, context.selector()); + Assertions.assertEquals(configuration, context.configuration()); + Assertions.assertEquals(providerName, context.providerName()); + } + + @Test + void shouldBuildListContextWithDefaultValues() { + // When + ListContext context = ListContext.builder().build(); + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, context.selector()); + Assertions.assertEquals(Configuration.empty(), context.configuration()); + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldBuildListContextWithOnlySelector() { + // When + ListContext context = ListContext.builder() + .selector(Selectors.NO_SELECTOR) + .build(); + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, context.selector()); + Assertions.assertEquals(Configuration.empty(), context.configuration()); + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldBuildListContextWithOnlyConfiguration() { + // Given + Configuration configuration = Configuration.of("key", "value"); + + // When + ListContext context = ListContext.builder() + .configuration(configuration) + .build(); + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, context.selector()); + Assertions.assertEquals(configuration, context.configuration()); + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldBuildListContextWithOnlyProviderName() { + // Given + String providerName = "test-provider"; + + // When + ListContext context = ListContext.builder() + .providerName(providerName) + .build(); + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, context.selector()); + Assertions.assertEquals(Configuration.empty(), context.configuration()); + Assertions.assertEquals(providerName, context.providerName()); + } + + @Test + void shouldBuildListContextUsingChainedBuilderCalls() { + // Given + Configuration config1 = Configuration.of("key1", "value1"); + Configuration config2 = Configuration.of("key2", "value2"); + + // When - each builder call returns a new builder + ListContext.Builder builder1 = ListContext.builder(); + ListContext.Builder builder2 = builder1.configuration(config1); + ListContext.Builder builder3 = builder2.configuration(config2); + ListContext.Builder builder4 = builder3.providerName("provider1"); + ListContext.Builder builder5 = builder4.providerName("provider2"); + + ListContext context = builder5.build(); + + // Then - last values should win + Assertions.assertEquals(config2, context.configuration()); + Assertions.assertEquals("provider2", context.providerName()); + } + + @Test + void shouldHaveEmptyDefaultInstance() { + // When + ListContext emptyContext = ListContext.Default.EMPTY; + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, emptyContext.selector()); + Assertions.assertEquals(Configuration.empty(), emptyContext.configuration()); + Assertions.assertNull(emptyContext.providerName()); + } + + @Test + void shouldCreateListContextRecord() { + // Given + Configuration configuration = Configuration.of("key", "value"); + String providerName = "test-provider"; + + // When + ListContext.Default context = new ListContext.Default( + Selectors.NO_SELECTOR, + configuration, + providerName + ); + + // Then + Assertions.assertEquals(Selectors.NO_SELECTOR, context.selector()); + Assertions.assertEquals(configuration, context.configuration()); + Assertions.assertEquals(providerName, context.providerName()); + } + + @Test + void shouldSupportNullProviderName() { + // When + ListContext context = ListContext.builder() + .providerName(null) + .build(); + + // Then + Assertions.assertNull(context.providerName()); + } +} diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/ReconciliationContextTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/ReconciliationContextTest.java index 6f383f494..9590c3f8c 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/ReconciliationContextTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/ReconciliationContextTest.java @@ -31,7 +31,8 @@ void shouldBuildReconciliationContext() { Configuration.empty(), true, NamedValueSet.setOf(new NamedValue("label", "value")), - NamedValueSet.setOf(new NamedValue("annotation", "value")) + NamedValueSet.setOf(new NamedValue("annotation", "value")), + null ), context); } diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/ClassExtensionAliasesGeneratorTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/ClassExtensionAliasesGeneratorTest.java index 46eeab395..0dd3e31db 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/ClassExtensionAliasesGeneratorTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/ClassExtensionAliasesGeneratorTest.java @@ -64,7 +64,7 @@ private static ExtensionDescriptor getDescriptor(Class clazz) { ExtensionCategory.EXTENSION, Collections.emptyList(), null, - () -> null, + (unused) -> null, clazz, clazz.getClassLoader(), () -> null, diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplierTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplierTest.java index 1fc8eb289..d54f71466 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplierTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultExtensionSupplierTest.java @@ -29,7 +29,7 @@ void shouldThrowExceptionForSupplierReturningNull() { ExtensionCategory.EXTENSION, NO_PROPERTIES, null, - () -> null, + (unused) -> null, ExtensionDescriptorModifiersTest.class, ExtensionDescriptorModifiersTest.class.getClassLoader(), () -> null, @@ -41,7 +41,7 @@ void shouldThrowExceptionForSupplierReturningNull() { Assertions.assertThrows( ExtensionCreationException.class, - () -> supplier.get(Mockito.mock(ExtensionFactory.class)) + () -> supplier.get(Mockito.mock(ExtensionFactory.class), null) ); } @@ -56,7 +56,7 @@ void shouldInvokeInitMethodForExtension() { ExtensionCategory.EXTENSION, NO_PROPERTIES, null, - () -> null, + (unused) -> null, Extension.class, Extension.class.getClassLoader(), () -> mock, @@ -65,7 +65,7 @@ void shouldInvokeInitMethodForExtension() { null ); var supplier = new DefaultExtensionSupplier<>(descriptor); - Extension extension = supplier.get(Mockito.mock(ExtensionFactory.class)); + Extension extension = supplier.get(Mockito.mock(ExtensionFactory.class), null); Assertions.assertEquals(mock, extension); Mockito.verify(mock, Mockito.atLeastOnce()).init(Mockito.any(ExtensionContext.class)); } diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultProviderConfigurationRegistryTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultProviderConfigurationRegistryTest.java new file mode 100644 index 000000000..9285838da --- /dev/null +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/DefaultProviderConfigurationRegistryTest.java @@ -0,0 +1,295 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.core.extension; + +import io.streamthoughts.jikkou.core.config.Configuration; +import io.streamthoughts.jikkou.core.exceptions.JikkouRuntimeException; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultProviderConfigurationRegistryTest { + + private static final String PROVIDER_TYPE_KAFKA = "io.streamthoughts.jikkou.kafka.KafkaExtensionProvider"; + private static final String PROVIDER_TYPE_SCHEMA_REGISTRY = "io.streamthoughts.jikkou.schema.registry.SchemaRegistryExtensionProvider"; + + private static final String PROVIDER_NAME_KAFKA_PROD = "kafka-prod"; + private static final String PROVIDER_NAME_KAFKA_DEV = "kafka-dev"; + private static final String PROVIDER_NAME_SCHEMA_REGISTRY = "schema-registry"; + + private DefaultProviderConfigurationRegistry registry; + + @BeforeEach + void beforeEach() { + registry = new DefaultProviderConfigurationRegistry(); + } + + @Test + void shouldRegisterProviderConfiguration() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + + // When + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, false); + + // Then + Assertions.assertTrue(registry.getProviderConfiguration(PROVIDER_NAME_KAFKA_PROD).isPresent()); + Assertions.assertEquals(config, registry.getProviderConfiguration(PROVIDER_NAME_KAFKA_PROD).get()); + } + + @Test + void shouldThrowExceptionWhenRegisteringDuplicateProviderName() { + // Given + Configuration config1 = Configuration.of("bootstrap.servers", "localhost:9092"); + Configuration config2 = Configuration.of("bootstrap.servers", "localhost:9093"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config1, false); + + // When - Then + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config2, false)); + Assertions.assertEquals( + "Configuration is already registered for provider name: " + PROVIDER_NAME_KAFKA_PROD, + exception.getMessage()); + } + + @Test + void shouldSetDefaultProviderWhenIsDefaultTrue() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + + // When + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, true); + + // Then + Optional defaultConfig = registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA); + Assertions.assertTrue(defaultConfig.isPresent()); + Assertions.assertEquals(config, defaultConfig.get()); + } + + @Test + void shouldReplaceDefaultProviderWhenNewDefaultRegistered() { + // Given + Configuration config1 = Configuration.of("bootstrap.servers", "prod:9092"); + Configuration config2 = Configuration.of("bootstrap.servers", "dev:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config1, true); + + // When + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_DEV, PROVIDER_TYPE_KAFKA, config2, true); + + // Then + Optional defaultConfig = registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA); + Assertions.assertTrue(defaultConfig.isPresent()); + Assertions.assertEquals(config2, defaultConfig.get()); + } + + @Test + void shouldReturnEmptyOptionalWhenProviderConfigurationNotFound() { + // When + Optional result = registry.getProviderConfiguration("non-existent"); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnEmptyOptionalWhenNoDefaultConfiguredAndMultipleProviders() { + // Given + Configuration config1 = Configuration.of("bootstrap.servers", "prod:9092"); + Configuration config2 = Configuration.of("bootstrap.servers", "dev:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config1, false); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_DEV, PROVIDER_TYPE_KAFKA, config2, false); + + // When + Optional result = registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnSingleProviderAsDefaultWhenNoExplicitDefault() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, false); + + // When + Optional result = registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA); + + // Then + Assertions.assertTrue(result.isPresent()); + Assertions.assertEquals(config, result.get()); + } + + @Test + void shouldReturnEmptyOptionalWhenNoProvidersForType() { + // When + Optional result = registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldGetConfigurationByProviderName() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, false); + + // When + Configuration result = registry.getConfiguration(PROVIDER_TYPE_KAFKA, PROVIDER_NAME_KAFKA_PROD); + + // Then + Assertions.assertEquals(config, result); + } + + @Test + void shouldGetDefaultConfigurationWhenProviderNameIsNull() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, true); + + // When + Configuration result = registry.getConfiguration(PROVIDER_TYPE_KAFKA, null); + + // Then + Assertions.assertEquals(config, result); + } + + @Test + void shouldGetDefaultConfigurationWhenProviderNameIsEmpty() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, true); + + // When + Configuration result = registry.getConfiguration(PROVIDER_TYPE_KAFKA, ""); + + // Then + Assertions.assertEquals(config, result); + } + + @Test + void shouldThrowExceptionWhenNoDefaultProviderAndProviderNameIsNull() { + // Given + Configuration config1 = Configuration.of("bootstrap.servers", "prod:9092"); + Configuration config2 = Configuration.of("bootstrap.servers", "dev:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config1, false); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_DEV, PROVIDER_TYPE_KAFKA, config2, false); + + // When - Then + JikkouRuntimeException exception = Assertions.assertThrows( + JikkouRuntimeException.class, () -> registry.getConfiguration(PROVIDER_TYPE_KAFKA, null)); + Assertions.assertEquals( + "No default configuration defined, and multiple configurations found for provider type: '" + PROVIDER_TYPE_KAFKA + "'", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenProviderNameNotFound() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, false); + + // When - Then + JikkouRuntimeException exception = Assertions.assertThrows( + JikkouRuntimeException.class, () -> registry.getConfiguration(PROVIDER_TYPE_KAFKA, "non-existent")); + Assertions.assertEquals("No provider configured for name: 'non-existent'", exception.getMessage()); + } + + @Test + void shouldReturnEmptySetWhenNoProvidersRegistered() { + // When + Set result = registry.getAllProviderNames(); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnAllProviderNames() { + // Given + Configuration config1 = Configuration.of("bootstrap.servers", "prod:9092"); + Configuration config2 = Configuration.of("bootstrap.servers", "dev:9092"); + Configuration config3 = Configuration.of("schema.registry.url", "http://localhost:8081"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config1, false); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_DEV, PROVIDER_TYPE_KAFKA, config2, false); + registry.registerProviderConfiguration(PROVIDER_NAME_SCHEMA_REGISTRY, PROVIDER_TYPE_SCHEMA_REGISTRY, config3, false); + + // When + Set result = registry.getAllProviderNames(); + + // Then + Assertions.assertEquals(3, result.size()); + Assertions.assertTrue(result.contains(PROVIDER_NAME_KAFKA_PROD)); + Assertions.assertTrue(result.contains(PROVIDER_NAME_KAFKA_DEV)); + Assertions.assertTrue(result.contains(PROVIDER_NAME_SCHEMA_REGISTRY)); + } + + @Test + void shouldReturnEmptySetWhenNoProvidersForType() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, false); + + // When + Set result = registry.getProviderNamesByType(PROVIDER_TYPE_SCHEMA_REGISTRY); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldReturnProviderNamesForSpecificType() { + // Given + Configuration config1 = Configuration.of("bootstrap.servers", "prod:9092"); + Configuration config2 = Configuration.of("bootstrap.servers", "dev:9092"); + Configuration config3 = Configuration.of("schema.registry.url", "http://localhost:8081"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config1, false); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_DEV, PROVIDER_TYPE_KAFKA, config2, false); + registry.registerProviderConfiguration(PROVIDER_NAME_SCHEMA_REGISTRY, PROVIDER_TYPE_SCHEMA_REGISTRY, config3, false); + + // When + Set kafkaProviders = registry.getProviderNamesByType(PROVIDER_TYPE_KAFKA); + + // Then + Assertions.assertEquals(2, kafkaProviders.size()); + Assertions.assertTrue(kafkaProviders.contains(PROVIDER_NAME_KAFKA_PROD)); + Assertions.assertTrue(kafkaProviders.contains(PROVIDER_NAME_KAFKA_DEV)); + Assertions.assertFalse(kafkaProviders.contains(PROVIDER_NAME_SCHEMA_REGISTRY)); + } + + @Test + void shouldNotSetDefaultWhenIsDefaultFalse() { + // Given + Configuration config = Configuration.of("bootstrap.servers", "localhost:9092"); + + // When + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, config, false); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_DEV, PROVIDER_TYPE_KAFKA, config, false); + + // Then - getDefaultConfiguration should return empty since no explicit default and multiple providers + Optional defaultConfig = registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA); + Assertions.assertTrue(defaultConfig.isEmpty()); + } + + @Test + void shouldHandleMultipleProviderTypes() { + // Given + Configuration kafkaConfig = Configuration.of("bootstrap.servers", "localhost:9092"); + Configuration schemaConfig = Configuration.of("schema.registry.url", "http://localhost:8081"); + registry.registerProviderConfiguration(PROVIDER_NAME_KAFKA_PROD, PROVIDER_TYPE_KAFKA, kafkaConfig, true); + registry.registerProviderConfiguration( + PROVIDER_NAME_SCHEMA_REGISTRY, PROVIDER_TYPE_SCHEMA_REGISTRY, schemaConfig, true); + + // When - Then + Assertions.assertEquals(kafkaConfig, registry.getDefaultConfiguration(PROVIDER_TYPE_KAFKA).orElse(null)); + Assertions.assertEquals( + schemaConfig, registry.getDefaultConfiguration(PROVIDER_TYPE_SCHEMA_REGISTRY).orElse(null)); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiersTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiersTest.java index 72b2e2cdc..578ff3e43 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiersTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/ExtensionDescriptorModifiersTest.java @@ -25,7 +25,7 @@ class ExtensionDescriptorModifiersTest { ExtensionCategory.EXTENSION, NO_PROPERTIES, null, - () -> null, + (unused) -> null, ExtensionDescriptorModifiersTest.class, ExtensionDescriptorModifiersTest.class.getClassLoader(), () -> null, diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilderTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilderTest.java index d21ebf119..adbb682f6 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilderTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/builder/ExtensionDescriptorBuilderTest.java @@ -32,7 +32,7 @@ void shouldCreateBuilderFromDescriptor() { ExtensionCategory.EXTENSION, NO_PROPERTIES, null, - () -> null, + (unused) -> null, ExtensionDescriptorBuilderTest.class, ExtensionDescriptorBuilderTest.class.getClassLoader(), () -> null, diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/AnyQualifierTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/AnyQualifierTest.java index bc3cf35d0..612cbc4a6 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/AnyQualifierTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/AnyQualifierTest.java @@ -40,7 +40,7 @@ private static ExtensionDescriptor getDescriptor(Class clazz, boolean ExtensionCategory.EXTENSION, Collections.emptyList(), null, - () -> null, + (unused) -> null, clazz, clazz.getClassLoader(), () -> null, diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/CompositeQualifierTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/CompositeQualifierTest.java index 1a0b31735..71593d15a 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/CompositeQualifierTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/CompositeQualifierTest.java @@ -40,7 +40,7 @@ private static ExtensionDescriptor getDescriptor(Class clazz, boolean ExtensionCategory.EXTENSION, Collections.emptyList(), null, - () -> null, + (unused) -> null, clazz, clazz.getClassLoader(), () -> null, diff --git a/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/EnabledQualifierTest.java b/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/EnabledQualifierTest.java index 09da18a98..83d8ae676 100644 --- a/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/EnabledQualifierTest.java +++ b/core/src/test/java/io/streamthoughts/jikkou/core/extension/qualifier/EnabledQualifierTest.java @@ -46,7 +46,7 @@ private static ExtensionDescriptor getDescriptor(Class clazz, boolean ExtensionCategory.EXTENSION, Collections.emptyList(), null, - () -> null, + (unused) -> null, clazz, clazz.getClassLoader(), () -> null, diff --git a/core/src/test/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfiguratorTest.java b/core/src/test/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfiguratorTest.java new file mode 100644 index 000000000..23d5e25b7 --- /dev/null +++ b/core/src/test/java/io/streamthoughts/jikkou/runtime/configurator/ProviderApiConfiguratorTest.java @@ -0,0 +1,281 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) The original authors + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.streamthoughts.jikkou.runtime.configurator; + +import static io.streamthoughts.jikkou.runtime.configurator.ExtensionConfigEntry.CONFIGURATION_CONFIG; +import static io.streamthoughts.jikkou.runtime.configurator.ExtensionConfigEntry.DEFAULT_CONFIG; +import static io.streamthoughts.jikkou.runtime.configurator.ExtensionConfigEntry.ENABLED_CONFIG; +import static io.streamthoughts.jikkou.runtime.configurator.ExtensionConfigEntry.NAME_CONFIG; +import static io.streamthoughts.jikkou.runtime.configurator.ExtensionConfigEntry.TYPE_CONFIG; + +import io.streamthoughts.jikkou.core.config.Configuration; +import io.streamthoughts.jikkou.core.models.NamedValueSet; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ProviderApiConfiguratorTest { + + @Test + void shouldFilterOutEntriesWithNullType() { + // Given - entries with null type should not cause NPE when grouping by type + Map entryWithNullType = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("test-provider")) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + // Note: TYPE_CONFIG is not set, so type() will return null + + List entries = List.of( + ExtensionConfigEntry.of(Configuration.from(entryWithNullType)) + ); + + // When - grouping by type should not throw NPE + Map> result = entries.stream() + .filter(entry -> entry.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldGroupEntriesByType() { + // Given + String providerType = "io.streamthoughts.jikkou.TestProvider"; + + Map entry1 = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-1")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(DEFAULT_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Map.of("key1", "value1"))) + .asMap(); + + Map entry2 = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-2")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(DEFAULT_CONFIG.asValue(false)) + .with(CONFIGURATION_CONFIG.asValue(Map.of("key2", "value2"))) + .asMap(); + + List entries = List.of( + ExtensionConfigEntry.of(Configuration.from(entry1)), + ExtensionConfigEntry.of(Configuration.from(entry2)) + ); + + // When + Map> result = entries.stream() + .filter(entry -> entry.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); + + // Then + Assertions.assertEquals(1, result.size()); + Assertions.assertTrue(result.containsKey(providerType)); + Assertions.assertEquals(2, result.get(providerType).size()); + } + + @Test + void shouldFilterOutDisabledProviders() { + // Given + String providerType = "io.streamthoughts.jikkou.TestProvider"; + + Map enabledEntry = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("enabled-provider")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + Map disabledEntry = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("disabled-provider")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(false)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + List entries = List.of( + ExtensionConfigEntry.of(Configuration.from(enabledEntry)), + ExtensionConfigEntry.of(Configuration.from(disabledEntry)) + ); + + // When + List enabledProviders = entries.stream() + .filter(ExtensionConfigEntry::enabled) + .toList(); + + // Then + Assertions.assertEquals(1, enabledProviders.size()); + Assertions.assertEquals("enabled-provider", enabledProviders.get(0).name()); + } + + @Test + void shouldHandleMixedEntriesWithAndWithoutType() { + // Given + String providerType = "io.streamthoughts.jikkou.TestProvider"; + + Map entryWithType = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-with-type")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + Map entryWithoutType = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-without-type")) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + List entries = List.of( + ExtensionConfigEntry.of(Configuration.from(entryWithType)), + ExtensionConfigEntry.of(Configuration.from(entryWithoutType)) + ); + + // When - should not throw NPE + Map> result = entries.stream() + .filter(ExtensionConfigEntry::enabled) + .filter(entry -> entry.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); + + // Then + Assertions.assertEquals(1, result.size()); + Assertions.assertTrue(result.containsKey(providerType)); + Assertions.assertEquals(1, result.get(providerType).size()); + Assertions.assertEquals("provider-with-type", result.get(providerType).get(0).name()); + } + + @Test + void shouldHandleEmptyProviderConfiguration() { + // Given + List entries = Collections.emptyList(); + + // When + Map> result = entries.stream() + .filter(ExtensionConfigEntry::enabled) + .filter(entry -> entry.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); + + // Then + Assertions.assertTrue(result.isEmpty()); + } + + @Test + void shouldGroupMultipleProviderTypes() { + // Given + String providerType1 = "io.streamthoughts.jikkou.TestProvider1"; + String providerType2 = "io.streamthoughts.jikkou.TestProvider2"; + + Map entry1 = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-type1-instance1")) + .with(TYPE_CONFIG.asValue(providerType1)) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + Map entry2 = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-type1-instance2")) + .with(TYPE_CONFIG.asValue(providerType1)) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + Map entry3 = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("provider-type2-instance1")) + .with(TYPE_CONFIG.asValue(providerType2)) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + List entries = List.of( + ExtensionConfigEntry.of(Configuration.from(entry1)), + ExtensionConfigEntry.of(Configuration.from(entry2)), + ExtensionConfigEntry.of(Configuration.from(entry3)) + ); + + // When + Map> result = entries.stream() + .filter(ExtensionConfigEntry::enabled) + .filter(entry -> entry.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); + + // Then + Assertions.assertEquals(2, result.size()); + Assertions.assertTrue(result.containsKey(providerType1)); + Assertions.assertTrue(result.containsKey(providerType2)); + Assertions.assertEquals(2, result.get(providerType1).size()); + Assertions.assertEquals(1, result.get(providerType2).size()); + } + + @Test + void shouldPreserveDefaultFlag() { + // Given + String providerType = "io.streamthoughts.jikkou.TestProvider"; + + Map defaultEntry = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("default-provider")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(DEFAULT_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + Map nonDefaultEntry = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue("non-default-provider")) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(DEFAULT_CONFIG.asValue(false)) + .with(CONFIGURATION_CONFIG.asValue(Collections.emptyMap())) + .asMap(); + + // When + ExtensionConfigEntry defaultConfig = ExtensionConfigEntry.of(Configuration.from(defaultEntry)); + ExtensionConfigEntry nonDefaultConfig = ExtensionConfigEntry.of(Configuration.from(nonDefaultEntry)); + + // Then + Assertions.assertTrue(defaultConfig.isDefault()); + Assertions.assertFalse(nonDefaultConfig.isDefault()); + } + + @Test + void shouldLookupProviderConfigByType() { + // Given - simulating the fix where we use providerType instead of providerName + String providerType = "io.streamthoughts.jikkou.TestProvider"; + String providerName = "test-provider"; // different from providerType + + Map entry = NamedValueSet.emptySet() + .with(NAME_CONFIG.asValue(providerName)) + .with(TYPE_CONFIG.asValue(providerType)) + .with(ENABLED_CONFIG.asValue(true)) + .with(CONFIGURATION_CONFIG.asValue(Map.of("key", "value"))) + .asMap(); + + List entries = List.of( + ExtensionConfigEntry.of(Configuration.from(entry)) + ); + + Map> providerConfigByType = entries.stream() + .filter(e -> e.type() != null) + .collect(Collectors.groupingBy(ExtensionConfigEntry::type)); + + // When - lookup by type (correct behavior after fix) + List configsByType = providerConfigByType.get(providerType); + + // When - lookup by name (incorrect behavior before fix) - would return null + List configsByName = providerConfigByType.get(providerName); + + // Then + Assertions.assertNotNull(configsByType); + Assertions.assertEquals(1, configsByType.size()); + Assertions.assertEquals(providerName, configsByType.get(0).name()); + Assertions.assertNull(configsByName); // This was the bug - using name instead of type + } +} diff --git a/jikkou_completion b/jikkou_completion index 33fd30dfb..073f82df8 100644 --- a/jikkou_completion +++ b/jikkou_completion @@ -269,7 +269,7 @@ function _picocli_jikkou_create() { local commands="" local flag_opts="--pretty --dry-run -h --help -V --version" - local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options" + local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --provider" local format_option_args=("TEXT" "COMPACT" "JSON" "YAML") # --output values local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values @@ -316,6 +316,9 @@ function _picocli_jikkou_create() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -335,7 +338,7 @@ function _picocli_jikkou_delete() { local commands="" local flag_opts="--pretty --dry-run -h --help -V --version" - local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options" + local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --provider" local format_option_args=("TEXT" "COMPACT" "JSON" "YAML") # --output values local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values @@ -382,6 +385,9 @@ function _picocli_jikkou_delete() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -401,7 +407,7 @@ function _picocli_jikkou_update() { local commands="" local flag_opts="--pretty --dry-run -h --help -V --version" - local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options" + local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --provider" local format_option_args=("TEXT" "COMPACT" "JSON" "YAML") # --output values local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values @@ -448,6 +454,9 @@ function _picocli_jikkou_update() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -467,7 +476,7 @@ function _picocli_jikkou_apply() { local commands="" local flag_opts="--pretty --dry-run -h --help -V --version" - local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options" + local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --provider" local format_option_args=("TEXT" "COMPACT" "JSON" "YAML") # --output values local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values @@ -514,6 +523,9 @@ function _picocli_jikkou_apply() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -533,7 +545,7 @@ function _picocli_jikkou_patch() { local commands="" local flag_opts="--pretty --dry-run -h --help -V --version" - local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --mode" + local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --provider --mode" local format_option_args=("TEXT" "COMPACT" "JSON" "YAML") # --output values local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values local mode_option_args=("CREATE" "DELETE" "UPDATE" "FULL") # --mode values @@ -581,6 +593,9 @@ function _picocli_jikkou_patch() { --options) return ;; + --provider) + return + ;; --mode) local IFS=$'\n' COMPREPLY=( $( compReplyArray "${mode_option_args[@]}" ) ) @@ -605,7 +620,7 @@ function _picocli_jikkou_replace() { local commands="" local flag_opts="--pretty --dry-run -h --help -V --version" - local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options" + local arg_opts="--logger-level --output -o --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --options --provider" local format_option_args=("TEXT" "COMPACT" "JSON" "YAML") # --output values local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values @@ -652,6 +667,9 @@ function _picocli_jikkou_replace() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -758,7 +776,7 @@ function _picocli_jikkou_diff() { local commands="" local flag_opts="--list -h --help -V --version" - local arg_opts="--logger-level --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --output -o --options --filter-resource-op --filter-change-op" + local arg_opts="--logger-level --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --output -o --options --provider --filter-resource-op --filter-change-op" local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values local format_option_args=("JSON" "YAML") # --output values local filterOutAllResourcesExcept_option_args=("NONE" "CREATE" "DELETE" "REPLACE" "UPDATE") # --filter-resource-op values @@ -807,6 +825,9 @@ function _picocli_jikkou_diff() { --options) return ;; + --provider) + return + ;; --filter-resource-op) local IFS=$'\n' COMPREPLY=( $( compReplyArray "${filterOutAllResourcesExcept_option_args[@]}" ) ) @@ -836,7 +857,7 @@ function _picocli_jikkou_prepare() { local commands="" local flag_opts="-h --help -V --version" - local arg_opts="--logger-level --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --output -o --options" + local arg_opts="--logger-level --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --output -o --options --provider" local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values local format_option_args=("JSON" "YAML") # --output values @@ -883,6 +904,9 @@ function _picocli_jikkou_prepare() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -902,7 +926,7 @@ function _picocli_jikkou_validate() { local commands="" local flag_opts="-h --help -V --version" - local arg_opts="--logger-level --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --output -o --options" + local arg_opts="--logger-level --files -f --file-name -n --values-files --values-file-name --set-label -l --set-annotation --set-value -v --selector -s --selector-match --output -o --options --provider" local selectorMatchingStrategy_option_args=("NONE" "ALL" "ANY") # --selector-match values local format_option_args=("JSON" "YAML") # --output values @@ -949,6 +973,9 @@ function _picocli_jikkou_validate() { --options) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then @@ -1259,7 +1286,7 @@ function _picocli_jikkou_health_get() { local commands="" local flag_opts="-h --help -V --version" - local arg_opts="--logger-level --output -o --timeout-ms" + local arg_opts="--logger-level --output -o --timeout-ms --provider" local format_option_args=("JSON" "YAML") # --output values type compopt &>/dev/null && compopt +o default @@ -1276,6 +1303,9 @@ function _picocli_jikkou_health_get() { --timeout-ms) return ;; + --provider) + return + ;; esac if [[ "${curr_word}" == -* ]]; then diff --git a/providers/jikkou-provider-core/src/test/java/io/streamthoughts/jikkou/core/repository/LocalResourceRepositoryTest.java b/providers/jikkou-provider-core/src/test/java/io/streamthoughts/jikkou/core/repository/LocalResourceRepositoryTest.java index 197d82dce..5baebca15 100644 --- a/providers/jikkou-provider-core/src/test/java/io/streamthoughts/jikkou/core/repository/LocalResourceRepositoryTest.java +++ b/providers/jikkou-provider-core/src/test/java/io/streamthoughts/jikkou/core/repository/LocalResourceRepositoryTest.java @@ -30,7 +30,7 @@ class LocalResourceRepositoryTest { void shouldGetEmptyListGivenNoFile() { // Given LocalResourceRepository repository = new LocalResourceRepository(); - repository.init(new DefaultExtensionContext(null, DESCRIPTOR)); + repository.init(new DefaultExtensionContext(null, DESCRIPTOR, null)); // When List results = repository.all(); @@ -50,7 +50,7 @@ void shouldLoadResourcesFromLocalFiles() { ).apply(DESCRIPTOR); LocalResourceRepository repository = new LocalResourceRepository(); - repository.init(new DefaultExtensionContext(null, descriptor)); + repository.init(new DefaultExtensionContext(null, descriptor, null)); // When List results = repository.all(); diff --git a/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/service/KafkaConnectClusterService.java b/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/service/KafkaConnectClusterService.java index 506224d83..1fc0f05f1 100644 --- a/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/service/KafkaConnectClusterService.java +++ b/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/service/KafkaConnectClusterService.java @@ -48,7 +48,7 @@ public KafkaConnectClusterService(@NotNull final String clusterName, public CompletableFuture getConnectorAsync(@NotNull final String connectorName, boolean expandStatus) { - if (Strings.isBlank(connectorName)) { + if (Strings.isNullOrEmpty(connectorName)) { throw new IllegalArgumentException("connectorName is null or empty."); } return CompletableFuture diff --git a/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/validation/KafkaConnectorResourceValidation.java b/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/validation/KafkaConnectorResourceValidation.java index a31117be4..591877abf 100644 --- a/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/validation/KafkaConnectorResourceValidation.java +++ b/providers/jikkou-provider-kafka-connect/src/main/java/io/streamthoughts/jikkou/kafka/connect/validation/KafkaConnectorResourceValidation.java @@ -49,10 +49,10 @@ public ValidationResult validate(@NotNull V1KafkaConnector resource) { //Validate spec Optional optionalSpec = Optional.ofNullable(resource.getSpec()); optionalSpec.ifPresentOrElse(spec -> { - if (Strings.isBlank(spec.getConnectorClass())) { + if (Strings.isNullOrEmpty(spec.getConnectorClass())) { errors.add(newError(resource, "Missing or empty field: 'spec.connectorClass'.")); } - if (Strings.isBlank(spec.getConnectorClass())) { + if (Strings.isNullOrEmpty(spec.getConnectorClass())) { errors.add(newError(resource, "Missing or empty field: 'spec.tasksMax'.")); } }, () -> errors.add(newError(resource, "Missing or empty field: 'spec'")) diff --git a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/action/KafkaConsumerGroupsResetOffsets.java b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/action/KafkaConsumerGroupsResetOffsets.java index 0db24f2e0..4ce4a61f0 100644 --- a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/action/KafkaConsumerGroupsResetOffsets.java +++ b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/action/KafkaConsumerGroupsResetOffsets.java @@ -132,7 +132,7 @@ public void init(@NotNull ExtensionContext context) { offsetSpec = Config.TO_DATETIME .getOptional(configuration) - .filter(Predicate.not(Strings::isBlank)) + .filter(Predicate.not(Strings::isNullOrEmpty)) .map(dataTime -> (KafkaOffsetSpec) KafkaOffsetSpec.ToTimestamp.fromISODateTime(dataTime)) .orElse(offsetSpec); diff --git a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/change/user/UserChangeComputer.java b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/change/user/UserChangeComputer.java index a345d6fb4..704a3ead5 100644 --- a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/change/user/UserChangeComputer.java +++ b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/change/user/UserChangeComputer.java @@ -162,14 +162,14 @@ public ResourceChange createChangeForDelete(final String key, final V1KafkaUser return switch (authentication) { case V1KafkaUserAuthentication.ScramSha256 scramSha256 -> { yield new V1KafkaUserAuthentication.ScramSha256( - Strings.isBlank(scramSha256.password()) ? SecurePasswordGenerator.getDefault().generate(32) : scramSha256.password(), + Strings.isNullOrEmpty(scramSha256.password()) ? SecurePasswordGenerator.getDefault().generate(32) : scramSha256.password(), Optional.ofNullable(scramSha256.iterations()).orElse(V1KafkaUserAuthentication.DEFAULT_ITERATIONS), scramSha256.salt() ); } case V1KafkaUserAuthentication.ScramSha512 scramSha512 -> { yield new V1KafkaUserAuthentication.ScramSha512( - Strings.isBlank(scramSha512.password()) ? SecurePasswordGenerator.getDefault().generate(32) : scramSha512.password(), + Strings.isNullOrEmpty(scramSha512.password()) ? SecurePasswordGenerator.getDefault().generate(32) : scramSha512.password(), Optional.ofNullable(scramSha512.iterations()).orElse(V1KafkaUserAuthentication.DEFAULT_ITERATIONS), scramSha512.salt() ); diff --git a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaAdminService.java b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaAdminService.java index e37abe0f2..c86cef70f 100644 --- a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaAdminService.java +++ b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaAdminService.java @@ -103,7 +103,7 @@ public V1KafkaConsumerGroup resetConsumerGroupOffsets(@NotNull String groupId, @NotNull List topics, @NotNull OffsetSpec offsetSpec, boolean dryRun) { - if (Strings.isBlank(groupId)) { + if (Strings.isNullOrEmpty(groupId)) { throw new IllegalArgumentException("groupId cannot be null"); } if (topics == null) { diff --git a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaUserService.java b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaUserService.java index 1a1e98166..239123f06 100644 --- a/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaUserService.java +++ b/providers/jikkou-provider-kafka/src/main/java/io/streamthoughts/jikkou/kafka/reconciler/service/KafkaUserService.java @@ -91,7 +91,7 @@ public static Pair han ); String password = auth.password(); - if (Strings.isBlank(auth.password())) { + if (Strings.isNullOrEmpty(auth.password())) { password = SecurePasswordGenerator.getDefault().generate(32); } @@ -114,7 +114,7 @@ public static Pair han ); String password = auth.password(); - if (Strings.isBlank(auth.password())) { + if (Strings.isNullOrEmpty(auth.password())) { password = SecurePasswordGenerator.getDefault().generate(32); } diff --git a/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/reconciler/SchemaRegistrySubjectCollector.java b/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/reconciler/SchemaRegistrySubjectCollector.java index 73efca363..f782337b9 100644 --- a/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/reconciler/SchemaRegistrySubjectCollector.java +++ b/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/reconciler/SchemaRegistrySubjectCollector.java @@ -100,7 +100,6 @@ public ResourceList listAll(@NotNull Configuration conf } public ResourceList listAll(@NotNull Configuration configuration, @NotNull List subjects) { - System.err.println(subjects); try (AsyncSchemaRegistryApi api = new DefaultAsyncSchemaRegistryApi(SchemaRegistryApiFactory.create(config))) { return listAll(configuration, Flux.fromIterable(subjects), api); } diff --git a/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/validation/AvroSchemaValidation.java b/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/validation/AvroSchemaValidation.java index bdcfdbd5b..44771c07f 100644 --- a/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/validation/AvroSchemaValidation.java +++ b/providers/jikkou-provider-schema-registry/src/main/java/io/streamthoughts/jikkou/schema/registry/validation/AvroSchemaValidation.java @@ -123,7 +123,7 @@ private List validateDocFields(V1SchemaRegistrySubject subject, String parentPath) { List errors = new ArrayList<>(); if (schema.getType() == Schema.Type.RECORD) { - if (Strings.isBlank(schema.getDoc())) { + if (Strings.isNullOrEmpty(schema.getDoc())) { errors.add(new ValidationError( getName(), subject, @@ -136,7 +136,7 @@ private List validateDocFields(V1SchemaRegistrySubject subject, } for (Schema.Field field : schema.getFields()) { String fieldPath = parentPath.isEmpty() ? field.name() : parentPath + "." + field.name(); - if (Strings.isBlank(field.doc())) { + if (Strings.isNullOrEmpty(field.doc())) { errors.add(new ValidationError( getName(), subject, diff --git a/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/DefaultJikkouApiClient.java b/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/DefaultJikkouApiClient.java index 7d4668323..250a33fe2 100644 --- a/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/DefaultJikkouApiClient.java +++ b/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/DefaultJikkouApiClient.java @@ -8,6 +8,8 @@ import io.micronaut.http.hateoas.Link; import io.micronaut.http.uri.UriBuilder; +import io.streamthoughts.jikkou.core.GetContext; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.ReconciliationMode; import io.streamthoughts.jikkou.core.config.Configuration; @@ -155,13 +157,17 @@ public ApiHealthIndicatorList getApiHealthIndicators() { **/ @Override public ApiHealthResult getApiHealth(@NotNull String name, - @NotNull Duration timeout) { - HttpUrl url = baseHttpUrlBuilder(API_CORE_VERSION) + @NotNull Duration timeout, + String providerName) { + HttpUrl.Builder urlBuilder = baseHttpUrlBuilder(API_CORE_VERSION) .addPathSegments(API_HEALTHS) .addPathSegments(name) .addPathSegments("status") - .addQueryParameter("timeout", String.valueOf(timeout.toMillis())) - .build(); + .addQueryParameter("timeout", String.valueOf(timeout.toMillis())); + if (providerName != null) { + urlBuilder.addQueryParameter("provider", providerName); + } + HttpUrl url = urlBuilder.build(); // Build Request Request httpRequest = new Request.Builder() .url(url) @@ -176,10 +182,10 @@ public ApiHealthResult getApiHealth(@NotNull String name, * {@inheritDoc} **/ @Override - public ApiHealthResult getApiHealth(@NotNull Duration timeout) { + public ApiHealthResult getApiHealth(@NotNull Duration timeout, String providerName) { ApiHealthIndicatorList list = getApiHealthIndicators(); List health = list.indicators().stream() - .map(indicator -> getApiHealth(indicator.name(), timeout)) + .map(indicator -> getApiHealth(indicator.name(), timeout, providerName)) .map(result -> Health .builder() .name(result.name()) @@ -310,19 +316,27 @@ public ResourceList getResources(@NotNull ResourceTyp @SuppressWarnings("unchecked") public T getResource(@NotNull ResourceType type, @NotNull String name, - @NotNull Configuration configuration) { + @NotNull GetContext context) { ApiResource resource = queryApiResourceForType(type); Link link = findResourceLinkByKey(Links.of(resource.metadata()), ResourceLinkKeys.GET, type); + + Configuration configuration = context.configuration(); + String providerName = context.providerName(); + HashMap expandValues = new HashMap<>(configuration.asMap()); expandValues.put(PATH_PARAM_NAME, name); final URI uri = UriBuilder.of(apiClient.getBasePath()) .path(link.getHref()) .expand(expandValues); - HttpUrl url = toHttpUrl(link); + HttpUrl.Builder urlBuilder = HttpUrl.get(uri).newBuilder(); + if (providerName != null) { + urlBuilder.addQueryParameter("provider", providerName); + } + // Build Request Request httpRequest = new Request.Builder() - .url(HttpUrl.get(uri)) + .url(urlBuilder.build()) .get() .build(); // Execute Request @@ -338,10 +352,14 @@ public T getResource(@NotNull ResourceType type, **/ @Override public ResourceList listResources(@NotNull ResourceType type, - @NotNull Selector selector, - @NotNull Configuration configuration) { + @NotNull ListContext context) { ApiResource resource = queryApiResourceForType(type); Link link = findResourceLinkByKey(Links.of(resource.metadata()), ResourceLinkKeys.SELECT, type); + + Configuration configuration = context.configuration(); + Selector selector = context.selector(); + String providerName = context.providerName(); + final URI uri = UriBuilder.of(apiClient.getBasePath()) .path(link.getHref()) .expand(configuration.asMap()); @@ -349,7 +367,8 @@ public ResourceList listResources(@NotNull ResourceTy ResourceListRequest payload = new ResourceListRequest( configuration.asMap(), selector.getSelectorExpressions(), - selector.getSelectorMatchingStrategy() + selector.getSelectorMatchingStrategy(), + providerName ); // Build Request RequestBody requestBody = apiClient.serialize(payload, "application/json"); @@ -516,13 +535,17 @@ public ApiResourceChangeList getDiff(@NotNull ResourceTy **/ @Override @SuppressWarnings("rawuse") - public ApiActionResultSet execute(@NotNull String action, @NotNull Configuration configuration) { + public ApiActionResultSet execute(@NotNull String action, @NotNull Configuration configuration, String providerName) { // Build Path HttpUrl.Builder builder = baseHttpUrlBuilder(API_CORE_VERSION) .addPathSegments(API_ACTIONS) .addPathSegments(action) .addPathSegments("execute"); + if (providerName != null) { + builder.addQueryParameter("provider", providerName); + } + for (String key : configuration.keys()) { Object value = configuration.getAny(key); if (List.class.isAssignableFrom(value.getClass())) { diff --git a/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiClient.java b/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiClient.java index 256b99dd3..819bf039b 100644 --- a/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiClient.java +++ b/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiClient.java @@ -6,6 +6,8 @@ */ package io.streamthoughts.jikkou.http.client; +import io.streamthoughts.jikkou.core.GetContext; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.ReconciliationMode; import io.streamthoughts.jikkou.core.config.Configuration; @@ -27,7 +29,6 @@ import io.streamthoughts.jikkou.core.models.ResourceType; import io.streamthoughts.jikkou.core.reconciler.Collector; import io.streamthoughts.jikkou.core.reconciler.ResourceChangeFilter; -import io.streamthoughts.jikkou.core.selector.Selector; import io.streamthoughts.jikkou.http.client.exception.JikkouApiClientException; import io.streamthoughts.jikkou.http.client.exception.JikkouApiResponseException; import io.streamthoughts.jikkou.rest.data.Info; @@ -85,22 +86,26 @@ public interface JikkouApiClient { * Gets the health details for the specified health indicator name. * * @param name the health indicator name. + * @param timeout the timeout duration. + * @param providerName the provider name for selecting specific provider instance, or null for default. * @return a new {@link ApiHealthResult} instance. * @throws JikkouApiResponseException if the client receives an error response from the server. * @throws JikkouApiClientException if the client has encountered an error while communicating with the server. * @throws JikkouRuntimeException if the client has encountered a previous fatal error or for any other unexpected error. */ - ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout); + ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout, String providerName); /** * Get the health details for all supported health indicators. * + * @param timeout the timeout duration. + * @param providerName the provider name for selecting specific provider instance, or null for default. * @return a new {@link ApiHealthResult} instance. * @throws JikkouApiResponseException if the client receives an error response from the server. * @throws JikkouApiClientException if the client has encountered an error while communicating with the server. * @throws JikkouRuntimeException if the client has encountered a previous fatal error or for any other unexpected error. */ - ApiHealthResult getApiHealth(@NotNull Duration timeout); + ApiHealthResult getApiHealth(@NotNull Duration timeout, String providerName); /** * Get the supported API extensions. @@ -158,29 +163,33 @@ public interface JikkouApiClient { /** * Get the resource associated for the specified type. * - * @param type The class of the resource to be described. - * @param name The name of the resource. + * @param type The type of the resource to be described. + * @param name The name of the resource. + * @param context The get context containing configuration and provider. * @return the {@link HasMetadata}. * @throws JikkouApiResponseException if the client receives an error response from the server. * @throws JikkouApiClientException if the client has encountered an error while communicating with the server. * @throws JikkouRuntimeException if the client has encountered a previous fatal error or for any other unexpected error. + * @since 0.37.0 */ T getResource(@NotNull ResourceType type, @NotNull String name, - @NotNull Configuration configuration); + @NotNull GetContext context); /** - * List all resources matching the specified type and selectors. + * List all resources matching the specified type and context. * + * @param resourceType the resource type. + * @param context the list context containing selector, configuration and provider. * @param type of the resource-list items. * @return a {@link ResourceList}. * @throws JikkouApiResponseException if the client receives an error response from the server. * @throws JikkouApiClientException if the client has encountered an error while communicating with the server. * @throws JikkouRuntimeException if the client has encountered a previous fatal error or for any other unexpected error. + * @since 0.37.0 */ ResourceList listResources(@NotNull ResourceType resourceType, - @NotNull Selector selector, - @NotNull Configuration configuration); + @NotNull ListContext context); /** * Applies the creation changes required to reconcile the specified resources. @@ -262,11 +271,12 @@ ApiResourceChangeList getDiff(@NotNull ResourceType type * * @param action The name of the action. * @param configuration The context of the execution. + * @param providerName the provider name for selecting specific provider instance, or null for default. * @return The ApiExecutionResult. * @throws JikkouApiResponseException if the client receives an error response from the server. * @throws JikkouApiClientException if the client has encountered an error while communicating with the server. * @throws JikkouRuntimeException if the client has encountered a previous fatal error or for any other unexpected error. */ - ApiActionResultSet execute(@NotNull String action, @NotNull Configuration configuration); + ApiActionResultSet execute(@NotNull String action, @NotNull Configuration configuration, String providerName); } diff --git a/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiProxy.java b/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiProxy.java index 984d6103f..7ac8113a5 100644 --- a/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiProxy.java +++ b/server/jikkou-api-client/src/main/java/io/streamthoughts/jikkou/http/client/JikkouApiProxy.java @@ -9,13 +9,16 @@ import io.streamthoughts.jikkou.common.utils.CollectionUtils; import io.streamthoughts.jikkou.core.BaseApi; import io.streamthoughts.jikkou.core.DefaultApi; +import io.streamthoughts.jikkou.core.GetContext; import io.streamthoughts.jikkou.core.JikkouApi; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.ReconciliationMode; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.extension.Extension; import io.streamthoughts.jikkou.core.extension.ExtensionCategory; import io.streamthoughts.jikkou.core.extension.ExtensionFactory; +import io.streamthoughts.jikkou.core.extension.ProviderConfigurationRegistry; import io.streamthoughts.jikkou.core.models.ApiActionResultSet; import io.streamthoughts.jikkou.core.models.ApiChangeResultList; import io.streamthoughts.jikkou.core.models.ApiExtension; @@ -34,7 +37,6 @@ import io.streamthoughts.jikkou.core.reconciler.ChangeResult; import io.streamthoughts.jikkou.core.reconciler.ResourceChangeFilter; import io.streamthoughts.jikkou.core.resource.ResourceRegistry; -import io.streamthoughts.jikkou.core.selector.Selector; import io.streamthoughts.jikkou.core.validation.ValidationError; import io.streamthoughts.jikkou.http.client.exception.JikkouApiResponseException; import io.streamthoughts.jikkou.rest.data.ErrorEntity; @@ -77,7 +79,7 @@ public Builder(@NotNull ExtensionFactory extensionFactory, **/ @Override public JikkouApiProxy build() { - return new JikkouApiProxy(extensionFactory, resourceRegistry, apiClient); + return new JikkouApiProxy(extensionFactory, resourceRegistry, providerConfigurationRegistry, apiClient); } } @@ -92,8 +94,9 @@ public JikkouApiProxy build() { */ public JikkouApiProxy(final @NotNull ExtensionFactory extensionFactory, final @NotNull ResourceRegistry resourceRegistry, + final @NotNull ProviderConfigurationRegistry providerConfigurationRegistry, final @NotNull JikkouApiClient apiClient) { - super(extensionFactory); + super(extensionFactory, providerConfigurationRegistry); this.apiClient = Objects.requireNonNull(apiClient, "apiClient must not be null"); this.resourceRegistry = Objects.requireNonNull(resourceRegistry, "resourceRegistry must not be null"); } @@ -126,16 +129,16 @@ public ApiHealthIndicatorList getApiHealthIndicators() { * {@inheritDoc} **/ @Override - public ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout) { - return apiClient.getApiHealth(name, timeout); + public ApiHealthResult getApiHealth(@NotNull String name, @NotNull Duration timeout, String providerName) { + return apiClient.getApiHealth(name, timeout, providerName); } /** * {@inheritDoc} **/ @Override - public ApiHealthResult getApiHealth(@NotNull Duration timeout) { - return apiClient.getApiHealth(timeout); + public ApiHealthResult getApiHealth(@NotNull Duration timeout, String providerName) { + return apiClient.getApiHealth(timeout, providerName); } /** @@ -242,8 +245,9 @@ public ApiChangeResultList replace(@NotNull HasItems resources, @NotNull Reconci @Override @SuppressWarnings("unchecked") public ApiActionResultSet execute(@NotNull String action, - @NotNull Configuration configuration) { - return (ApiActionResultSet) apiClient.execute(action, configuration); + @NotNull Configuration configuration, + String providerName) { + return (ApiActionResultSet) apiClient.execute(action, configuration, providerName); } /** @@ -293,8 +297,8 @@ public ApiValidationResult validate(@NotNull HasItems resources, @Override public T getResource(@NotNull ResourceType type, @NotNull String name, - @NotNull Configuration configuration) { - return apiClient.getResource(type, name, configuration); + @NotNull GetContext context) { + return apiClient.getResource(type, name, context); } /** @@ -320,9 +324,8 @@ public ApiResourceChangeList getDiff(@NotNull HasItems resources, **/ @Override public ResourceList listResources(@NotNull ResourceType resourceType, - @NotNull Selector selector, - @NotNull Configuration configuration) { - return apiClient.listResources(resourceType, selector, configuration); + @NotNull ListContext context) { + return apiClient.listResources(resourceType, context); } /** diff --git a/server/jikkou-api-client/src/test/java/io/streamthoughts/jikkou/http/client/JikkouApiProxyTest.java b/server/jikkou-api-client/src/test/java/io/streamthoughts/jikkou/http/client/JikkouApiProxyTest.java index f88a6fe3f..764613d9b 100644 --- a/server/jikkou-api-client/src/test/java/io/streamthoughts/jikkou/http/client/JikkouApiProxyTest.java +++ b/server/jikkou-api-client/src/test/java/io/streamthoughts/jikkou/http/client/JikkouApiProxyTest.java @@ -10,6 +10,7 @@ import io.streamthoughts.jikkou.core.extension.DefaultExtensionDescriptorFactory; import io.streamthoughts.jikkou.core.extension.DefaultExtensionFactory; import io.streamthoughts.jikkou.core.extension.DefaultExtensionRegistry; +import io.streamthoughts.jikkou.core.extension.DefaultProviderConfigurationRegistry; import io.streamthoughts.jikkou.core.extension.ExtensionFactory; import io.streamthoughts.jikkou.core.models.ApiExtensionList; import io.streamthoughts.jikkou.core.models.ApiExtensionSummary; @@ -60,7 +61,7 @@ public static void beforeAll() throws IOException { ) ); DefaultResourceRegistry resourceRegistry = new DefaultResourceRegistry(); - API = new JikkouApiProxy(factory, resourceRegistry, new DefaultJikkouApiClient(client)); + API = new JikkouApiProxy(factory, resourceRegistry, new DefaultProviderConfigurationRegistry(), new DefaultJikkouApiClient(client)); } @Test diff --git a/server/jikkou-api-data/src/main/java/io/streamthoughts/jikkou/rest/data/ResourceListRequest.java b/server/jikkou-api-data/src/main/java/io/streamthoughts/jikkou/rest/data/ResourceListRequest.java index 88e806740..51edfe4e5 100644 --- a/server/jikkou-api-data/src/main/java/io/streamthoughts/jikkou/rest/data/ResourceListRequest.java +++ b/server/jikkou-api-data/src/main/java/io/streamthoughts/jikkou/rest/data/ResourceListRequest.java @@ -22,21 +22,26 @@ * @param options The parameters. * @param selectors The list of selector expressions. * @param selectorMatchingStrategy The selector matching strategy. + * @param provider The provider name for selecting specific provider instance. */ public record ResourceListRequest(@Nullable @JsonProperty("options") Map options, @Nullable @JsonProperty("selectors") List selectors, - @Nullable @JsonProperty("selectors_match") SelectorMatchingStrategy selectorMatchingStrategy) { + @Nullable @JsonProperty("selectors_match") SelectorMatchingStrategy selectorMatchingStrategy, + @Nullable @JsonProperty("provider") String provider) { /** * Creates a new {@link ResourceListRequest} instance. * * @param options The parameters. * @param selectors The selector expression. + * @param selectorMatchingStrategy The selector matching strategy. + * @param provider The provider name. */ @ConstructorProperties({ "options", "selectors", - "selectors_match" + "selectors_match", + "provider" }) public ResourceListRequest { } @@ -47,20 +52,33 @@ public record ResourceListRequest(@Nullable @JsonProperty("options") Map options) { - this(options, null, null); + this(options, null, null, null); } /** * Creates a new {@link ResourceListRequest} instance. */ public ResourceListRequest() { - this(null, null, null); + this(null, null, null, null); + } + + /** + * Creates a new {@link ResourceListRequest} instance with selector parameters. + * + * @param options The parameters. + * @param selectors The selector expressions. + * @param selectorMatchingStrategy The selector matching strategy. + */ + public ResourceListRequest(Map options, + List selectors, + SelectorMatchingStrategy selectorMatchingStrategy) { + this(options, selectors, selectorMatchingStrategy, null); } public ResourceListRequest options(Map options) { Map map = new HashMap<>(options()); map.putAll(options); - return new ResourceListRequest(map, selectors, selectorMatchingStrategy); + return new ResourceListRequest(map, selectors, selectorMatchingStrategy, provider); } /** diff --git a/server/jikkou-api-data/src/test/java/io/streamthoughts/jikkou/rest/data/ResourceListRequestTest.java b/server/jikkou-api-data/src/test/java/io/streamthoughts/jikkou/rest/data/ResourceListRequestTest.java index d29ef6fd1..51996bd1b 100644 --- a/server/jikkou-api-data/src/test/java/io/streamthoughts/jikkou/rest/data/ResourceListRequestTest.java +++ b/server/jikkou-api-data/src/test/java/io/streamthoughts/jikkou/rest/data/ResourceListRequestTest.java @@ -6,6 +6,10 @@ */ package io.streamthoughts.jikkou.rest.data; +import io.streamthoughts.jikkou.core.selector.SelectorMatchingStrategy; +import java.util.Collections; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,5 +20,132 @@ void shouldGetEmptyForNoArgs() { ResourceListRequest request = new ResourceListRequest(); Assertions.assertNotNull(request.options()); Assertions.assertNotNull(request.selectors()); + Assertions.assertNull(request.provider()); + } + + @Test + void shouldCreateRequestWithProvider() { + // Given + String providerName = "kafka-prod"; + + // When + ResourceListRequest request = new ResourceListRequest( + Map.of("key", "value"), + List.of("selector1"), + SelectorMatchingStrategy.ALL, + providerName + ); + + // Then + Assertions.assertEquals(providerName, request.provider()); + Assertions.assertEquals(Map.of("key", "value"), request.options()); + Assertions.assertEquals(List.of("selector1"), request.selectors()); + Assertions.assertEquals(SelectorMatchingStrategy.ALL, request.selectorMatchingStrategy()); + } + + @Test + void shouldCreateRequestWithNullProvider() { + // When + ResourceListRequest request = new ResourceListRequest( + Map.of("key", "value"), + List.of("selector1"), + SelectorMatchingStrategy.ALL, + null + ); + + // Then + Assertions.assertNull(request.provider()); + } + + @Test + void shouldCreateRequestWithOptionsOnly() { + // Given + Map options = Map.of("key", "value"); + + // When + ResourceListRequest request = new ResourceListRequest(options); + + // Then + Assertions.assertEquals(options, request.options()); + Assertions.assertTrue(request.selectors().isEmpty()); + Assertions.assertEquals(SelectorMatchingStrategy.ALL, request.selectorMatchingStrategy()); + Assertions.assertNull(request.provider()); + } + + @Test + void shouldCreateRequestWithSelectorsButNoProvider() { + // When + ResourceListRequest request = new ResourceListRequest( + Map.of("key", "value"), + List.of("selector1", "selector2"), + SelectorMatchingStrategy.ANY + ); + + // Then + Assertions.assertEquals(Map.of("key", "value"), request.options()); + Assertions.assertEquals(List.of("selector1", "selector2"), request.selectors()); + Assertions.assertEquals(SelectorMatchingStrategy.ANY, request.selectorMatchingStrategy()); + Assertions.assertNull(request.provider()); + } + + @Test + void shouldPreserveProviderWhenAddingOptions() { + // Given + ResourceListRequest original = new ResourceListRequest( + Map.of("key1", "value1"), + List.of("selector1"), + SelectorMatchingStrategy.ALL, + "kafka-prod" + ); + + // When + ResourceListRequest updated = original.options(Map.of("key2", "value2")); + + // Then + Assertions.assertEquals("kafka-prod", updated.provider()); + Assertions.assertEquals("value1", updated.options().get("key1")); + Assertions.assertEquals("value2", updated.options().get("key2")); + } + + @Test + void shouldReturnDefaultSelectorMatchingStrategyWhenNull() { + // When + ResourceListRequest request = new ResourceListRequest( + Collections.emptyMap(), + Collections.emptyList(), + null, + "provider" + ); + + // Then + Assertions.assertEquals(SelectorMatchingStrategy.ALL, request.selectorMatchingStrategy()); + } + + @Test + void shouldReturnEmptyListWhenSelectorsNull() { + // When + ResourceListRequest request = new ResourceListRequest( + Collections.emptyMap(), + null, + SelectorMatchingStrategy.ALL, + "provider" + ); + + // Then + Assertions.assertTrue(request.selectors().isEmpty()); + } + + @Test + void shouldReturnEmptyMapWhenOptionsNull() { + // When + ResourceListRequest request = new ResourceListRequest( + null, + Collections.emptyList(), + SelectorMatchingStrategy.ALL, + "provider" + ); + + // Then + Assertions.assertTrue(request.options().isEmpty()); } } \ No newline at end of file diff --git a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapter.java b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapter.java index ea2bae559..b73d7383b 100644 --- a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapter.java +++ b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapter.java @@ -6,6 +6,7 @@ */ package io.streamthoughts.jikkou.rest.adapters; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.config.Configuration; import io.streamthoughts.jikkou.core.models.NamedValueSet; @@ -52,6 +53,24 @@ public ReconciliationContext getReconciliationContext(ResourceListRequest reques .dryRun(true) .configuration(Configuration.from(request.options())) .selector(selector) + .providerName(request.provider()) + .build(); + } + + /** + * Creates a ListContext from a ResourceListRequest. + * + * @param request the ResourceListRequest. + * @return a new ListContext instance. + */ + public ListContext getListContext(ResourceListRequest request) { + Selector selector = request.selectorMatchingStrategy() + .combines(selectorFactory.make(request.selectors())); + return ListContext + .builder() + .configuration(Configuration.from(request.options())) + .selector(selector) + .providerName(request.provider()) .build(); } } diff --git a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiExtensionResource.java b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiExtensionResource.java index eaa34655d..2a56a6306 100644 --- a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiExtensionResource.java +++ b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiExtensionResource.java @@ -52,9 +52,9 @@ public ResourceResponse list(HttpRequest request, ApiExtensionList extensions; - if (!Strings.isBlank(category)) { + if (!Strings.isNullOrEmpty(category)) { extensions = api.getApiExtensions(ExtensionCategory.getForNameIgnoreCase(category)); - }else if (!Strings.isBlank(type)) { + }else if (!Strings.isNullOrEmpty(type)) { extensions = api.getApiExtensions(type); } else { extensions = api.getApiExtensions(); diff --git a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiResource.java b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiResource.java index 1eebf17a3..b5762c78f 100644 --- a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiResource.java +++ b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/resources/ApiResource.java @@ -119,7 +119,7 @@ private MutableHttpResponse doSelect(HttpRequest httpRequest, ApiResourceIdentifier identifier = new ApiResourceIdentifier(group, version, plural); ResourceList result = service.search( identifier, - adapter.getReconciliationContext(requestBody) + adapter.getListContext(requestBody) ); return HttpResponse.>ok() .body(new ResourceResponse<>(result).link(Link.SELF, getSelfLink(httpRequest))); diff --git a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/ApiResourceService.java b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/ApiResourceService.java index 29df05656..da4213045 100644 --- a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/ApiResourceService.java +++ b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/ApiResourceService.java @@ -6,6 +6,7 @@ */ package io.streamthoughts.jikkou.rest.services; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.ReconciliationMode; import io.streamthoughts.jikkou.core.config.Configuration; @@ -71,11 +72,11 @@ ResourceList validate(ApiResourceIdentifier identifier, * Search resources for the specified identifier and context. * * @param identifier The resource identifier. - * @param context The reconciliation context. - * @return an optional + * @param context The list context containing selector, configuration and provider. + * @return the list of matching resources. */ ResourceList search(ApiResourceIdentifier identifier, - ReconciliationContext context); + ListContext context); /** * Gets the resource for the specified identifier and name. diff --git a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/DefaultApiResourceService.java b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/DefaultApiResourceService.java index 8fb7f11c0..7d0b24b0b 100644 --- a/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/DefaultApiResourceService.java +++ b/server/jikkou-api-server/src/main/java/io/streamthoughts/jikkou/rest/services/DefaultApiResourceService.java @@ -7,6 +7,7 @@ package io.streamthoughts.jikkou.rest.services; import io.streamthoughts.jikkou.core.JikkouApi; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.ReconciliationMode; import io.streamthoughts.jikkou.core.config.Configuration; @@ -102,12 +103,11 @@ public ResourceList validate(ApiResourceIdentifier identifier, **/ @Override public ResourceList search(ApiResourceIdentifier identifier, - ReconciliationContext context) { + ListContext context) { ResourceDescriptor descriptor = getResourceDescriptorByIdentifier(identifier); return api.listResources( descriptor.resourceType(), - context.selector(), - context.configuration() + context ); } diff --git a/server/jikkou-api-server/src/test/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapterTest.java b/server/jikkou-api-server/src/test/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapterTest.java index 7f06d7466..6a90a2607 100644 --- a/server/jikkou-api-server/src/test/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapterTest.java +++ b/server/jikkou-api-server/src/test/java/io/streamthoughts/jikkou/rest/adapters/ReconciliationContextAdapterTest.java @@ -6,11 +6,14 @@ */ package io.streamthoughts.jikkou.rest.adapters; +import io.streamthoughts.jikkou.core.ListContext; import io.streamthoughts.jikkou.core.ReconciliationContext; import io.streamthoughts.jikkou.core.selector.SelectorFactory; +import io.streamthoughts.jikkou.core.selector.SelectorMatchingStrategy; import io.streamthoughts.jikkou.rest.data.ResourceListRequest; import io.streamthoughts.jikkou.rest.data.ResourceReconcileRequest; import java.util.Collections; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -20,8 +23,8 @@ class ReconciliationContextAdapterTest { void shouldGetCtxForResourceReconcileRequest() { ReconciliationContextAdapter adapter = new ReconciliationContextAdapter(new SelectorFactory()); ReconciliationContext context = adapter.getReconciliationContext(new ResourceReconcileRequest( - new ResourceReconcileRequest.Params(), - Collections.emptyList() + new ResourceReconcileRequest.Params(), + Collections.emptyList() ), false); Assertions.assertNotNull(context); } @@ -31,6 +34,60 @@ void shouldGetCtxForResourceListRequest() { ReconciliationContextAdapter adapter = new ReconciliationContextAdapter(new SelectorFactory()); ReconciliationContext context = adapter.getReconciliationContext(new ResourceListRequest()); Assertions.assertNotNull(context); + } + + @Test + void shouldGetListContextForResourceListRequest() { + // Given + ReconciliationContextAdapter adapter = new ReconciliationContextAdapter(new SelectorFactory()); + ResourceListRequest request = new ResourceListRequest(); + + // When + ListContext context = adapter.getListContext(request); + + // Then + Assertions.assertNotNull(context); + Assertions.assertNotNull(context.selector()); + Assertions.assertNotNull(context.configuration()); + Assertions.assertNull(context.providerName()); + } + + @Test + void shouldGetListContextWithProvider() { + // Given + ReconciliationContextAdapter adapter = new ReconciliationContextAdapter(new SelectorFactory()); + ResourceListRequest request = new ResourceListRequest( + Map.of("key", "value"), + Collections.emptyList(), + SelectorMatchingStrategy.ALL, + "kafka-prod" + ); + + // When + ListContext context = adapter.getListContext(request); + + // Then + Assertions.assertNotNull(context); + Assertions.assertEquals("kafka-prod", context.providerName()); + Assertions.assertEquals("value", context.configuration().getString("key")); + } + @Test + void shouldGetReconciliationContextWithProviderFromResourceListRequest() { + // Given + ReconciliationContextAdapter adapter = new ReconciliationContextAdapter(new SelectorFactory()); + ResourceListRequest request = new ResourceListRequest( + Map.of("key", "value"), + Collections.emptyList(), + SelectorMatchingStrategy.ALL, + "kafka-prod" + ); + + // When + ReconciliationContext context = adapter.getReconciliationContext(request); + + // Then + Assertions.assertNotNull(context); + Assertions.assertEquals("kafka-prod", context.providerName()); } } \ No newline at end of file diff --git a/template-jinja/src/main/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRenderer.java b/template-jinja/src/main/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRenderer.java index 950bac6f8..50c623fb3 100644 --- a/template-jinja/src/main/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRenderer.java +++ b/template-jinja/src/main/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRenderer.java @@ -6,6 +6,8 @@ */ package io.streamthoughts.jikkou.api.template; +import static java.util.stream.Collectors.toCollection; + import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.RenderResult; @@ -22,12 +24,6 @@ import io.streamthoughts.jikkou.core.exceptions.JikkouRuntimeException; import io.streamthoughts.jikkou.core.template.ResourceTemplateRenderer; import io.streamthoughts.jikkou.core.template.TemplateBindings; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.File; import java.io.FileNotFoundException; import java.net.URI; @@ -41,8 +37,11 @@ import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; - -import static java.util.stream.Collectors.toCollection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A Jinja based template rendered. diff --git a/template-jinja/src/test/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRendererTest.java b/template-jinja/src/test/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRendererTest.java index 080a4ebf2..59e1bbe37 100644 --- a/template-jinja/src/test/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRendererTest.java +++ b/template-jinja/src/test/java/io/streamthoughts/jikkou/api/template/JinjaResourceTemplateRendererTest.java @@ -15,6 +15,7 @@ import io.streamthoughts.jikkou.core.models.HasMetadata; import io.streamthoughts.jikkou.core.models.NamedValueSet; import io.streamthoughts.jikkou.core.models.generics.GenericResource; +import io.streamthoughts.jikkou.core.template.TemplateBindings; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -22,8 +23,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import io.streamthoughts.jikkou.core.template.TemplateBindings; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir;