diff --git a/starter-test/src/test/java/io/javaoperatorsdk/operator/springboot/starter/test/EnableMockOperatorTests.java b/starter-test/src/test/java/io/javaoperatorsdk/operator/springboot/starter/test/EnableMockOperatorTests.java index 464944a..7b4edf6 100644 --- a/starter-test/src/test/java/io/javaoperatorsdk/operator/springboot/starter/test/EnableMockOperatorTests.java +++ b/starter-test/src/test/java/io/javaoperatorsdk/operator/springboot/starter/test/EnableMockOperatorTests.java @@ -22,8 +22,8 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.springboot.starter.OperatorConfigurationProperties; -import io.javaoperatorsdk.operator.springboot.starter.ReconcilerProperties; +import io.javaoperatorsdk.operator.springboot.starter.properties.OperatorConfigurationProperties; +import io.javaoperatorsdk.operator.springboot.starter.properties.ReconcilerProperties; import io.javaoperatorsdk.operator.springboot.starter.sample.CustomService; import io.javaoperatorsdk.operator.springboot.starter.sample.CustomServiceReconciler; import io.javaoperatorsdk.operator.springboot.starter.sample.ServiceSpec; diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplier.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplier.java deleted file mode 100644 index 5b7364e..0000000 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplier.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.javaoperatorsdk.operator.springboot.starter; - -import java.io.IOException; -import java.nio.file.Paths; -import java.util.List; -import java.util.function.UnaryOperator; - -import org.slf4j.Logger; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.client.KubernetesClient; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.joining; -import static org.slf4j.LoggerFactory.getLogger; - -@FunctionalInterface -public interface CRDApplier { - - CRDApplier NOOP = () -> getLogger(CRDApplier.class).debug("Not searching for CRDs to apply"); - - void apply(); - - interface CRDTransformer extends UnaryOperator { - default CRDTransformer thenTransform(CRDApplier.CRDTransformer after) { - return t -> after.apply(apply(t)); - } - - static CRDTransformer reduce(List transformers) { - return transformers.stream().reduce(t -> t, CRDTransformer::thenTransform); - } - } - - class DefaultCRDApplier implements CRDApplier { - - private static final Logger log = getLogger(DefaultCRDApplier.class); - - private static final int CRD_READY_WAIT = 2000; - - private final CRDTransformer crdTransformer; - private final KubernetesClient kubernetesClient; - private final String crdSuffix; - private final String crdPath; - - public DefaultCRDApplier(KubernetesClient kubernetesClient, List transformers, - String crdPath, String crdSuffix) { - this.crdTransformer = CRDTransformer.reduce(transformers); - this.kubernetesClient = kubernetesClient; - this.crdSuffix = crdSuffix; - this.crdPath = crdPath; - } - - @Override - public void apply() { - log.debug("Uploading CRDs with suffix {} under {}", crdSuffix, crdPath); - stream(findResources()).forEach(this::applyCrd); - } - - private Resource[] findResources() { - final var resourceResolver = new PathMatchingResourcePatternResolver(); - final var resourceLocationPattern = Paths.get(crdPath, '*' + crdSuffix).toString(); - try { - return resourceResolver.getResources(resourceLocationPattern); - } catch (IOException e) { - throw new RuntimeException( - "could not find CRD resources from the location pattern: " + resourceLocationPattern); - } - } - - private void applyCrd(Resource crdResource) { - try (var is = crdResource.getInputStream()) { - var crds = kubernetesClient.load(is).items().stream().map(crdTransformer).toList(); - kubernetesClient.resourceList(crds).createOrReplace(); - - Thread.sleep(CRD_READY_WAIT); // readiness is not applicable for CRD, just wait a little - - logUploaded(crds); - } catch (InterruptedException | IOException ex) { - throw new RuntimeException(ex); - } - } - - private void logUploaded(List crds) { - var crdNames = crds.stream() - .map(HasMetadata::getMetadata) - .map(ObjectMeta::getName) - .collect(joining(", ")); - log.info("Uploaded CRDs: {}", crdNames); - } - - } -} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/DependentResourceConfigurator.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/DependentResourceConfigurator.java new file mode 100644 index 0000000..80cadce --- /dev/null +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/DependentResourceConfigurator.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.springboot.starter; + +@FunctionalInterface +public interface DependentResourceConfigurator { + + String getName(); + +} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorAutoConfiguration.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorAutoConfiguration.java index 7c17585..7555bf1 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorAutoConfiguration.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorAutoConfiguration.java @@ -1,18 +1,15 @@ package io.javaoperatorsdk.operator.springboot.starter; import java.util.List; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,14 +25,12 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.DefaultResourceClassResolver; import io.javaoperatorsdk.operator.api.config.ResourceClassResolver; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -import io.javaoperatorsdk.operator.springboot.starter.CRDApplier.CRDTransformer; -import io.javaoperatorsdk.operator.springboot.starter.CRDApplier.DefaultCRDApplier; +import io.javaoperatorsdk.operator.springboot.starter.crd.CRDApplier; +import io.javaoperatorsdk.operator.springboot.starter.properties.OperatorConfigurationProperties; @Configuration @EnableConfigurationProperties(OperatorConfigurationProperties.class) @@ -44,25 +39,23 @@ public class OperatorAutoConfiguration { private final static Logger log = LoggerFactory.getLogger(OperatorAutoConfiguration.class); @Autowired - private OperatorConfigurationProperties configuration; + private OperatorConfigurationProperties configurationProperties; @Bean @ConditionalOnMissingBean - public KubernetesClient kubernetesClient(Optional httpClientFactory, - Config config) { - return configuration.getClient().isOpenshift() - ? httpClientFactory - .map(it -> new KubernetesClientBuilder().withHttpClientFactory(it).withConfig(config) - .build().adapt(OpenShiftClient.class)) - // new DefaultOpenShiftClient(it.createHttpClient(config), - // new OpenShiftConfig(config))) - .orElseGet(() -> new KubernetesClientBuilder().withConfig(config) - .build().adapt(OpenShiftClient.class)) - : httpClientFactory - .map(it -> new KubernetesClientBuilder().withHttpClientFactory(it).withConfig(config) - .build()) - .orElseGet(() -> new KubernetesClientBuilder().withConfig(config) - .build()); + public KubernetesClient kubernetesClient( + @Autowired(required = false) HttpClient.Factory httpClientFactory, Config config) { + KubernetesClientBuilder clientBuilder = new KubernetesClientBuilder().withConfig(config); + + if (httpClientFactory != null) { + clientBuilder = clientBuilder.withHttpClientFactory(httpClientFactory); + } + KubernetesClient client = clientBuilder.build(); + + if (configurationProperties.getClient().isOpenshift()) { + return client.adapt(OpenShiftClient.class); + } + return client; } @Bean @@ -71,107 +64,78 @@ public ResourceClassResolver resourceClassResolver() { return new DefaultResourceClassResolver(); } - @Bean - @ConditionalOnProperty(value = "javaoperatorsdk.crd.apply-on-startup", havingValue = "true") - public CRDApplier crdApplier(KubernetesClient client, List transformers) { - var crd = configuration.getCrd(); - return new DefaultCRDApplier(client, transformers, crd.getPath(), crd.getSuffix()); - } - @Bean @ConditionalOnMissingBean(CRDApplier.class) public CRDApplier disabledCrdApplier() { + if (log.isDebugEnabled()) { + log.debug("no CRDApplier loaded, using NOOP"); + } return CRDApplier.NOOP; } - @Bean - public OperatorStarter operatorStarter(Operator operator, CRDApplier applier) { - return new OperatorStarter(operator, applier); - } - @Bean(destroyMethod = "stop") @ConditionalOnMissingBean(Operator.class) - public Operator operator( - BiConsumer> reconcilerRegisterer, - @Qualifier("compositeConfigurationServiceOverrider") Consumer compositeConfigurationServiceOverrider, - KubernetesClient kubernetesClient, - List> reconcilers) { - - var operator = new Operator(compositeConfigurationServiceOverrider); - reconcilers.forEach(reconciler -> reconcilerRegisterer.accept(operator, reconciler)); - - return operator; - } - - @Bean - public BiConsumer> reconcilerRegisterer() { - return (operator, reconciler) -> { - var name = ReconcilerUtils.getNameFor(reconciler); - var props = configuration.getReconcilers().get(name); - - operator.register(reconciler, overrider -> overrideFromProps(overrider, props)); - }; - } - - @Bean - public Consumer compositeConfigurationServiceOverrider( - List> configServiceOverriders) { - return configServiceOverriders.stream() + public Operator operator(List> configServiceOverriders, + List> reconcilers, + List dependentResourceConfigurators) { + var chainedOverriders = configServiceOverriders.stream() .reduce(Consumer::andThen) .orElseThrow( () -> new IllegalStateException("Default Config Service Overrider Not Created")); + + var dependentResourceConfiguratorsMap = dependentResourceConfigurators == null ? null + : dependentResourceConfigurators.stream() + .collect(Collectors.groupingBy(DependentResourceConfigurator::getName)); + + var operator = new Operator(chainedOverriders); + reconcilers.forEach(reconciler -> { + var name = ReconcilerUtils.getNameFor(reconciler); + var props = configurationProperties.getReconcilers().get(name); + operator.register(reconciler, o -> { + ReconcilerRegistrationUtil.overrideFromProps(o, props); + if (dependentResourceConfiguratorsMap != null) { + var drsForReconciler = ReconcilerRegistrationUtil.filterConfigurators(reconciler, + dependentResourceConfiguratorsMap); + drsForReconciler.forEach(dr -> o.replacingNamedDependentResourceConfig(dr.getName(), dr)); + } + }); + }); + return operator; } @Bean @Order(0) public Consumer defaultConfigServiceOverrider( - @Autowired(required = false) Cloner cloner, - Metrics metrics, KubernetesClient kubernetesClient) { + @Autowired(required = false) Cloner cloner, Metrics metrics, + KubernetesClient kubernetesClient) { return overrider -> { - doIfPresent(cloner, overrider::withResourceCloner); - doIfPresent(configuration.getStopOnInformerErrorDuringStartup(), + ReconcilerRegistrationUtil.doIfPresent(cloner, overrider::withResourceCloner); + ReconcilerRegistrationUtil.doIfPresent( + configurationProperties.getStopOnInformerErrorDuringStartup(), overrider::withStopOnInformerErrorDuringStartup); - doIfPresent(configuration.getConcurrentWorkflowExecutorThreads(), + ReconcilerRegistrationUtil.doIfPresent( + configurationProperties.getConcurrentWorkflowExecutorThreads(), overrider::withConcurrentWorkflowExecutorThreads); - doIfPresent(configuration.getCloseClientOnStop(), overrider::withCloseClientOnStop); - doIfPresent(configuration.getCacheSyncTimeout(), overrider::withCacheSyncTimeout); + ReconcilerRegistrationUtil.doIfPresent(configurationProperties.getCloseClientOnStop(), + overrider::withCloseClientOnStop); + ReconcilerRegistrationUtil.doIfPresent(configurationProperties.getCacheSyncTimeout(), + overrider::withCacheSyncTimeout); + overrider - .withConcurrentReconciliationThreads(configuration.getConcurrentReconciliationThreads()) + .withConcurrentReconciliationThreads( + configurationProperties.getConcurrentReconciliationThreads()) .withMetrics(metrics) - .checkingCRDAndValidateLocalModel(configuration.getCheckCrdAndValidateLocalModel()) + .checkingCRDAndValidateLocalModel( + configurationProperties.isCheckCrdAndValidateLocalModel()) .withKubernetesClient(kubernetesClient); }; } - private void overrideFromProps(ControllerConfigurationOverrider overrider, - ReconcilerProperties props) { - if (props != null) { - doIfPresent(props.getFinalizerName(), overrider::withFinalizer); - doIfPresent(props.getName(), overrider::withName); - doIfPresent(props.getNamespaces(), overrider::settingNamespaces); - doIfPresent(props.getRetry(), r -> { - var retry = new GenericRetry(); - doIfPresent(r.getInitialInterval(), retry::setInitialInterval); - doIfPresent(r.getMaxAttempts(), retry::setMaxAttempts); - doIfPresent(r.getMaxInterval(), retry::setMaxInterval); - doIfPresent(r.getIntervalMultiplier(), retry::setIntervalMultiplier); - overrider.withRetry(retry); - }); - doIfPresent(props.isGenerationAware(), overrider::withGenerationAware); - doIfPresent(props.isClusterScoped(), clusterScoped -> { - if (clusterScoped) { - overrider.watchingAllNamespaces(); - } - }); - doIfPresent(props.getLabelSelector(), overrider::withLabelSelector); - doIfPresent(props.getReconciliationMaxInterval(), overrider::withReconciliationMaxInterval); - } - } - @Bean @ConditionalOnMissingBean(name = "reconciliationExecutorService") public ExecutorService reconciliationExecutorService() { - return Executors.newFixedThreadPool(configuration.getConcurrentReconciliationThreads()); + return Executors + .newFixedThreadPool(configurationProperties.getConcurrentReconciliationThreads()); } @Bean @@ -183,7 +147,7 @@ public Metrics metrics() { @Bean public Config getClientConfiguration( @Autowired(required = false) KubernetesConfigCustomizer configCustomizer) { - return configuration.getClient().getContext() + return configurationProperties.getClient().getContext() .map(Config::autoConfigure) .map(it -> { if (configCustomizer != null) { @@ -195,7 +159,7 @@ public Config getClientConfiguration( } }) .orElseGet(() -> { - final var clientCfg = configuration.getClient(); + final var clientCfg = configurationProperties.getClient(); ConfigBuilder config = new ConfigBuilder(); config.withTrustCerts(clientCfg.isTrustSelfSignedCertificates()); clientCfg.getMasterUrl().ifPresent(config::withMasterUrl); @@ -210,8 +174,4 @@ public Config getClientConfiguration( }); } - private void doIfPresent(T prop, Consumer action) { - Optional.ofNullable(prop).ifPresent(action); - } - } diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorHealthIndicator.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorHealthIndicator.java index 8f28782..698e999 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorHealthIndicator.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorHealthIndicator.java @@ -12,9 +12,8 @@ @Component public class OperatorHealthIndicator extends AbstractHealthIndicator { - private final Operator operator; - private final static Logger log = LoggerFactory.getLogger(OperatorHealthIndicator.class); + private final Operator operator; public OperatorHealthIndicator(final Operator operator) { super("OperatorSDK health check failed"); diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarter.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarter.java index e75235b..5407876 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarter.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarter.java @@ -5,9 +5,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.springboot.starter.crd.CRDApplier; + +@Component public class OperatorStarter { private static final Logger log = LoggerFactory.getLogger(OperatorStarter.class); @@ -15,6 +19,7 @@ public class OperatorStarter { private final Operator operator; private final CRDApplier crdApplier; + public OperatorStarter(Operator operator, CRDApplier crdApplier) { this.operator = operator; this.crdApplier = crdApplier; @@ -22,18 +27,17 @@ public OperatorStarter(Operator operator, CRDApplier crdApplier) { @EventListener public void start(ApplicationReadyEvent event) { - if (!operator.getRegisteredControllers().isEmpty()) { - try { - crdApplier.apply(); - operator.start(); - } catch (Exception ex) { - log.error("Could not start operator", ex); - SpringApplication.exit(event.getApplicationContext(), () -> 1); - } - } else { + if (operator.getRegisteredControllers().isEmpty()) { log.warn( "No Reconcilers found in the application context: Not starting the Operator, not looking for CRDs"); + return; + } + try { + crdApplier.apply(); + operator.start(); + } catch (Exception ex) { + log.error("Could not start operator", ex); + SpringApplication.exit(event.getApplicationContext(), () -> 1); } } - } diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerRegistrationUtil.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerRegistrationUtil.java new file mode 100644 index 0000000..b93f47f --- /dev/null +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerRegistrationUtil.java @@ -0,0 +1,68 @@ +package io.javaoperatorsdk.operator.springboot.starter; + +import java.util.*; +import java.util.function.Consumer; + +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.springboot.starter.properties.ReconcilerProperties; + + +public class ReconcilerRegistrationUtil { + + public static List filterConfigurators(Reconciler reconciler, + Map> configuratorsMap) { + var workflow = reconciler.getClass().getAnnotation(Workflow.class); + if (workflow == null) { + return Collections.emptyList(); + } + + var dependents = workflow.dependents(); + ArrayList relevant = new ArrayList<>(); + for (Dependent dependent : dependents) { + var name = dependent.name(); + var configurators = configuratorsMap.get(name); + if (configurators == null || configurators.isEmpty()) { + continue; + } + if (configurators.size() > 1) { + throw new IllegalStateException("more than one config for Dependent Resource " + name + + " - " + configurators.stream().map(o -> o.getClass().getName()).toList()); + } + relevant.add(configurators.get(0)); + } + return relevant; + } + + public static void overrideFromProps(ControllerConfigurationOverrider overrider, + ReconcilerProperties props) { + if (props != null) { + doIfPresent(props.getFinalizerName(), overrider::withFinalizer); + doIfPresent(props.getName(), overrider::withName); + doIfPresent(props.getNamespaces(), overrider::settingNamespaces); + doIfPresent(props.getRetry(), r -> { + var retry = new GenericRetry(); + doIfPresent(r.getInitialInterval(), retry::setInitialInterval); + doIfPresent(r.getMaxAttempts(), retry::setMaxAttempts); + doIfPresent(r.getMaxInterval(), retry::setMaxInterval); + doIfPresent(r.getIntervalMultiplier(), retry::setIntervalMultiplier); + overrider.withRetry(retry); + }); + doIfPresent(props.getGenerationAware(), overrider::withGenerationAware); + doIfPresent(props.getClusterScoped(), clusterScoped -> { + if (clusterScoped) { + overrider.watchingAllNamespaces(); + } + }); + doIfPresent(props.getLabelSelector(), overrider::withLabelSelector); + doIfPresent(props.getReconciliationMaxInterval(), overrider::withReconciliationMaxInterval); + } + } + + public static void doIfPresent(T prop, Consumer action) { + Optional.ofNullable(prop).ifPresent(action); + } +} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/CRDApplier.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/CRDApplier.java new file mode 100644 index 0000000..6888498 --- /dev/null +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/CRDApplier.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.springboot.starter.crd; + +import static org.slf4j.LoggerFactory.getLogger; + +@FunctionalInterface +public interface CRDApplier { + + CRDApplier NOOP = () -> getLogger(CRDApplier.class).debug("Not searching for CRDs to apply"); + + void apply(); +} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/CRDTransformer.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/CRDTransformer.java new file mode 100644 index 0000000..309347e --- /dev/null +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/CRDTransformer.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.springboot.starter.crd; + +import java.util.List; +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface CRDTransformer extends UnaryOperator { + static CRDTransformer reduce(List transformers) { + return transformers.stream().reduce(t -> t, CRDTransformer::thenTransform); + } + + default CRDTransformer thenTransform(CRDTransformer after) { + return t -> after.apply(apply(t)); + } +} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/DefaultCRDApplier.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/DefaultCRDApplier.java new file mode 100644 index 0000000..a8f2060 --- /dev/null +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/crd/DefaultCRDApplier.java @@ -0,0 +1,80 @@ +package io.javaoperatorsdk.operator.springboot.starter.crd; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; + +import org.slf4j.Logger; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.springboot.starter.properties.OperatorConfigurationProperties; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +import static org.slf4j.LoggerFactory.getLogger; + +@Component +@ConditionalOnProperty(value = "javaoperatorsdk.crd.apply-on-startup", havingValue = "true") +public class DefaultCRDApplier implements CRDApplier { + + private static final Logger log = getLogger(DefaultCRDApplier.class); + private static final int CRD_READY_WAIT = 2000; + + private final KubernetesClient kubernetesClient; + private final CRDTransformer crdTransformer; + private final String crdSuffix; + private final String crdPath; + + public DefaultCRDApplier(KubernetesClient kubernetesClient, + OperatorConfigurationProperties configurationProperties, List transformers) { + this.kubernetesClient = kubernetesClient; + this.crdSuffix = configurationProperties.getCrd().getSuffix(); + this.crdPath = configurationProperties.getCrd().getPath(); + this.crdTransformer = CRDTransformer.reduce(transformers); + } + + @Override + public void apply() { + log.debug("Uploading CRDs with suffix {} under {}", crdSuffix, crdPath); + stream(findResources()).forEach(this::applyCrd); + } + + private Resource[] findResources() { + final var resourceResolver = new PathMatchingResourcePatternResolver(); + final var resourceLocationPattern = Paths.get(crdPath, '*' + crdSuffix).toString(); + try { + return resourceResolver.getResources(resourceLocationPattern); + } catch (IOException e) { + throw new RuntimeException( + "could not find CRD resources from the location pattern: " + resourceLocationPattern); + } + } + + private void applyCrd(Resource crdResource) { + try (var is = crdResource.getInputStream()) { + var crds = kubernetesClient.load(is).items().stream().map(crdTransformer).toList(); + kubernetesClient.resourceList(crds).serverSideApply(); + + Thread.sleep(CRD_READY_WAIT); // readiness is not applicable for CRD, just wait a little + + logUploaded(crds); + } catch (InterruptedException | IOException ex) { + throw new RuntimeException(ex); + } + } + + private void logUploaded(List crds) { + var crdNames = crds.stream() + .map(HasMetadata::getMetadata) + .map(ObjectMeta::getName) + .collect(joining(", ")); + log.info("Uploaded CRDs: {}", crdNames); + } + +} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/CrdProperties.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/CrdProperties.java new file mode 100644 index 0000000..7e4f3ab --- /dev/null +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/CrdProperties.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.springboot.starter.properties; + +public class CrdProperties { + + private boolean applyOnStartup; + /** + * path to the resource folder where CRDs are located + */ + private String path = "/META-INF/fabric8/"; + /** + * file suffix to filter out CRDs + */ + private String suffix = "-v1.yml"; + + public boolean isApplyOnStartup() { + return applyOnStartup; + } + + public void setApplyOnStartup(boolean applyOnStartup) { + this.applyOnStartup = applyOnStartup; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } +} diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/KubernetesClientProperties.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/KubernetesClientProperties.java similarity index 64% rename from starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/KubernetesClientProperties.java rename to starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/KubernetesClientProperties.java index 57dcf60..077e691 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/KubernetesClientProperties.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/KubernetesClientProperties.java @@ -1,5 +1,6 @@ -package io.javaoperatorsdk.operator.springboot.starter; +package io.javaoperatorsdk.operator.springboot.starter.properties; +import java.util.Objects; import java.util.Optional; public class KubernetesClientProperties { @@ -12,6 +13,7 @@ public class KubernetesClientProperties { private String masterUrl; private boolean trustSelfSignedCertificates = false; + public boolean isOpenshift() { return openshift; } @@ -21,12 +23,23 @@ public KubernetesClientProperties setOpenshift(boolean openshift) { return this; } + public boolean isTrustSelfSignedCertificates() { + return trustSelfSignedCertificates; + } + + public KubernetesClientProperties setTrustSelfSignedCertificates( + boolean trustSelfSignedCertificates) { + this.trustSelfSignedCertificates = trustSelfSignedCertificates; + return this; + } + public Optional getContext() { return Optional.ofNullable(context); } - public void setContext(String context) { + public KubernetesClientProperties setContext(String context) { this.context = context; + return this; } public Optional getUsername() { @@ -65,13 +78,22 @@ public KubernetesClientProperties setMasterUrl(String masterUrl) { return this; } - public boolean isTrustSelfSignedCertificates() { - return trustSelfSignedCertificates; + @Override + public boolean equals(Object object) { + if (!(object instanceof KubernetesClientProperties that)) + return false; + return isOpenshift() == that.isOpenshift() + && isTrustSelfSignedCertificates() == that.isTrustSelfSignedCertificates() + && Objects.equals(getContext(), that.getContext()) + && Objects.equals(getUsername(), that.getUsername()) + && Objects.equals(getPassword(), that.getPassword()) + && Objects.equals(getOauthToken(), that.getOauthToken()) + && Objects.equals(getMasterUrl(), that.getMasterUrl()); } - public KubernetesClientProperties setTrustSelfSignedCertificates( - boolean trustSelfSignedCertificates) { - this.trustSelfSignedCertificates = trustSelfSignedCertificates; - return this; + @Override + public int hashCode() { + return Objects.hash(isOpenshift(), getContext(), getUsername(), getPassword(), getOauthToken(), + getMasterUrl(), isTrustSelfSignedCertificates()); } } diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorConfigurationProperties.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/OperatorConfigurationProperties.java similarity index 65% rename from starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorConfigurationProperties.java rename to starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/OperatorConfigurationProperties.java index 18b4c3c..5b18a65 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/OperatorConfigurationProperties.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/OperatorConfigurationProperties.java @@ -1,8 +1,9 @@ -package io.javaoperatorsdk.operator.springboot.starter; +package io.javaoperatorsdk.operator.springboot.starter.properties; import java.time.Duration; import java.util.Collections; import java.util.Map; +import java.util.Objects; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -11,8 +12,9 @@ @ConfigurationProperties(prefix = "javaoperatorsdk") public class OperatorConfigurationProperties { - private CrdProperties crd = new CrdProperties(); - private KubernetesClientProperties client = new KubernetesClientProperties(); + CrdProperties crd = new CrdProperties(); + KubernetesClientProperties client = new KubernetesClientProperties(); + private Map reconcilers = Collections.emptyMap(); private boolean checkCrdAndValidateLocalModel = true; private int concurrentReconciliationThreads = @@ -24,6 +26,14 @@ public class OperatorConfigurationProperties { private Boolean stopOnInformerErrorDuringStartup; private Duration cacheSyncTimeout; + public CrdProperties getCrd() { + return crd; + } + + public void setCrd(CrdProperties crd) { + this.crd = crd; + } + public KubernetesClientProperties getClient() { return client; } @@ -40,7 +50,7 @@ public void setReconcilers(Map reconcilers) { this.reconcilers = reconcilers; } - public boolean getCheckCrdAndValidateLocalModel() { + public boolean isCheckCrdAndValidateLocalModel() { return checkCrdAndValidateLocalModel; } @@ -105,48 +115,33 @@ public void setCacheSyncTimeout(Duration cacheSyncTimeout) { this.cacheSyncTimeout = cacheSyncTimeout; } - public CrdProperties getCrd() { - return crd; - } - - public void setCrd(CrdProperties crd) { - this.crd = crd; - } - - public static class CrdProperties { - - private boolean applyOnStartup; - /** - * path to the resource folder where CRDs are located - */ - private String path = "/META-INF/fabric8/"; - /** - * file suffix to filter out CRDs - */ - private String suffix = "-v1.yml"; - - public boolean isApplyOnStartup() { - return applyOnStartup; - } - - public void setApplyOnStartup(boolean applyOnStartup) { - this.applyOnStartup = applyOnStartup; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public String getSuffix() { - return suffix; - } - - public void setSuffix(String suffix) { - this.suffix = suffix; - } + @Override + public boolean equals(Object object) { + if (!(object instanceof OperatorConfigurationProperties that)) + return false; + return isCheckCrdAndValidateLocalModel() == that.isCheckCrdAndValidateLocalModel() + && getConcurrentReconciliationThreads() == that.getConcurrentReconciliationThreads() + && Objects.equals(getCrd(), that.getCrd()) + && Objects.equals(getClient(), that.getClient()) + && Objects.equals(getReconcilers(), that.getReconcilers()) + && Objects.equals(getMinConcurrentReconciliationThreads(), + that.getMinConcurrentReconciliationThreads()) + && Objects.equals(getConcurrentWorkflowExecutorThreads(), + that.getConcurrentWorkflowExecutorThreads()) + && Objects.equals(getMinConcurrentWorkflowExecutorThreads(), + that.getMinConcurrentWorkflowExecutorThreads()) + && Objects.equals(getCloseClientOnStop(), that.getCloseClientOnStop()) + && Objects.equals(getStopOnInformerErrorDuringStartup(), + that.getStopOnInformerErrorDuringStartup()) + && Objects.equals(getCacheSyncTimeout(), that.getCacheSyncTimeout()); + } + + @Override + public int hashCode() { + return Objects.hash(getCrd(), getClient(), getReconcilers(), isCheckCrdAndValidateLocalModel(), + getConcurrentReconciliationThreads(), getMinConcurrentReconciliationThreads(), + getConcurrentWorkflowExecutorThreads(), + getMinConcurrentWorkflowExecutorThreads(), getCloseClientOnStop(), + getStopOnInformerErrorDuringStartup(), getCacheSyncTimeout()); } } diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerProperties.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/ReconcilerProperties.java similarity index 59% rename from starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerProperties.java rename to starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/ReconcilerProperties.java index 8987f8d..e49830b 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerProperties.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/ReconcilerProperties.java @@ -1,8 +1,11 @@ -package io.javaoperatorsdk.operator.springboot.starter; +package io.javaoperatorsdk.operator.springboot.starter.properties; + import java.time.Duration; +import java.util.Objects; import java.util.Set; + public class ReconcilerProperties { private String name; private String finalizerName; @@ -29,7 +32,7 @@ public void setFinalizerName(String finalizerName) { this.finalizerName = finalizerName; } - public Boolean isGenerationAware() { + public Boolean getGenerationAware() { return generationAware; } @@ -37,7 +40,7 @@ public void setGenerationAware(Boolean generationAware) { this.generationAware = generationAware; } - public Boolean isClusterScoped() { + public Boolean getClusterScoped() { return clusterScoped; } @@ -76,4 +79,24 @@ public Duration getReconciliationMaxInterval() { public void setReconciliationMaxInterval(Duration reconciliationMaxInterval) { this.reconciliationMaxInterval = reconciliationMaxInterval; } + + @Override + public boolean equals(Object object) { + if (!(object instanceof ReconcilerProperties that)) + return false; + return Objects.equals(getName(), that.getName()) + && Objects.equals(getFinalizerName(), that.getFinalizerName()) + && Objects.equals(getGenerationAware(), that.getGenerationAware()) + && Objects.equals(getClusterScoped(), that.getClusterScoped()) + && Objects.equals(getNamespaces(), that.getNamespaces()) + && Objects.equals(getRetry(), that.getRetry()) + && Objects.equals(getLabelSelector(), that.getLabelSelector()) + && Objects.equals(getReconciliationMaxInterval(), that.getReconciliationMaxInterval()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getFinalizerName(), getGenerationAware(), getClusterScoped(), + getNamespaces(), getRetry(), getLabelSelector(), getReconciliationMaxInterval()); + } } diff --git a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/RetryProperties.java b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/RetryProperties.java similarity index 58% rename from starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/RetryProperties.java rename to starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/RetryProperties.java index 8c75090..a76da4b 100644 --- a/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/RetryProperties.java +++ b/starter/src/main/java/io/javaoperatorsdk/operator/springboot/starter/properties/RetryProperties.java @@ -1,4 +1,7 @@ -package io.javaoperatorsdk.operator.springboot.starter; +package io.javaoperatorsdk.operator.springboot.starter.properties; + + +import java.util.Objects; public class RetryProperties { @@ -43,4 +46,19 @@ public RetryProperties setMaxInterval(Long maxInterval) { return this; } + @Override + public boolean equals(Object object) { + if (!(object instanceof RetryProperties that)) + return false; + return Objects.equals(getMaxAttempts(), that.getMaxAttempts()) + && Objects.equals(getInitialInterval(), that.getInitialInterval()) + && Objects.equals(getIntervalMultiplier(), that.getIntervalMultiplier()) + && Objects.equals(getMaxInterval(), that.getMaxInterval()); + } + + @Override + public int hashCode() { + return Objects.hash(getMaxAttempts(), getInitialInterval(), getIntervalMultiplier(), + getMaxInterval()); + } } diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationIntegrationTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationIntegrationTest.java index ca6fba2..9a564df 100644 --- a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationIntegrationTest.java +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationIntegrationTest.java @@ -10,7 +10,7 @@ import io.fabric8.kubernetes.api.model.ListOptionsBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.jenvtest.junit.EnableKubeAPIServer; -import io.javaoperatorsdk.operator.springboot.starter.CRDApplier.CRDTransformer; +import io.javaoperatorsdk.operator.springboot.starter.crd.CRDTransformer; import static org.assertj.core.api.Assertions.assertThat; @@ -42,22 +42,21 @@ void crdsUploadedAndTransformersApplied() { @TestConfiguration static class TestConfig { + private static HasMetadata addLabel(HasMetadata crd, String k, String v) { + crd.getMetadata().getLabels().put(k, v); + return crd; + } + @Bean public CRDTransformer transformerOne() { return crd -> addLabel(crd, "Glory", "Glory"); } - @Bean public CRDTransformer transformerTwo() { return crd -> addLabel(crd, "Man", "United"); } - private static HasMetadata addLabel(HasMetadata crd, String k, String v) { - crd.getMetadata().getLabels().put(k, v); - return crd; - } - } } diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationTest.java index 30e57a9..f06ce38 100644 --- a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationTest.java +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/AutoConfigurationTest.java @@ -11,6 +11,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.lang.NonNull; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; @@ -22,6 +23,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.springboot.starter.properties.OperatorConfigurationProperties; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.atIndex; @@ -50,19 +52,16 @@ public class AutoConfigurationTest { @Autowired private List> reconcilers; - @Autowired - private Consumer compositeConfigurationServiceOverrider; - @MockitoBean private Cloner cloner; @Test public void loadsKubernetesClientPropertiesProperly() { final var operatorProperties = config.getClient(); - assertEquals("user", operatorProperties.getUsername().get()); - assertEquals("password", operatorProperties.getPassword().get()); - assertEquals("token", operatorProperties.getOauthToken().get()); - assertEquals("http://master.url", operatorProperties.getMasterUrl().get()); + assertEquals("user", operatorProperties.getUsername().orElseThrow()); + assertEquals("password", operatorProperties.getPassword().orElseThrow()); + assertEquals("token", operatorProperties.getOauthToken().orElseThrow()); + assertEquals("http://master.url", operatorProperties.getMasterUrl().orElseThrow()); } @Test @@ -78,13 +77,12 @@ public void loadsRetryPropertiesProperly() { @Test public void beansCreated() { assertNotNull(kubernetesClient); - assertNotNull(compositeConfigurationServiceOverrider); } @Test public void reconcilersAreDiscovered() { assertEquals(1, reconcilers.size()); - assertTrue(reconcilers.get(0) instanceof TestReconciler); + assertInstanceOf(TestReconciler.class, reconcilers.get(0)); } @Test @@ -120,7 +118,7 @@ static class TestConfig { public BeanPostProcessor operatorPostProcessor() { return new BeanPostProcessor() { @Override - public Object postProcessAfterInitialization(Object bean, String beanName) + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { if (bean instanceof Operator operator) { doNothing().when(operator).start(); diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplierTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplierTest.java index 3feb998..ef11541 100644 --- a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplierTest.java +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CRDApplierTest.java @@ -11,9 +11,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; -import io.javaoperatorsdk.operator.springboot.starter.CRDApplier.CRDTransformer; -import io.javaoperatorsdk.operator.springboot.starter.CRDApplier.DefaultCRDApplier; +import io.javaoperatorsdk.operator.springboot.starter.crd.CRDApplier; +import io.javaoperatorsdk.operator.springboot.starter.crd.CRDTransformer; +import io.javaoperatorsdk.operator.springboot.starter.crd.DefaultCRDApplier; import io.javaoperatorsdk.operator.springboot.starter.model.TestResource; +import io.javaoperatorsdk.operator.springboot.starter.properties.CrdProperties; +import io.javaoperatorsdk.operator.springboot.starter.properties.OperatorConfigurationProperties; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -32,10 +35,15 @@ class CRDApplierTest { @Mock private NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable loadedResource; private String crdSuffix = "-v2.yml"; - private String crdPath = "/META-INF/fabric8/"; + private final String crdPath = "/META-INF/fabric8/"; private CRDApplier applier() { - return new DefaultCRDApplier(kubernetesClient, crdTransformers, crdPath, crdSuffix); + var props = new OperatorConfigurationProperties(); + var crd = new CrdProperties(); + crd.setPath(crdPath); + crd.setSuffix(crdSuffix); + props.setCrd(crd); + return new DefaultCRDApplier(kubernetesClient, props, crdTransformers); } @Test diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CrdApplierConfigurationTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CrdApplierConfigurationTest.java index e247b19..23f680c 100644 --- a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CrdApplierConfigurationTest.java +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/CrdApplierConfigurationTest.java @@ -4,6 +4,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.springboot.starter.crd.DefaultCRDApplier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -12,12 +13,13 @@ public class CrdApplierConfigurationTest { private static final ApplicationContextRunner runner = new ApplicationContextRunner() .withUserConfiguration(OperatorAutoConfiguration.class) + .withBean(DefaultCRDApplier.class) .withBean(Operator.class, () -> mock(Operator.class)); @Test void shouldNotCreateByDefault() { runner.run(ctx -> assertThat(ctx) - .doesNotHaveBean("crdApplier") + .doesNotHaveBean("defaultCRDApplier") .hasBean("disabledCrdApplier")); } @@ -25,7 +27,7 @@ void shouldNotCreateByDefault() { void shouldNotCreateWhenDisabled() { runner.withPropertyValues("javaoperatorsdk.crd.apply-on-startup=false") .run(ctx -> assertThat(ctx) - .doesNotHaveBean("crdApplier") + .doesNotHaveBean("defaultCRDApplier") .hasBean("disabledCrdApplier")); } @@ -33,7 +35,7 @@ void shouldNotCreateWhenDisabled() { void shouldCreateWhenEnabled() { runner.withPropertyValues("javaoperatorsdk.crd.apply-on-startup=true") .run(ctx -> assertThat(ctx) - .hasBean("crdApplier") + .hasBean("defaultCRDApplier") .doesNotHaveBean("disabledCrdApplier")); } } diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarterTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarterTest.java index b79b007..25d8b6b 100644 --- a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarterTest.java +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/OperatorStarterTest.java @@ -13,11 +13,7 @@ import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.RegisteredController; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class OperatorStarterTest { @@ -25,7 +21,7 @@ public class OperatorStarterTest { @Mock private Operator operator; @Mock - private CRDApplier CRDApplier; + private io.javaoperatorsdk.operator.springboot.starter.crd.CRDApplier CRDApplier; @Mock private ApplicationReadyEvent event; @Mock diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerRegistrationUtilTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerRegistrationUtilTest.java new file mode 100644 index 0000000..4f9d5cb --- /dev/null +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/ReconcilerRegistrationUtilTest.java @@ -0,0 +1,131 @@ +package io.javaoperatorsdk.operator.springboot.starter; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringJUnitConfig +class ReconcilerRegistrationUtilTest { + + Map> configurators = Map.of( + "SingleEntry", List.of(() -> "SingleEntry"), + "SecondEntry", List.of(() -> "SecondEntry"), + "DoubleEntry", List.of(() -> "DoubleEntry", () -> "DoubleEntry")); + + @Test + void testFilterConfiguratorsNoWorkflow() { + Reconciler test = (resource, context) -> null; + + List result = assertDoesNotThrow( + () -> ReconcilerRegistrationUtil.filterConfigurators(test, configurators)); + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void testFilterConfiguratorsWithEmptyWorkflow() { + Reconciler test = new TestEmptyReconciler(); + + List result = assertDoesNotThrow( + () -> ReconcilerRegistrationUtil.filterConfigurators(test, configurators)); + assertThat(result).isNotNull().isEmpty(); + } + + @Test + void testFilterConfiguratorsWithSingleEntryWorkflow() { + Reconciler test = new TestSingleEntryReconciler(); + + List result = assertDoesNotThrow( + () -> ReconcilerRegistrationUtil.filterConfigurators(test, configurators)); + assertThat(result) + .isNotNull() + .hasSize(1) + .element(0) + .matches(e -> "SingleEntry".equals(e.getName())); + } + + @Test + void testFilterConfiguratorsWithSecondEntryWorkflow() { + Reconciler test = new TestSecondEntryReconciler(); + + List result = assertDoesNotThrow( + () -> ReconcilerRegistrationUtil.filterConfigurators(test, configurators)); + assertThat(result) + .isNotNull() + .hasSize(2) + .anyMatch(e -> "SingleEntry".equals(e.getName())) + .anyMatch(e -> "SecondEntry".equals(e.getName())); + } + + @Test + void testFilterConfiguratorsWithDoubleEntryWorkflow() { + Reconciler test = new TestDoubleEntryReconciler(); + + Throwable th = assertThrows(IllegalStateException.class, + () -> ReconcilerRegistrationUtil.filterConfigurators(test, configurators)); + assertThat(th) + .isNotNull() + .hasMessageContaining("more than one config for Dependent Resource") + .hasMessageContaining("DoubleEntry"); + } + + @Workflow(dependents = {}) + private static class TestEmptyReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(HasMetadata resource, + Context context) { + return null; + } + } + + @Workflow(dependents = { + @Dependent(name = "SingleEntry", type = DependentResource.class) + }) + private static class TestSingleEntryReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(HasMetadata resource, + Context context) { + return null; + } + } + + @Workflow(dependents = { + @Dependent(name = "SingleEntry", type = DependentResource.class), + @Dependent(name = "SecondEntry", type = DependentResource.class) + }) + private static class TestSecondEntryReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(HasMetadata resource, + Context context) { + return null; + } + } + + @Workflow(dependents = { + @Dependent(name = "DoubleEntry", type = DependentResource.class) + }) + private static class TestDoubleEntryReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(HasMetadata resource, + Context context) { + return null; + } + } +} diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/ConfigMapDRConfigurator.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/ConfigMapDRConfigurator.java new file mode 100644 index 0000000..551a9e6 --- /dev/null +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/ConfigMapDRConfigurator.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.springboot.starter.dependentresourceconfigurator; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.springboot.starter.DependentResourceConfigurator; + +public class ConfigMapDRConfigurator extends KubernetesDependentResourceConfig + implements DependentResourceConfigurator { + + public ConfigMapDRConfigurator() { + super(null, true, InformerConfiguration.builder(ConfigMap.class).build()); + } + + @Override + public String getName() { + return DependentResourceConfigMap.NAME; + } + + public void getsCalled() {} +} diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceConfigMap.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceConfigMap.java new file mode 100644 index 0000000..7d3a434 --- /dev/null +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceConfigMap.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.springboot.starter.dependentresourceconfigurator; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.springboot.starter.model.TestResource; + +public class DependentResourceConfigMap + extends CRUDKubernetesDependentResource { + + public static final String NAME = "configMapDR"; + + public DependentResourceConfigMap() { + super(ConfigMap.class, NAME); + } + + @Override + public void configureWith(KubernetesDependentResourceConfig config) { + if (config instanceof ConfigMapDRConfigurator drc) { + drc.getsCalled(); + } + } +} diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceConfiguratorIntegrationTest.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceConfiguratorIntegrationTest.java new file mode 100644 index 0000000..9a89dd4 --- /dev/null +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceConfiguratorIntegrationTest.java @@ -0,0 +1,55 @@ +package io.javaoperatorsdk.operator.springboot.starter.dependentresourceconfigurator; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.javaoperatorsdk.jenvtest.junit.EnableKubeAPIServer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.springboot.starter.crd.CRDTransformer; +import io.javaoperatorsdk.operator.springboot.starter.model.TestResource; + +import static org.mockito.Mockito.times; + +@EnableKubeAPIServer(updateKubeConfigFile = true) +@SpringBootTest(properties = { + "javaoperatorsdk.crd.apply-on-startup = true", + "javaoperatorsdk.crd.path = META-INF/fabric8/deeper" +}) +@SpringJUnitConfig +public class DependentResourceConfiguratorIntegrationTest { + + @MockitoSpyBean + ConfigMapDRConfigurator dependentResourceConfigurator; + + @Test + void testLoadingCustomDependentResourceConfigurator() { + Mockito.verify(dependentResourceConfigurator, times(1)).getsCalled(); + } + + @TestConfiguration + public static class TestConfig { + + @Bean + public CRDTransformer transformer() { + return crd -> { + crd.getMetadata().getLabels().put("DRtest", "CRD"); + return crd; + }; + } + + @Bean + public ConfigMapDRConfigurator drConfigurator() { + return new ConfigMapDRConfigurator(); + } + + @Bean + public Reconciler drReconciler() { + return new DependentResourceReconciler(); + } + } +} diff --git a/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceReconciler.java b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceReconciler.java new file mode 100644 index 0000000..495fb5b --- /dev/null +++ b/starter/src/test/java/io/javaoperatorsdk/operator/springboot/starter/dependentresourceconfigurator/DependentResourceReconciler.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.springboot.starter.dependentresourceconfigurator; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.springboot.starter.model.TestResource; + +@Workflow(dependents = @Dependent(type = DependentResourceConfigMap.class, + name = DependentResourceConfigMap.NAME)) +@ControllerConfiguration +public class DependentResourceReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(TestResource resource, + Context context) { + return UpdateControl.noUpdate(); + } +} diff --git a/starter/src/test/resources/META-INF/fabric8/deeper/test-crd-v1.yml b/starter/src/test/resources/META-INF/fabric8/deeper/test-crd-v1.yml index dc4b837..5138b90 100644 --- a/starter/src/test/resources/META-INF/fabric8/deeper/test-crd-v1.yml +++ b/starter/src/test/resources/META-INF/fabric8/deeper/test-crd-v1.yml @@ -37,7 +37,7 @@ spec: served: true storage: false subresources: - status: {} + status: { } - name: v2 schema: openAPIV3Schema: @@ -67,7 +67,7 @@ spec: served: true storage: true subresources: - status: {} + status: { } --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition @@ -108,4 +108,4 @@ spec: served: true storage: true subresources: - status: {} \ No newline at end of file + status: { } \ No newline at end of file diff --git a/starter/src/test/resources/META-INF/fabric8/test-crd-v2.yml b/starter/src/test/resources/META-INF/fabric8/test-crd-v2.yml index 2e81227..cc7cedb 100644 --- a/starter/src/test/resources/META-INF/fabric8/test-crd-v2.yml +++ b/starter/src/test/resources/META-INF/fabric8/test-crd-v2.yml @@ -19,4 +19,4 @@ spec: served: true storage: false subresources: - status: {} \ No newline at end of file + status: { } \ No newline at end of file