From aa82130af51ed03a95a0c253ae4de9855e3a8eeb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 24 Dec 2021 15:14:41 +0100 Subject: [PATCH 0001/1319] feat: support for declarative management of dependent resources - Kubernetes-native resources are automatically handled without users having to deal with explicit event sources - Reconcilers and event sources can exchange information via the `EventSourceContext` - Declarative management is completely opt-in and can cohabit with "traditional" handling i.e. it's possible to have only part of the dependents being declared via annotations and others explicitly handled (though from a maintenance perspective it's better to stick to one approach or the other) --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- .../api/config/ControllerConfiguration.java | 83 ++------ .../ControllerConfigurationOverrider.java | 3 +- .../DefaultControllerConfiguration.java | 14 +- .../config/DefaultResourceConfiguration.java | 65 +++++++ .../operator/api/config/Dependent.java | 8 + .../api/config/DependentResource.java | 15 ++ .../api/config/ExecutorServiceManager.java | 3 + .../api/config/KubernetesDependent.java | 34 ++++ .../api/config/ResourceConfiguration.java | 78 ++++++++ .../operator/api/config/Utils.java | 6 + .../api/reconciler/AttributeHolder.java | 12 ++ .../operator/api/reconciler/Context.java | 9 +- .../api/reconciler/ContextInitializer.java | 7 + .../reconciler/ControllerConfiguration.java | 12 ++ .../api/reconciler/DefaultContext.java | 2 +- .../api/reconciler/EventSourceContext.java | 4 +- .../EventSourceContextInjector.java | 5 + .../api/reconciler/MapAttributeHolder.java | 28 +++ .../api/reconciler/dependent/Builder.java | 9 + .../api/reconciler/dependent/Cleaner.java | 9 + .../DependentResourceController.java | 87 +++++++++ .../DependentResourceControllerFactory.java | 14 ++ ...KubernetesDependentResourceController.java | 74 ++++++++ .../api/reconciler/dependent/Persister.java | 11 ++ .../api/reconciler/dependent/Updater.java | 10 + .../operator/processing/Controller.java | 52 +++-- .../dependent/DependentResourceManager.java | 140 ++++++++++++++ .../processing/event/EventSourceManager.java | 41 ++-- .../event/source/AbstractEventSource.java | 2 +- .../event/source/CachingEventSource.java | 8 +- .../event/source/EventSourceContextAware.java | 8 + .../ControllerResourceEventSource.java | 106 ++--------- .../informer/InformerConfiguration.java | 132 +++++++++++++ .../source/informer/InformerEventSource.java | 159 +++++----------- .../source/informer/InformerManager.java | 144 ++++++++++++++ .../informer/InformerResourceCache.java | 64 +++++++ .../source/informer/InformerWrapper.java | 84 ++++++++ .../informer/ManagedInformerEventSource.java | 43 +++++ .../event/source/informer/Mappers.java | 2 + .../operator/ControllerManagerTest.java | 9 +- .../operator/MockKubernetesClient.java | 52 +++++ .../operator/OperatorTest.java | 3 +- .../config/ControllerConfigurationTest.java | 3 + .../operator/processing/ControllerTest.java | 20 +- .../event/EventSourceManagerTest.java | 6 +- .../event/ReconciliationDispatcherTest.java | 5 +- .../source/CustomResourceSelectorTest.java | 3 + .../event/source/ResourceEventFilterTest.java | 19 +- .../ControllerResourceEventSourceTest.java | 15 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- .../runtime/AnnotationConfiguration.java | 65 ++++++- ...formerEventSourceTestCustomReconciler.java | 47 ++--- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- .../operator/sample/MySQLSchemaOperator.java | 3 +- .../sample/MySQLSchemaReconciler.java | 179 ++++++++---------- .../sample/SchemaDependentResource.java | 88 +++++++++ .../operator/sample/schema/SchemaService.java | 4 +- .../sample/MySQLSchemaOperatorE2E.java | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- .../sample/DeploymentDependentResource.java | 52 +++++ .../sample/ServiceDependentResource.java | 28 +++ .../sample/TomcatDependentResource.java | 41 ++++ .../operator/sample/TomcatOperator.java | 2 +- .../operator/sample/TomcatReconciler.java | 134 ++----------- .../operator/sample/WebappReconciler.java | 51 +++-- .../operator/sample/deployment.yaml | 5 - .../operator/sample/service.yaml | 5 - .../operator/sample/TomcatOperatorE2E.java | 2 +- sample-operators/webpage/pom.xml | 3 +- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 78 files changed, 1793 insertions(+), 664 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 9ca97c7186..73c8e7ed7d 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a615428790..a1804086d6 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index b85df49eba..d4739f8267 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -1,103 +1,36 @@ package io.javaoperatorsdk.operator.api.config; -import java.lang.reflect.ParameterizedType; import java.util.Collections; -import java.util.Set; +import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceControllerFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -public interface ControllerConfiguration { +@SuppressWarnings("rawtypes") +public interface ControllerConfiguration extends ResourceConfiguration { default String getName() { return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } - default String getResourceTypeName() { - return ReconcilerUtils.getResourceTypeName(getResourceClass()); - } - default String getFinalizer() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } - /** - * Retrieves the label selector that is used to filter which custom resources are actually watched - * by the associated controller. See - * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on - * syntax. - * - * @return the label selector filtering watched custom resources - */ - default String getLabelSelector() { - return null; - } - default boolean isGenerationAware() { return true; } - default Class getResourceClass() { - ParameterizedType type = (ParameterizedType) getClass().getGenericInterfaces()[0]; - return (Class) type.getActualTypeArguments()[0]; - } - String getAssociatedReconcilerClassName(); - default Set getNamespaces() { - return Collections.emptySet(); - } - - default boolean watchAllNamespaces() { - return allNamespacesWatched(getNamespaces()); - } - - static boolean allNamespacesWatched(Set namespaces) { - return namespaces == null || namespaces.isEmpty(); - } - - default boolean watchCurrentNamespace() { - return currentNamespaceWatched(getNamespaces()); - } - - static boolean currentNamespaceWatched(Set namespaces) { - return namespaces != null - && namespaces.size() == 1 - && namespaces.contains( - Constants.WATCH_CURRENT_NAMESPACE); - } - - /** - * Computes the effective namespaces based on the set specified by the user, in particular - * retrieves the current namespace from the client when the user specified that they wanted to - * watch the current namespace only. - * - * @return a Set of namespace names the associated controller will watch - */ - default Set getEffectiveNamespaces() { - var targetNamespaces = getNamespaces(); - if (watchCurrentNamespace()) { - final var parent = getConfigurationService(); - if (parent == null) { - throw new IllegalStateException( - "Parent ConfigurationService must be set before calling this method"); - } - targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); - } - return targetNamespaces; - } - default RetryConfiguration getRetryConfiguration() { return RetryConfiguration.DEFAULT; } - ConfigurationService getConfigurationService(); - - default void setConfigurationService(ConfigurationService service) {} - default boolean useFinalizer() { return !Constants.NO_FINALIZER .equals(getFinalizer()); @@ -114,4 +47,12 @@ default boolean useFinalizer() { default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } + + default List getDependentResources() { + return Collections.emptyList(); + } + + default DependentResourceControllerFactory dependentFactory() { + return new DependentResourceControllerFactory<>() {}; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index e8e2ef1162..fee1212deb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -86,7 +86,8 @@ public ControllerConfiguration build() { labelSelector, customResourcePredicate, original.getResourceClass(), - original.getConfigurationService()); + original.getConfigurationService(), + original.getDependentResources()); } public static ControllerConfigurationOverrider override( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 860152745b..bc10002ebd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.util.Collections; +import java.util.List; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -20,8 +21,10 @@ public class DefaultControllerConfiguration private final String labelSelector; private final ResourceEventFilter resourceEventFilter; private final Class resourceClass; + private final List dependents; private ConfigurationService service; + // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( String associatedControllerClassName, String name, @@ -33,7 +36,8 @@ public DefaultControllerConfiguration( String labelSelector, ResourceEventFilter resourceEventFilter, Class resourceClass, - ConfigurationService service) { + ConfigurationService service, + List dependents) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; @@ -52,6 +56,7 @@ public DefaultControllerConfiguration( resourceClass == null ? ControllerConfiguration.super.getResourceClass() : resourceClass; setConfigurationService(service); + this.dependents = dependents != null ? dependents : Collections.emptyList(); } @Override @@ -102,7 +107,7 @@ public ConfigurationService getConfigurationService() { @Override public void setConfigurationService(ConfigurationService service) { if (this.service != null) { - throw new RuntimeException("A ConfigurationService is already associated with '" + name + throw new IllegalStateException("A ConfigurationService is already associated with '" + name + "' ControllerConfiguration. Cannot change it once set!"); } this.service = service; @@ -122,4 +127,9 @@ public Class getResourceClass() { public ResourceEventFilter getEventFilter() { return resourceEventFilter; } + + @Override + public List getDependentResources() { + return dependents; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java new file mode 100644 index 0000000000..d7b5f76815 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class DefaultResourceConfiguration + implements ResourceConfiguration { + + private final String labelSelector; + private final Set namespaces; + private final boolean watchAllNamespaces; + private final Class resourceClass; + private ConfigurationService service; + + public DefaultResourceConfiguration(String labelSelector, Class resourceClass, + String... namespaces) { + this(labelSelector, resourceClass, + namespaces != null ? Set.of(namespaces) : Collections.emptySet()); + } + + public DefaultResourceConfiguration(String labelSelector, Class resourceClass, + Set namespaces) { + this.labelSelector = labelSelector; + this.resourceClass = resourceClass; + this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); + this.watchAllNamespaces = this.namespaces.isEmpty(); + } + + @Override + public String getResourceTypeName() { + return ResourceConfiguration.super.getResourceTypeName(); + } + + @Override + public String getLabelSelector() { + return labelSelector; + } + + @Override + public Set getNamespaces() { + return namespaces; + } + + @Override + public boolean watchAllNamespaces() { + return watchAllNamespaces; + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public Class getResourceClass() { + return resourceClass; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + this.service = service; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java new file mode 100644 index 0000000000..ed013fafb0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.api.config; + +public @interface Dependent { + + Class resourceType(); + + Class type(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java new file mode 100644 index 0000000000..f4d5df89fb --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public interface DependentResource { + default EventSource initEventSource(EventSourceContext

context) { + throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); + }; + + default Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index dfe64d97d0..254a539ac3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -36,6 +36,9 @@ public static void init(ConfigurationService configuration) { log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}", configuration.getExecutorService().getClass(), configuration.getTerminationTimeoutSeconds()); + log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}", + configuration.getExecutorService().getClass(), + configuration.getTerminationTimeoutSeconds()); } else { log.debug("Already started, reusing already setup instance!"); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java new file mode 100644 index 0000000000..587fd58f85 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; + +@Target({ElementType.TYPE}) +public @interface KubernetesDependent { + + boolean OWNED_DEFAULT = true; + boolean SKIP_UPDATE_DEFAULT = true; + + boolean owned() default OWNED_DEFAULT; + + boolean skipUpdateIfUnchanged() default SKIP_UPDATE_DEFAULT; + + /** + * Specified which namespaces this Controller monitors for custom resources events. If no + * namespace is specified then the controller will monitor all namespaces by default. + * + * @return the list of namespaces this controller monitors + */ + String[] namespaces() default {}; + + /** + * Optional label selector used to identify the set of custom resources the controller will acc + * upon. The label selector can be made of multiple comma separated requirements that acts as a + * logical AND operator. + * + * @return the label selector + */ + String labelSelector() default EMPTY_STRING; +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java new file mode 100644 index 0000000000..1e1d58b917 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -0,0 +1,78 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Constants; + +public interface ResourceConfiguration { + + default String getResourceTypeName() { + return ReconcilerUtils.getResourceTypeName(getResourceClass()); + } + + /** + * Retrieves the label selector that is used to filter which resources are actually watched by the + * associated event source. See + * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on + * syntax. + * + * @return the label selector filtering watched resources + */ + default String getLabelSelector() { + return null; + } + + @SuppressWarnings("unchecked") + default Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } + + default Set getNamespaces() { + return Collections.emptySet(); + } + + default boolean watchAllNamespaces() { + return allNamespacesWatched(getNamespaces()); + } + + static boolean allNamespacesWatched(Set namespaces) { + return namespaces == null || namespaces.isEmpty(); + } + + default boolean watchCurrentNamespace() { + return currentNamespaceWatched(getNamespaces()); + } + + static boolean currentNamespaceWatched(Set namespaces) { + return namespaces != null + && namespaces.size() == 1 + && namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE); + } + + /** + * Computes the effective namespaces based on the set specified by the user, in particular + * retrieves the current namespace from the client when the user specified that they wanted to + * watch the current namespace only. + * + * @return a Set of namespace names the associated controller will watch + */ + default Set getEffectiveNamespaces() { + var targetNamespaces = getNamespaces(); + if (watchCurrentNamespace()) { + final var parent = getConfigurationService(); + if (parent == null) { + throw new IllegalStateException( + "Parent ConfigurationService must be set before calling this method"); + } + targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); + } + return targetNamespaces; + } + + ConfigurationService getConfigurationService(); + + void setConfigurationService(ConfigurationService service); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index b36c0468cd..3944cd3ecc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; @@ -67,4 +68,9 @@ public static boolean shouldCheckCRDAndValidateLocalModel() { public static boolean debugThreadPool() { return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); } + + public static Class getFirstTypeArgumentFromInterface(Class clazz) { + ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; + return (Class) type.getActualTypeArguments()[0]; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java new file mode 100644 index 0000000000..8fee3c2557 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; + +public interface AttributeHolder { + + Optional get(Object key, Class expectedType); + + T getMandatory(Object key, Class expectedType); + + Optional put(Object key, Object value); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index d697325219..6870fef80e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,7 +2,7 @@ import java.util.Optional; -public interface Context { +public interface Context extends AttributeHolder { Optional getRetryInfo(); @@ -11,4 +11,11 @@ default Optional getSecondaryResource(Class expectedType) { } Optional getSecondaryResource(Class expectedType, String eventSourceName); + + @Override + default T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java new file mode 100644 index 0000000000..e076723d0c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface ContextInitializer

{ + void initContext(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index fad4bc8573..5870820f0f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -5,6 +5,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @@ -56,4 +58,14 @@ */ @SuppressWarnings("rawtypes") Class[] eventFilters() default {}; + + /** + * Optional list of classes providing {@link DependentResourceController} implementations + * encapsulating logic to handle the associated + * {@link io.javaoperatorsdk.operator.processing.Controller}'s reconciliation of dependent + * resources + * + * @return the list of {@link DependentResourceController} implementations + */ + Dependent[] dependents() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 3d924c2753..c2553a5d57 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.Controller; -public class DefaultContext

implements Context { +public class DefaultContext

extends MapAttributeHolder implements Context { private final RetryInfo retryInfo; private final Controller

controller; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index 0a93d33d40..026af88923 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -11,7 +11,7 @@ * * @param

the type associated with the primary resource that is handled by your reconciler */ -public class EventSourceContext

{ +public class EventSourceContext

extends MapAttributeHolder { private final ResourceCache

primaryCache; private final ConfigurationService configurationService; @@ -49,7 +49,7 @@ public ConfigurationService getConfigurationService() { /** * Provides access to the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. - * + * * @return the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java new file mode 100644 index 0000000000..af57fe1b32 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +public interface EventSourceContextInjector { + void injectInto(EventSourceContext context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java new file mode 100644 index 0000000000..aee5ee7e54 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class MapAttributeHolder { + + private final ConcurrentHashMap attributes = new ConcurrentHashMap(); + + public Optional get(Object key, Class expectedType) { + return Optional.ofNullable(attributes.get(key)) + .filter(expectedType::isInstance) + .map(expectedType::cast); + } + + public Optional put(Object key, Object value) { + if (value == null) { + return Optional.ofNullable(attributes.remove(key)); + } + return Optional.ofNullable(attributes.put(key, value)); + } + + public T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java new file mode 100644 index 0000000000..4dfaf91e64 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@FunctionalInterface +public interface Builder { + R buildFor(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java new file mode 100644 index 0000000000..a6a3d691be --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface Cleaner { + + void delete(R fetched, P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java new file mode 100644 index 0000000000..043aec2f53 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java @@ -0,0 +1,87 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public class DependentResourceController + implements DependentResource, Builder, Updater, Persister, + Cleaner { + + private final Builder builder; + private final Updater updater; + private final Cleaner cleaner; + private final Persister persister; + private final DependentResource delegate; + + @SuppressWarnings("unchecked") + public DependentResourceController(DependentResource delegate) { + this.delegate = delegate; + builder = (delegate instanceof Builder) ? (Builder) delegate : null; + updater = (delegate instanceof Updater) ? (Updater) delegate : null; + cleaner = (delegate instanceof Cleaner) ? (Cleaner) delegate : null; + persister = initPersister(delegate); + } + + @SuppressWarnings("unchecked") + protected Persister initPersister(DependentResource delegate) { + if (delegate instanceof Persister) { + return (Persister) delegate; + } else { + throw new IllegalArgumentException( + "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); + } + } + + public String descriptionFor(R resource) { + return resource.toString(); + } + + @Override + public R buildFor(P primary, Context context) { + return builder.buildFor(primary, context); + } + + @Override + public R update(R fetched, P primary, Context context) { + return updater.update(fetched, primary, context); + } + + @Override + public void delete(R fetched, P primary, Context context) { + cleaner.delete(fetched, primary, context); + } + + public Class getResourceType() { + return delegate.resourceType(); + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + return delegate.initEventSource(context); + } + + public boolean creatable() { + return builder != null; + } + + public boolean updatable() { + return updater != null; + } + + public boolean deletable() { + return cleaner != null; + } + + @Override + public void createOrReplace(R dependentResource, Context context) { + persister.createOrReplace(dependentResource, context); + } + + @Override + public R getFor(P primary, Context context) { + return persister.getFor(primary, context); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java new file mode 100644 index 0000000000..f814cd7a1a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.DependentResource; + +public interface DependentResourceControllerFactory

{ + + default DependentResourceController from(DependentResource dependent) { + // todo: this needs to be cleaned-up / redesigned + return dependent instanceof DependentResourceController + ? (DependentResourceController) dependent + : new DependentResourceController<>(dependent); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java new file mode 100644 index 0000000000..da11c4f938 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java @@ -0,0 +1,74 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +public class KubernetesDependentResourceController + extends DependentResourceController { + private final InformerConfiguration configuration; + private final boolean owned; + private KubernetesClient client; + private InformerEventSource informer; + + + public KubernetesDependentResourceController(DependentResource delegate, + InformerConfiguration configuration, boolean owned) { + super(delegate); + // todo: check if we can validate that types actually match properly + final var associatedPrimaries = + (delegate instanceof PrimaryResourcesRetriever) + ? (PrimaryResourcesRetriever) delegate + : configuration.getPrimaryResourcesRetriever(); + final var associatedSecondary = + (delegate instanceof AssociatedSecondaryResourceIdentifier) + ? (AssociatedSecondaryResourceIdentifier

) delegate + : configuration.getAssociatedResourceIdentifier(); + + this.configuration = InformerConfiguration.from(configuration) + .withPrimaryResourcesRetriever(associatedPrimaries) + .withAssociatedSecondaryResourceIdentifier(associatedSecondary) + .build(); + this.owned = owned; + } + + @Override + protected Persister initPersister(DependentResource delegate) { + return (delegate instanceof Persister) ? (Persister) delegate : this; + } + + @Override + public String descriptionFor(R resource) { + return String.format("'%s' %s dependent in namespace %s", resource.getMetadata().getName(), + resource.getFullResourceName(), + resource.getMetadata().getNamespace()); + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + this.client = context.getClient(); + informer = new InformerEventSource<>(configuration, context); + return informer; + } + + @Override + public void createOrReplace(R dependentResource, Context context) { + client.resource(dependentResource).createOrReplace(); + } + + @Override + public R getFor(P primary, Context context) { + return informer.getAssociated(primary).orElse(null); + } + + public boolean owned() { + return owned; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java new file mode 100644 index 0000000000..ab34372917 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface Persister { + + void createOrReplace(R dependentResource, Context context); + + R getFor(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java new file mode 100644 index 0000000000..c92f3c5ced --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@FunctionalInterface +public interface Updater { + + R update(R fetched, P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 5e93b0d7af..e0b9efae77 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -1,8 +1,12 @@ package io.javaoperatorsdk.operator.processing; +import java.util.LinkedList; import java.util.List; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; @@ -25,16 +29,23 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.dependent.DependentResourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +@SuppressWarnings({"rawtypes", "unchecked"}) public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { + + private static final Logger log = LoggerFactory.getLogger(Controller.class); + private final Reconciler reconciler; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; - private EventSourceManager eventSourceManager; - private volatile ConfigurationService configurationService; + private final EventSourceManager eventSourceManager; + private final DependentResourceManager dependents; + + private ConfigurationService configurationService; public Controller(Reconciler reconciler, ControllerConfiguration configuration, @@ -42,10 +53,15 @@ public Controller(Reconciler reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + + eventSourceManager = new EventSourceManager<>(this); + dependents = new DependentResourceManager<>(this); } @Override public DeleteControl cleanup(R resource, Context context) { + dependents.cleanup(resource, context); + return metrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -72,6 +88,8 @@ public DeleteControl execute() { @Override public UpdateControl reconcile(R resource, Context context) { + dependents.reconcile(resource, context); + return metrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -103,6 +121,7 @@ public UpdateControl execute() { }); } + private Metrics metrics() { final var metrics = configurationService().getMetrics(); return metrics != null ? metrics : Metrics.NOOP; @@ -110,7 +129,14 @@ private Metrics metrics() { @Override public List prepareEventSources(EventSourceContext context) { - throw new UnsupportedOperationException("This method should never be called directly"); + final var dependentSources = dependents.prepareEventSources(context); + List sources = new LinkedList<>(dependentSources); + + // add manually defined event sources + if (reconciler instanceof EventSourceInitializer) { + sources.addAll(((EventSourceInitializer) reconciler).prepareEventSources(context)); + } + return sources; } @Override @@ -165,6 +191,8 @@ public void start() throws OperatorException { final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; + log.info("Starting '{}' controller for reconciler: {}, resource: {}", controllerName, + reconciler.getClass().getCanonicalName(), resClass.getCanonicalName()); try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config @@ -180,21 +208,23 @@ public void start() throws OperatorException { CustomResourceUtils.assertCustomResource(resClass, crd); } - eventSourceManager = new EventSourceManager<>(this); - if (reconciler instanceof EventSourceInitializer) { - ((EventSourceInitializer) reconciler) - .prepareEventSources(new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), - configurationService(), kubernetesClient)) - .forEach(eventSourceManager::registerEventSource); - } if (failOnMissingCurrentNS()) { throw new OperatorException( "Controller '" + controllerName + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } + + final var context = new EventSourceContext<>( + eventSourceManager.getControllerResourceEventSource().getResourceCache(), + configurationService(), kubernetesClient); + + dependents.injectInto(context); + prepareEventSources(context).forEach(eventSourceManager::registerEventSource); + eventSourceManager.start(); + + log.info("'{}' controller started, pending event sources initialization", controllerName); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java new file mode 100644 index 0000000000..655113d251 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -0,0 +1,140 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class DependentResourceManager implements EventSourceInitializer, + EventSourceContextInjector, Reconciler { + + private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); + + private final Reconciler reconciler; + private final ControllerConfiguration configuration; + private List dependents; + + + public DependentResourceManager(Controller controller) { + this.reconciler = controller.getReconciler(); + this.configuration = controller.getConfiguration(); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + final List configured = configuration.getDependentResources(); + dependents = new ArrayList<>(configured.size()); + + List sources = new ArrayList<>(configured.size() + 5); + configured.forEach(dependent -> { + dependents.add(configuration.dependentFactory().from(dependent)); + sources.add(dependent.initEventSource(context)); + }); + + return sources; + } + + @Override + public void injectInto(EventSourceContext context) { + if (reconciler instanceof EventSourceContextInjector) { + EventSourceContextInjector injector = (EventSourceContextInjector) reconciler; + injector.injectInto(context); + } + } + + @Override + public UpdateControl reconcile(R resource, Context context) { + initContextIfNeeded(resource, context); + + dependents.stream() + .filter(dependent -> dependent.creatable() || dependent.updatable()) + .forEach(dependent -> { + var dependentResource = dependent.getFor(resource, context); + if (dependent.creatable() && dependentResource == null) { + // we need to create the dependent + dependentResource = dependent.buildFor(resource, context); + createOrReplaceDependent(resource, context, dependent, dependentResource, "Creating"); + } else if (dependent.updatable()) { + dependentResource = dependent.update(dependentResource, resource, context); + createOrReplaceDependent(resource, context, dependent, dependentResource, "Updating"); + } else { + logOperationInfo(resource, dependent, dependentResource, "Ignoring"); + } + }); + + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(R resource, Context context) { + initContextIfNeeded(resource, context); + + dependents.stream() + .filter(DependentResourceController::deletable) + .forEach(dependent -> { + var dependentResource = dependent.getFor(resource, context); + if (dependentResource != null) { + dependent.delete(dependentResource, resource, context); + logOperationInfo(resource, dependent, dependentResource, "Deleting"); + } else { + log.info("Ignoring already deleted {} for '{}' {}", + dependent.getResourceType().getName(), + resource.getMetadata().getName(), + configuration.getResourceTypeName()); + } + }); + + return Reconciler.super.cleanup(resource, context); + } + + private void createOrReplaceDependent(R primaryResource, + Context context, DependentResourceController dependentController, + Object dependentResource, String operationDescription) { + // add owner reference if needed + if (dependentResource instanceof HasMetadata + && ((KubernetesDependentResourceController) dependentController).owned()) { + ((HasMetadata) dependentResource).addOwnerReference(primaryResource); + } + + logOperationInfo(primaryResource, dependentController, dependentResource, operationDescription); + + // commit the changes + // todo: add metrics timing for dependent resource + dependentController.createOrReplace(dependentResource, context); + } + + private void logOperationInfo(R resource, DependentResourceController dependent, + Object dependentResource, String operationDescription) { + if (log.isInfoEnabled()) { + log.info("{} {} for '{}' {}", operationDescription, + dependent.descriptionFor(dependentResource), + resource.getMetadata().getName(), + configuration.getResourceTypeName()); + } + } + + private void initContextIfNeeded(R resource, Context context) { + if (reconciler instanceof ContextInitializer) { + final var initializer = (ContextInitializer) reconciler; + initializer.initContext(resource, context); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 55f42cd438..6e0b218d55 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -48,6 +48,9 @@ public EventSourceManager(Controller controller) { // controller event source needs to be available before we create the event processor final var controllerEventSource = eventSources.initControllerEventSource(controller); this.eventProcessor = new EventProcessor<>(this); + + // sources need to be registered after the event processor is created since it's set on the + // event source registerEventSource(eventSources.retryEventSource()); registerEventSource(controllerEventSource); } @@ -66,12 +69,15 @@ public EventSourceManager(Controller controller) { public void start() { lock.lock(); try { - log.debug("Starting event sources."); for (var eventSource : eventSources) { try { + logEventSourceEvent(eventSource, "Starting"); eventSource.start(); + logEventSourceEvent(eventSource, "Started"); + } catch (MissingCRDException e) { + throw e; // leave untouched } catch (Exception e) { - log.warn("Error starting {} -> {}", eventSource, e); + throw new OperatorException("Couldn't start source " + eventSource.name(), e); } } eventProcessor.start(); @@ -80,16 +86,30 @@ public void start() { } } + @SuppressWarnings("rawtypes") + private void logEventSourceEvent(EventSource eventSource, String event) { + if (log.isDebugEnabled()) { + if (eventSource instanceof ResourceEventSource) { + ResourceEventSource source = (ResourceEventSource) eventSource; + log.debug("{} event source {} for {}", event, eventSource.name(), + source.getResourceClass()); + } else { + log.debug("{} event source {}", event, eventSource.name()); + } + } + } + @Override public void stop() { lock.lock(); try { - log.debug("Closing event sources."); for (var eventSource : eventSources) { try { + logEventSourceEvent(eventSource, "Stopping"); eventSource.stop(); + logEventSourceEvent(eventSource, "Stopped"); } catch (Exception e) { - log.warn("Error closing {} -> {}", eventSource, e); + log.warn("Error closing {} -> {}", eventSource.name(), e); } } eventSources.clear(); @@ -106,13 +126,10 @@ public final void registerEventSource(EventSource eventSource) try { eventSources.add(eventSource); eventSource.setEventHandler(eventProcessor); - } catch (Throwable e) { - if (e instanceof IllegalStateException || e instanceof MissingCRDException) { - // leave untouched - throw e; - } - throw new OperatorException( - "Couldn't register event source: " + eventSource.getClass().getName(), e); + } catch (IllegalStateException | MissingCRDException e) { + throw e; // leave untouched + } catch (Exception e) { + throw new OperatorException("Couldn't register event source: " + eventSource.name(), e); } finally { lock.unlock(); } @@ -219,7 +236,7 @@ public void add(EventSource eventSource) { sources.computeIfAbsent(keyFor(eventSource), k -> new ArrayList<>()).add(eventSource); } - private Class getDependentType(EventSource source) { + private Class getDependentType(EventSource source) { return source instanceof ResourceEventSource ? ((ResourceEventSource) source).getResourceClass() : source.getClass(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index 5fa45e0a25..6767e974e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -5,7 +5,7 @@ public abstract class AbstractEventSource implements EventSource { - private volatile EventHandler handler; + private EventHandler handler; private volatile boolean running = false; protected EventHandler getEventHandler() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 486ffb81a6..aac04115a3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -7,7 +7,6 @@ import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -28,7 +27,7 @@ public abstract class CachingEventSource protected UpdatableCache cache; - public CachingEventSource(Class resourceClass) { + protected CachingEventSource(Class resourceClass) { super(resourceClass); cache = initCache(); } @@ -84,11 +83,6 @@ public Optional getCachedValue(ResourceID resourceID) { return cache.get(resourceID); } - @Override - public void stop() throws OperatorException { - super.stop(); - } - @Override public Optional getAssociated(P primary) { return cache.get(ResourceID.fromResource(primary)); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java new file mode 100644 index 0000000000..95042d0c41 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; + +public interface EventSourceContextAware

{ + void initWith(EventSourceContext

context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 9feabf40bf..2a224a87df 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -1,34 +1,27 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; -import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.javaoperatorsdk.operator.MissingCRDException; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; public class ControllerResourceEventSource - extends AbstractResourceEventSource + extends ManagedInformerEventSource> implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; @@ -36,20 +29,12 @@ public class ControllerResourceEventSource private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); private final Controller controller; - private final Map> sharedIndexInformers = - new ConcurrentHashMap<>(); - private final ResourceEventFilter filter; private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; - private final ControllerResourceCache cache; public ControllerResourceEventSource(Controller controller) { - super(controller.getConfiguration().getResourceClass()); + super(controller.getCRClient(), controller.getConfiguration()); this.controller = controller; - final var configurationService = controller.getConfiguration().getConfigurationService(); - var cloner = configurationService != null ? configurationService.getResourceCloner() - : ConfigurationService.DEFAULT_CLONER; - this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), @@ -73,70 +58,27 @@ public ControllerResourceEventSource(Controller controller) { @Override public void start() { - final var configuration = controller.getConfiguration(); - final var targetNamespaces = configuration.getEffectiveNamespaces(); - final var client = controller.getCRClient(); - final var labelSelector = configuration.getLabelSelector(); - try { - if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - final var informer = - createAndRunInformerFor(client.inAnyNamespace() - .withLabelSelector(labelSelector), ANY_NAMESPACE_MAP_KEY); - log.debug("Registered {} -> {} for any namespace", controller, informer); - } else { - targetNamespaces.forEach(ns -> { - final var informer = createAndRunInformerFor( - client.inNamespace(ns).withLabelSelector(labelSelector), ns); - log.debug("Registered {} -> {} for namespace: {}", controller, informer, ns); - }); - } + super.start(); } catch (Exception e) { if (e instanceof KubernetesClientException) { handleKubernetesClientException(e); } throw e; } - super.start(); - } - - private SharedIndexInformer createAndRunInformerFor( - FilterWatchListDeletable> filteredBySelectorClient, String key) { - var informer = filteredBySelectorClient.runnableInformer(0); - informer.addEventHandler(this); - sharedIndexInformers.put(key, informer); - informer.run(); - return informer; } - @Override - public void stop() { - for (SharedIndexInformer informer : sharedIndexInformers.values()) { - try { - log.info("Stopping informer {} -> {}", controller, informer); - informer.stop(); - } catch (Exception e) { - log.warn("Error stopping informer {} -> {}", controller, informer, e); - } - } - super.stop(); - } - - public void eventReceived(ResourceAction action, T customResource, T oldResource) { + public void eventReceived(ResourceAction action, T resource, T oldResource) { try { - log.debug( - "Event received for resource: {}", getName(customResource)); - MDCUtils.addResourceInfo(customResource); - controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, - oldResource); - if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { + log.debug("Event received for resource: {}", getName(resource)); + MDCUtils.addResourceInfo(resource); + controller.getEventSourceManager().broadcastOnResourceEvent(action, resource, oldResource); + if (filter.acceptChange(controller.getConfiguration(), oldResource, resource)) { getEventHandler().handleEvent( - new ResourceEvent(action, ResourceID.fromResource(customResource))); + new ResourceEvent(action, ResourceID.fromResource(resource))); } else { - log.debug( - "Skipping event handling resource {} with version: {}", - getUID(customResource), - getVersion(customResource)); + log.debug("Skipping event handling resource {} with version: {}", getUID(resource), + getVersion(resource)); } } finally { MDCUtils.removeResourceInfo(); @@ -158,24 +100,8 @@ public void onDelete(T resource, boolean b) { eventReceived(ResourceAction.DELETED, resource, null); } - public Optional get(ResourceID resourceID) { - return cache.get(resourceID); - } - - public ControllerResourceCache getResourceCache() { - return cache; - } - - /** - * @return shared informers by namespace. If custom resource is not namespace scoped use - * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY - */ - public Map> getInformers() { - return Collections.unmodifiableMap(sharedIndexInformers); - } - - public SharedIndexInformer getInformer(String namespace) { - return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); + public ResourceCache getResourceCache() { + return manager(); } /** @@ -204,6 +130,6 @@ private void handleKubernetesClientException(Exception e) { @Override public Optional getAssociated(T primary) { - return cache.get(ResourceID.fromResource(primary)); + return manager().get(ResourceID.fromResource(primary)); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java new file mode 100644 index 0000000000..10bfdcdcb8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -0,0 +1,132 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; + +public class InformerConfiguration + extends DefaultResourceConfiguration { + + private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private final AssociatedSecondaryResourceIdentifier

associatedWith; + private final boolean skipUpdateEventPropagationIfNoChange; + + private InformerConfiguration(ConfigurationService service, String labelSelector, + Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { + super(labelSelector, resourceClass, namespaces); + setConfigurationService(service); + this.secondaryToPrimaryResourcesIdSet = + Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, Mappers.fromOwnerReference()); + this.associatedWith = + Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); + this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; + } + + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + return secondaryToPrimaryResourcesIdSet; + } + + public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { + return associatedWith; + } + + public boolean isSkipUpdateEventPropagationIfNoChange() { + return skipUpdateEventPropagationIfNoChange; + } + + public static class InformerConfigurationBuilder { + + private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private AssociatedSecondaryResourceIdentifier

associatedWith; + private boolean skipUpdateEventPropagationIfNoChange = true; + private Set namespaces; + private String labelSelector; + private final Class resourceClass; + private final ConfigurationService configurationService; + + private InformerConfigurationBuilder(Class resourceClass, + ConfigurationService configurationService) { + this.resourceClass = resourceClass; + this.configurationService = configurationService; + } + + public InformerConfigurationBuilder withPrimaryResourcesRetriever( + PrimaryResourcesRetriever primaryResourcesRetriever) { + this.secondaryToPrimaryResourcesIdSet = primaryResourcesRetriever; + return this; + } + + public InformerConfigurationBuilder withAssociatedSecondaryResourceIdentifier( + AssociatedSecondaryResourceIdentifier

associatedWith) { + this.associatedWith = associatedWith; + return this; + } + + public InformerConfigurationBuilder withoutSkippingEventPropagationIfUnchanged() { + this.skipUpdateEventPropagationIfNoChange = false; + return this; + } + + public InformerConfigurationBuilder skippingEventPropagationIfUnchanged( + boolean skipIfUnchanged) { + this.skipUpdateEventPropagationIfNoChange = skipIfUnchanged; + return this; + } + + public InformerConfigurationBuilder withNamespaces(String... namespaces) { + this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet(); + return this; + } + + public InformerConfigurationBuilder withNamespaces(Set namespaces) { + this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); + return this; + } + + + public InformerConfigurationBuilder withLabelSelector(String labelSelector) { + this.labelSelector = labelSelector; + return this; + } + + public InformerConfiguration build() { + return new InformerConfiguration<>(configurationService, labelSelector, resourceClass, + secondaryToPrimaryResourcesIdSet, associatedWith, skipUpdateEventPropagationIfNoChange, + namespaces); + } + } + + public static InformerConfigurationBuilder from( + EventSourceContext

context, Class resourceClass) { + return new InformerConfigurationBuilder<>(resourceClass, context.getConfigurationService()); + } + + public static InformerConfigurationBuilder from(ConfigurationService configurationService, + Class resourceClass) { + return new InformerConfigurationBuilder<>(resourceClass, configurationService); + } + + public static InformerConfigurationBuilder from( + InformerConfiguration configuration) { + return new InformerConfigurationBuilder(configuration.getResourceClass(), + configuration.getConfigurationService()) + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .skippingEventPropagationIfUnchanged( + configuration.isSkipUpdateEventPropagationIfNoChange()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index eada096713..fa9e516585 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,112 +1,70 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; -import io.fabric8.kubernetes.client.informers.SharedInformer; -import io.fabric8.kubernetes.client.informers.cache.Store; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; public class InformerEventSource - extends AbstractResourceEventSource - implements ResourceCache { - - private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); + extends ManagedInformerEventSource> + implements ResourceCache, ResourceEventHandler { - private final SharedInformer sharedInformer; - private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private final AssociatedSecondaryResourceIdentifier

associatedWith; - private final boolean skipUpdateEventPropagationIfNoChange; + private final InformerConfiguration configuration; - public InformerEventSource(SharedInformer sharedInformer, - PrimaryResourcesRetriever resourceToTargetResourceIDSet) { - this(sharedInformer, resourceToTargetResourceIDSet, null, true); - } + public InformerEventSource(InformerConfiguration configuration, + EventSourceContext

context) { + super(context.getClient().resources(configuration.getResourceClass()), configuration); + this.configuration = configuration; - public InformerEventSource(KubernetesClient client, Class type, - PrimaryResourcesRetriever resourceToTargetResourceIDSet) { - this(client, type, resourceToTargetResourceIDSet, false); - } + // init mappers with context if needed + final var primaryResourcesRetriever = configuration.getPrimaryResourcesRetriever(); + if (primaryResourcesRetriever instanceof EventSourceContextAware) { + ((EventSourceContextAware) primaryResourcesRetriever).initWith(context); + } - public InformerEventSource(KubernetesClient client, Class type, - PrimaryResourcesRetriever resourceToTargetResourceIDSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, - associatedWith, - skipUpdateEventPropagationIfNoChange); + final var associatedResourceIdentifier = configuration.getAssociatedResourceIdentifier(); + if (associatedResourceIdentifier instanceof EventSourceContextAware) { + ((EventSourceContextAware) associatedResourceIdentifier).initWith(context); + } } - InformerEventSource(KubernetesClient client, Class type, - PrimaryResourcesRetriever resourceToTargetResourceIDSet, - boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, - skipUpdateEventPropagationIfNoChange); + @Override + public void onAdd(T t) { + propagateEvent(t); } - public InformerEventSource(SharedInformer sharedInformer, - PrimaryResourcesRetriever resourceToTargetResourceIDSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange) { - super(sharedInformer.getApiTypeClass()); - this.sharedInformer = sharedInformer; - this.secondaryToPrimaryResourcesIdSet = resourceToTargetResourceIDSet; - this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; - if (sharedInformer.isRunning()) { - log.warn( - "Informer is already running on event source creation, this is not desirable and may " + - "lead to non deterministic behavior."); + @Override + public void onUpdate(T oldObject, T newObject) { + if (newObject == null) { + // this is a fix for this potential issue with informer: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 + propagateEvent(oldObject); + return; } - this.associatedWith = - Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - - sharedInformer.addEventHandler(new ResourceEventHandler<>() { - @Override - public void onAdd(T t) { - propagateEvent(t); - } - - @Override - public void onUpdate(T oldObject, T newObject) { - if (newObject == null) { - // this is a fix for this potential issue with informer: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 - propagateEvent(oldObject); - return; - } - - if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange && - oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { - return; - } - propagateEvent(newObject); - } + if (configuration.isSkipUpdateEventPropagationIfNoChange() && + oldObject.getMetadata().getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { + return; + } + propagateEvent(newObject); + } - @Override - public void onDelete(T t, boolean b) { - propagateEvent(t); - } - }); + @Override + public void onDelete(T t, boolean b) { + propagateEvent(t); } private void propagateEvent(T object) { - var primaryResourceIdSet = secondaryToPrimaryResourcesIdSet.associatedPrimaryResources(object); + var primaryResourceIdSet = + configuration.getPrimaryResourcesRetriever().associatedPrimaryResources(object); if (primaryResourceIdSet.isEmpty()) { return; } @@ -126,16 +84,12 @@ private void propagateEvent(T object) { @Override public void start() { - sharedInformer.run(); + manager().start(); } @Override public void stop() { - sharedInformer.close(); - } - - private Store getStore() { - return sharedInformer.getStore(); + manager().stop(); } /** @@ -145,37 +99,14 @@ private Store getStore() { * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ + @Override public Optional getAssociated(P resource) { - final var id = associatedWith.associatedSecondaryID(resource); + final var id = configuration.getAssociatedResourceIdentifier().associatedSecondaryID(resource); return get(id); } - - public SharedInformer getSharedInformer() { - return sharedInformer; - } - - @Override - public Optional get(ResourceID resourceID) { - return Optional.ofNullable(sharedInformer.getStore() - .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( - resourceID.getNamespace().orElse(null), - resourceID.getName()))); - } - - @Override - public Stream list(Predicate predicate) { - return getStore().list().stream().filter(predicate); - } - @Override public Stream list(String namespace, Predicate predicate) { - return getStore().list().stream() - .filter(v -> namespace.equals(v.getMetadata().getNamespace()) && predicate.test(v)); - } - - @Override - public Stream keys() { - return getStore().listKeys().stream().map(Mappers::fromString); + return manager().list(namespace, predicate); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java new file mode 100644 index 0000000000..e65f34df48 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -0,0 +1,144 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.Cache; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +public class InformerManager> + implements LifecycleAware, ResourceCache, UpdatableCache { + + private static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; + private static final Logger log = LoggerFactory.getLogger(InformerManager.class); + + private final Map> sources = new ConcurrentHashMap<>(); + private Cloner cloner; + + @Override + public void start() throws OperatorException { + sources.values().parallelStream().forEach(LifecycleAware::start); + } + + void initSources(MixedOperation, Resource> client, + C configuration, ResourceEventHandler eventHandler) { + final var service = configuration.getConfigurationService(); + cloner = service == null ? ConfigurationService.DEFAULT_CLONER : service.getResourceCloner(); + + final var targetNamespaces = configuration.getEffectiveNamespaces(); + final var labelSelector = configuration.getLabelSelector(); + + if (ResourceConfiguration.allNamespacesWatched(targetNamespaces)) { + final var filteredBySelectorClient = + client.inAnyNamespace().withLabelSelector(labelSelector); + final var source = + createEventSource(filteredBySelectorClient, eventHandler, ANY_NAMESPACE_MAP_KEY); + log.debug("Registered {} -> {} for any namespace", this, source); + } else { + targetNamespaces.forEach( + ns -> { + final var source = + createEventSource(client.inNamespace(ns).withLabelSelector(labelSelector), + eventHandler, ns); + log.debug("Registered {} -> {} for namespace: {}", this, source, + ns); + }); + } + } + + + private InformerWrapper createEventSource( + FilterWatchListDeletable> filteredBySelectorClient, + ResourceEventHandler eventHandler, String key) { + var source = new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0)); + source.addEventHandler(eventHandler); + sources.put(key, source); + return source; + } + + @Override + public void stop() { + for (InformerWrapper source : sources.values()) { + try { + log.info("Stopping informer {} -> {}", this, source); + source.stop(); + } catch (Exception e) { + log.warn("Error stopping informer {} -> {}", this, source, e); + } + } + } + + @Override + public Stream list(Predicate predicate) { + if (predicate == null) { + return sources.values().stream().flatMap(ResourceCache::list); + } + return sources.values().stream().flatMap(i -> i.list(predicate)); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + if (isWatchingAllNamespaces()) { + return getSource(ANY_NAMESPACE_MAP_KEY) + .map(source -> source.list(namespace, predicate)) + .orElse(Stream.empty()); + } else { + return getSource(namespace) + .map(source -> source.list(predicate)) + .orElse(Stream.empty()); + } + } + + @Override + public Optional get(ResourceID resourceID) { + return getSource(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .flatMap(source -> source.get(resourceID)) + .map(cloner::clone); + } + + @Override + public Stream keys() { + return sources.values().stream().flatMap(Cache::keys); + } + + private boolean isWatchingAllNamespaces() { + return sources.containsKey(ANY_NAMESPACE_MAP_KEY); + } + + private Optional> getSource(String namespace) { + namespace = isWatchingAllNamespaces() || namespace == null ? ANY_NAMESPACE_MAP_KEY : namespace; + return Optional.ofNullable(sources.get(namespace)); + } + + @Override + public T remove(ResourceID key) { + return getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .map(c -> c.remove(key)) + .orElse(null); + } + + @Override + public void put(ResourceID key, T resource) { + getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .ifPresent(c -> c.put(key, resource)); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java new file mode 100644 index 0000000000..5199b4fb0d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java @@ -0,0 +1,64 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.SharedInformer; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +class InformerResourceCache implements ResourceCache, UpdatableCache { + + private final Cache cache; + + public InformerResourceCache(SharedInformer informer) { + this.cache = (Cache) informer.getStore(); + } + + @Override + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(cache.getByKey(getKey(resourceID))); + } + + private String getKey(ResourceID resourceID) { + return Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName()); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list().stream().filter(predicate); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + final var stream = cache.list().stream() + .filter(r -> namespace.equals(r.getMetadata().getNamespace())); + return predicate != null ? stream.filter(predicate) : stream; + } + + @Override + public Stream keys() { + return cache.listKeys().stream().map(Mappers::fromString); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(cache.getByKey(getKey(key))); + } + + @Override + public void put(ResourceID key, T resource) { + // check that key matches the resource + final var fromResource = ResourceID.fromResource(resource); + if (!Objects.equals(key, fromResource)) { + throw new IllegalArgumentException( + "Key and resource don't match. Key: " + key + ", resource: " + fromResource); + } + cache.put(resource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java new file mode 100644 index 0000000000..c7de41f331 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -0,0 +1,84 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +class InformerWrapper + implements LifecycleAware, ResourceCache, UpdatableCache { + private final SharedIndexInformer informer; + private final InformerResourceCache cache; + + public InformerWrapper(SharedIndexInformer informer) { + this.informer = informer; + this.cache = new InformerResourceCache<>(informer); + } + + @Override + public void start() throws OperatorException { + informer.run(); + } + + @Override + public void stop() throws OperatorException { + informer.stop(); + } + + @Override + public Optional get(ResourceID resourceID) { + return cache.get(resourceID); + } + + @Override + public boolean contains(ResourceID resourceID) { + return cache.contains(resourceID); + } + + @Override + public Stream keys() { + return cache.keys(); + } + + @Override + public Stream list() { + return cache.list(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list(predicate); + } + + @Override + public Stream list(String namespace) { + return cache.list(namespace); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return cache.list(namespace, predicate); + } + + public void addEventHandler(ResourceEventHandler eventHandler) { + informer.addEventHandler(eventHandler); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(key); + } + + @Override + public void put(ResourceID key, T resource) { + cache.put(key, resource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java new file mode 100644 index 0000000000..f0a1a1185d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +public abstract class ManagedInformerEventSource> + extends CachingEventSource + implements ResourceEventHandler { + + protected ManagedInformerEventSource( + MixedOperation, Resource> client, C configuration) { + super(configuration.getResourceClass()); + manager().initSources(client, configuration, this); + } + + @Override + protected UpdatableCache initCache() { + return new InformerManager<>(); + } + + protected InformerManager manager() { + return (InformerManager) cache; + } + + @Override + public void start() { + manager().start(); + super.start(); + } + + + @Override + public void stop() { + super.stop(); + manager().stop(); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 053528e0ff..21cd3b162a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -9,6 +9,8 @@ public class Mappers { + private Mappers() {} + public static PrimaryResourcesRetriever fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index a1cfa3e82d..73d3382cc4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -44,8 +44,10 @@ private void checkException( TestControllerConfiguration duplicated) { final var exception = assertThrows(OperatorException.class, () -> { final var controllerManager = new ControllerManager(); - controllerManager.add(new Controller<>(registered.controller, registered, null)); - controllerManager.add(new Controller<>(duplicated.controller, duplicated, null)); + controllerManager.add(new Controller<>(registered.controller, registered, + MockKubernetesClient.client(registered.getResourceClass()))); + controllerManager.add(new Controller<>(duplicated.controller, duplicated, + MockKubernetesClient.client(duplicated.getResourceClass()))); }); final var msg = exception.getMessage(); assertTrue( @@ -60,7 +62,8 @@ private static class TestControllerConfiguration public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), - CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null); + CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null, + null); this.controller = controller; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java new file mode 100644 index 0000000000..0c2db0f6b1 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.V1ApiextensionAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockKubernetesClient { + + public static KubernetesClient client(Class clazz) { + final var client = mock(KubernetesClient.class); + MixedOperation, Resource> resources = + mock(MixedOperation.class); + NonNamespaceOperation, Resource> nonNamespaceOperation = + mock(NonNamespaceOperation.class); + FilterWatchListMultiDeletable> inAnyNamespace = mock( + FilterWatchListMultiDeletable.class); + FilterWatchListDeletable> filterable = + mock(FilterWatchListDeletable.class); + when(resources.inNamespace(anyString())).thenReturn(nonNamespaceOperation); + when(nonNamespaceOperation.withLabelSelector(nullable(String.class))).thenReturn(filterable); + when(resources.inAnyNamespace()).thenReturn(inAnyNamespace); + when(inAnyNamespace.withLabelSelector(nullable(String.class))).thenReturn(filterable); + SharedIndexInformer informer = mock(SharedIndexInformer.class); + when(filterable.runnableInformer(anyLong())).thenReturn(informer); + when(client.resources(clazz)).thenReturn(resources); + + final var apiGroupDSL = mock(ApiextensionsAPIGroupDSL.class); + when(client.apiextensions()).thenReturn(apiGroupDSL); + final var v1 = mock(V1ApiextensionAPIGroupDSL.class); + when(apiGroupDSL.v1()).thenReturn(v1); + final var operation = mock(NonNamespaceOperation.class); + when(v1.customResourceDefinitions()).thenReturn(operation); + when(operation.withName(any())).thenReturn(mock(Resource.class)); + + return client; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index c234a0b9b2..f532756f58 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -19,7 +19,8 @@ class OperatorTest { - private final KubernetesClient kubernetesClient = mock(KubernetesClient.class); + private final KubernetesClient kubernetesClient = + MockKubernetesClient.client(FooCustomResource.class); private final ConfigurationService configurationService = mock(ConfigurationService.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final Operator operator = new Operator(kubernetesClient, configurationService); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 3bc72d81f2..65c73b6b31 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -20,6 +20,9 @@ public String getAssociatedReconcilerClassName() { public ConfigurationService getConfigurationService() { return null; } + + @Override + public void setConfigurationService(ConfigurationService service) {} }; assertEquals(TestCustomResource.class, conf.getResourceClass()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index d61b50d583..2954af5aa4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -3,19 +3,14 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.V1ApiextensionAPIGroupDSL; -import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL; -import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -26,7 +21,7 @@ class ControllerTest { @Test void crdShouldNotBeCheckedForNativeResources() { - final var client = mock(KubernetesClient.class); + final var client = MockKubernetesClient.client(Secret.class); final var configurationService = mock(ConfigurationService.class); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); @@ -40,7 +35,7 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { - final var client = mock(KubernetesClient.class); + final var client = MockKubernetesClient.client(TestCustomResource.class); final var configurationService = mock(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); final var reconciler = mock(Reconciler.class); @@ -55,20 +50,13 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { @Test void crdShouldBeCheckedForCustomResourcesByDefault() { - final var client = mock(KubernetesClient.class); + final var client = MockKubernetesClient.client(TestCustomResource.class); final var configurationService = mock(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); when(configuration.getConfigurationService()).thenReturn(configurationService); - final var apiGroupDSL = mock(ApiextensionsAPIGroupDSL.class); - when(client.apiextensions()).thenReturn(apiGroupDSL); - final var v1 = mock(V1ApiextensionAPIGroupDSL.class); - when(apiGroupDSL.v1()).thenReturn(v1); - final var operation = mock(NonNamespaceOperation.class); - when(v1.customResourceDefinitions()).thenReturn(operation); - when(operation.withName(any())).thenReturn(mock(Resource.class)); final var controller = new Controller(reconciler, configuration, client); // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index a330b4130c..64e742d381 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -8,8 +8,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -169,10 +171,10 @@ void timerAndControllerEventSourcesShouldBeListedFirst() { } private EventSourceManager initManager() { - final Controller controller = mock(Controller.class); final ControllerConfiguration configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(HasMetadata.class); - when(controller.getConfiguration()).thenReturn(configuration); + final Controller controller = new Controller(mock(Reconciler.class), configuration, + MockKubernetesClient.client(HasMetadata.class)); return new EventSourceManager(controller); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 9da51337d3..9cd7d9481b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationService; @@ -90,7 +91,9 @@ public T clone(T object) { }); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); - Controller controller = new Controller<>(reconciler, configuration, null); + Controller controller = new Controller<>(reconciler, configuration, + MockKubernetesClient.client(customResource.getClass())); + controller.start(); return new ReconciliationDispatcher<>(controller, customResourceFacade); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index ce865b7888..446e654501 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -158,6 +158,9 @@ public Set getNamespaces() { public ConfigurationService getConfigurationService() { return service; } + + @Override + public void setConfigurationService(ConfigurationService service) {} } @ControllerConfiguration(namespaces = NAMESPACE) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index 17168876a5..6c5e306263 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -7,10 +7,8 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -171,6 +169,7 @@ public ControllerConfig(String finalizer, boolean generationAware, null, eventFilter, customResourceClass, + null, null); } } @@ -178,18 +177,13 @@ public ControllerConfig(String finalizer, boolean generationAware, private static class TestController extends Controller { public TestController(ControllerConfiguration configuration) { - super(null, configuration, null); + super(null, configuration, MockKubernetesClient.client(TestCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } } private static class ObservedGenController @@ -197,17 +191,12 @@ private static class ObservedGenController public ObservedGenController( ControllerConfiguration configuration) { - super(null, configuration, null); + super(null, configuration, MockKubernetesClient.client(ObservedGenCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 5cdc85c553..f67ea44a5d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -6,9 +6,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; @@ -28,8 +26,6 @@ class ControllerResourceEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { public static final String FINALIZER = "finalizer"; - private static final MixedOperation, Resource> client = - mock(MixedOperation.class); private TestController testController = new TestController(true); @@ -141,18 +137,14 @@ private static class TestController extends Controller { mock(EventSourceManager.class); public TestController(boolean generationAware) { - super(null, new TestConfiguration(generationAware), null); + super(null, new TestConfiguration(generationAware), + MockKubernetesClient.client(TestCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return eventSourceManager; } - - @Override - public MixedOperation, Resource> getCRClient() { - return client; - } } private static class TestConfiguration extends @@ -170,6 +162,7 @@ public TestConfiguration(boolean generationAware) { null, null, TestCustomResource.class, + null, null); } } diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 69a163b7d7..6f66b97810 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index fd124ea222..8e7ff3fe8f 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 1000cb61c4..89a100741f 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,22 +1,33 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; +@SuppressWarnings("rawtypes") public class AnnotationConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; + private List dependents; public AnnotationConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -108,14 +119,54 @@ public ResourceEventFilter getEventFilter() { : ResourceEventFilters.passthrough(); } - public static T valueOrDefault(ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); + @Override + public List getDependentResources() { + if (dependents == null) { + final var dependentConfigs = valueOrDefault(annotation, + ControllerConfiguration::dependents, new Dependent[] {}); + if (dependentConfigs.length > 0) { + dependents = new ArrayList<>(dependentConfigs.length); + for (Dependent dependentConfig : dependentConfigs) { + final Class dependentType = dependentConfig.type(); + DependentResource dependent; + try { + dependent = dependentType.getConstructor().newInstance(); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException + | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + + final var resourceType = dependentConfig.resourceType(); + if (HasMetadata.class.isAssignableFrom(resourceType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); + final var labelSelector = + valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, + KubernetesDependent.OWNED_DEFAULT); + final var skipIfUnchanged = + valueOrDefault(kubeDependent, KubernetesDependent::skipUpdateIfUnchanged, + KubernetesDependent.SKIP_UPDATE_DEFAULT); + final var configuration = InformerConfiguration.from(service, resourceType) + .withLabelSelector(labelSelector) + .skippingEventPropagationIfUnchanged(skipIfUnchanged) + .withNamespaces(namespaces) + .build(); + dependent = new KubernetesDependentResourceController(dependent, configuration, owned); + } + + dependents.add(dependent); + } + } else { + dependents = Collections.emptyList(); + } } + return dependents; + } + + private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { + return annotation == null ? defaultValue : mapper.apply(annotation); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index b7ceb4c11c..2a04b85d19 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -1,24 +1,23 @@ package io.javaoperatorsdk.operator.sample.informereventsource; -import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.junit.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.ConfigMapDR; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -26,10 +25,10 @@ * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class InformerEventSourceTestCustomReconciler implements - Reconciler, KubernetesClientAware, - EventSourceInitializer { +@ControllerConfiguration(finalizerName = NO_FINALIZER, + dependents = @Dependent(resourceType = ConfigMap.class, type = ConfigMapDR.class)) +public class InformerEventSourceTestCustomReconciler + implements Reconciler { private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); @@ -38,14 +37,18 @@ public class InformerEventSourceTestCustomReconciler implements public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; public static final String MISSING_CONFIG_MAP = "Missing Config Map"; - private KubernetesClient kubernetesClient; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - @Override - public List prepareEventSources( - EventSourceContext context) { - return List.of(new InformerEventSource<>(kubernetesClient, ConfigMap.class, - Mappers.fromAnnotation(RELATED_RESOURCE_NAME))); + public static class ConfigMapDR + implements DependentResource, + PrimaryResourcesRetriever { + private final PrimaryResourcesRetriever retriever = Mappers.fromAnnotation( + RELATED_RESOURCE_NAME); + + @Override + public Set associatedPrimaryResources(ConfigMap dependentResource) { + return retriever.associatedPrimaryResources(dependentResource); + } } @Override @@ -69,16 +72,6 @@ public UpdateControl reconcile( return UpdateControl.updateStatus(resource); } - @Override - public KubernetesClient getKubernetesClient() { - return kubernetesClient; - } - - @Override - public void setKubernetesClient(KubernetesClient kubernetesClient) { - this.kubernetesClient = kubernetesClient; - } - public int getNumberOfExecutions() { return numberOfExecutions.get(); } diff --git a/pom.xml b/pom.xml index a520155deb..fbca5b3ae2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 58d1f3155a..64be615e03 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index ebccbd4fe5..d9012637cf 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -32,11 +32,10 @@ public static void main(String[] args) throws IOException { new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) .build()); - operator.register(new MySQLSchemaReconciler(client, MySQLDbConfig.loadFromEnvironmentVars())); + operator.register(new MySQLSchemaReconciler(MySQLDbConfig.loadFromEnvironmentVars())); operator.installShutdownHook(); operator.start(); - new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index af77b6ce97..ecc2aa9be7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -1,101 +1,107 @@ package io.javaoperatorsdk.operator.sample; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; import java.util.Base64; -import java.util.List; import java.util.Optional; import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; -import io.javaoperatorsdk.operator.sample.schema.SchemaService; import static java.lang.String.format; -@ControllerConfiguration +@ControllerConfiguration( + dependents = { + @Dependent(resourceType = Secret.class, type = SecretDependentResource.class), + @Dependent(resourceType = Schema.class, type = SchemaDependentResource.class) + }) public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, - EventSourceInitializer { - public static final String SECRET_FORMAT = "%s-secret"; - public static final String USERNAME_FORMAT = "%s-user"; - public static final int POLL_PERIOD = 500; - private final Logger log = LoggerFactory.getLogger(getClass()); + ContextInitializer, EventSourceContextInjector { + + private static final String SECRET_FORMAT = "%s-secret"; + private static final String USERNAME_FORMAT = "%s-user"; + + protected static final String MYSQL_SECRET_NAME = "mysql.secret.name"; + protected static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; + protected static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; + protected static final String MYSQL_DB_CONFIG = "mysql.db.config"; + protected static final String BUILT_SCHEMA = "built schema"; + static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); - private final KubernetesClient kubernetesClient; private final MySQLDbConfig mysqlDbConfig; - public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig mysqlDbConfig) { - this.kubernetesClient = kubernetesClient; + public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { this.mysqlDbConfig = mysqlDbConfig; } + public static class SecretDependentResource + implements DependentResource, Builder { + + @Override + public Secret buildFor(MySQLSchema schema, Context context) { + return new SecretBuilder() + .withNewMetadata() + .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) + .withNamespace(schema.getMetadata().getNamespace()) + .endMetadata() + .addToData("MYSQL_USERNAME", encode( + context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) + .addToData("MYSQL_PASSWORD", encode( + context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) + .build(); + } + + private static String encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } + } + @Override - public List prepareEventSources( - EventSourceContext context) { - return List.of(new PerResourcePollingEventSource<>( - new SchemaPollingResourceSupplier(mysqlDbConfig), context.getPrimaryCache(), POLL_PERIOD, - Schema.class)); + public void injectInto(EventSourceContext context) { + context.put(MYSQL_DB_CONFIG, mysqlDbConfig); } @Override - public UpdateControl reconcile(MySQLSchema schema, Context context) { - log.info("Reconciling MySQLSchema with name: {}", schema.getMetadata().getName()); - var dbSchema = context.getSecondaryResource(Schema.class); - log.debug("Schema: {} found for: {} ", dbSchema, schema.getMetadata().getName()); - try (Connection connection = getConnection()) { - if (dbSchema.isEmpty()) { - log.debug("Creating Schema and related resources for: {}", schema.getMetadata().getName()); - var schemaName = schema.getMetadata().getName(); - String password = RandomStringUtils.randomAlphanumeric(16); - String secretName = String.format(SECRET_FORMAT, schemaName); - String userName = String.format(USERNAME_FORMAT, schemaName); - - SchemaService.createSchemaAndRelatedUser(connection, schemaName, - schema.getSpec().getEncoding(), userName, password); - createSecret(schema, password, secretName, userName); - updateStatusPojo(schema, secretName, userName); - log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); - return UpdateControl.updateStatus(schema); - } else { - log.debug("No update on MySQLSchema with name: {}", schema.getMetadata().getName()); - return UpdateControl.noUpdate(); - } - } catch (SQLException e) { - log.error("Error while creating Schema", e); - throw new IllegalStateException(e); - } + public void initContext(MySQLSchema primary, Context context) { + final var name = primary.getMetadata().getName(); + // NOSONAR we don't need cryptographically-strong randomness here + final var password = RandomStringUtils.randomAlphanumeric(16); + final var secretName = String.format(SECRET_FORMAT, name); + final var userName = String.format(USERNAME_FORMAT, name); + + // put information in context for other dependents and reconciler to use + context.put(MYSQL_SECRET_PASSWORD, password); + context.put(MYSQL_SECRET_NAME, secretName); + context.put(MYSQL_SECRET_USERNAME, userName); } @Override - public DeleteControl cleanup(MySQLSchema schema, Context context) { - log.info("Cleaning up for: {}", schema.getMetadata().getName()); - try (Connection connection = getConnection()) { - var dbSchema = SchemaService.getSchema(connection, schema.getMetadata().getName()); - if (dbSchema.isPresent()) { - var userName = schema.getStatus() != null ? schema.getStatus().getUserName() : null; - SchemaService.deleteSchemaAndRelatedUser(connection, schema.getMetadata().getName(), - userName); - } else { - log.info( - "Delete event ignored for schema '{}', real schema doesn't exist", - schema.getMetadata().getName()); - } - return DeleteControl.defaultDelete(); - } catch (SQLException e) { - log.error("Error while trying to delete Schema", e); - return DeleteControl.noFinalizerRemoval(); - } + public UpdateControl reconcile(MySQLSchema schema, Context context) { + // we only need to update the status if we just built the schema, i.e. when it's present in the + // context + return context.get(BUILT_SCHEMA, Schema.class).map(s -> { + updateStatusPojo(schema, context.getMandatory(MYSQL_SECRET_NAME, String.class), + context.getMandatory(MYSQL_SECRET_USERNAME, String.class)); + log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); + return UpdateControl.updateStatus(schema); + }).orElse(UpdateControl.noUpdate()); } @Override @@ -110,14 +116,6 @@ public Optional updateErrorStatus(MySQLSchema schema, RetryInfo ret return Optional.empty(); } - private Connection getConnection() throws SQLException { - String connectionString = - format("jdbc:mysql://%1$s:%2$s", mysqlDbConfig.getHost(), mysqlDbConfig.getPort()); - - log.debug("Connecting to '{}' with user '{}'", connectionString, mysqlDbConfig.getUser()); - return DriverManager.getConnection(connectionString, mysqlDbConfig.getUser(), - mysqlDbConfig.getPassword()); - } private void updateStatusPojo(MySQLSchema schema, String secretName, String userName) { SchemaStatus status = new SchemaStatus(); @@ -130,33 +128,4 @@ private void updateStatusPojo(MySQLSchema schema, String secretName, String user status.setStatus("CREATED"); schema.setStatus(status); } - - private void createSecret(MySQLSchema schema, String password, String secretName, - String userName) { - - var currentSecret = kubernetesClient.secrets().inNamespace(schema.getMetadata().getNamespace()) - .withName(secretName).get(); - if (currentSecret != null) { - return; - } - Secret credentialsSecret = - new SecretBuilder() - .withNewMetadata() - .withName(secretName) - .withOwnerReferences(new OwnerReference("mysql.sample.javaoperatorsdk/v1", - false, false, "MySQLSchema", - schema.getMetadata().getName(), schema.getMetadata().getUid())) - .endMetadata() - .addToData( - "MYSQL_USERNAME", Base64.getEncoder().encodeToString(userName.getBytes())) - .addToData( - "MYSQL_PASSWORD", Base64.getEncoder().encodeToString(password.getBytes())) - .build(); - this.kubernetesClient - .secrets() - .inNamespace(schema.getMetadata().getNamespace()) - .create(credentialsSecret); - } - - } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java new file mode 100644 index 0000000000..de4b0ece58 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -0,0 +1,88 @@ +package io.javaoperatorsdk.operator.sample; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.schema.Schema; +import io.javaoperatorsdk.operator.sample.schema.SchemaService; + +import static java.lang.String.format; + +public class SchemaDependentResource + implements DependentResource, Builder, + Cleaner, Persister { + + private static final int POLL_PERIOD = 500; + private MySQLDbConfig dbConfig; + + @Override + public EventSource initEventSource(EventSourceContext context) { + dbConfig = context.getMandatory(MySQLSchemaReconciler.MYSQL_DB_CONFIG, MySQLDbConfig.class); + return new PerResourcePollingEventSource<>( + new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), POLL_PERIOD, + Schema.class); + } + + @Override + public Schema buildFor(MySQLSchema primary, Context context) { + try (Connection connection = getConnection()) { + final var schema = SchemaService.createSchemaAndRelatedUser( + connection, + primary.getMetadata().getName(), + primary.getSpec().getEncoding(), + context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class), + context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class)); + + // put the newly built schema in the context to let the reconciler know we just built it + context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); + return schema; + } catch (SQLException e) { + MySQLSchemaReconciler.log.error("Error while creating Schema", e); + throw new IllegalStateException(e); + } + } + + private Connection getConnection() throws SQLException { + String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); + + MySQLSchemaReconciler.log.debug("Connecting to '{}' with user '{}'", connectURL, + dbConfig.getUser()); + return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword()); + } + + @Override + public void delete(Schema fetched, MySQLSchema primary, Context context) { + try (Connection connection = getConnection()) { + var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; + SchemaService.deleteSchemaAndRelatedUser(connection, primary.getMetadata().getName(), + userName); + } catch (SQLException e) { + throw new RuntimeException("Error while trying to delete Schema", e); + } + } + + @Override + public void createOrReplace(Schema dependentResource, Context context) { + // this is actually implemented in buildFor, the cleaner way to do this would be to have all + // the needed information in Schema instead of creating both the schema and user from + // heterogeneous information + } + + @Override + public Schema getFor(MySQLSchema primary, Context context) { + try (Connection connection = getConnection()) { + return SchemaService.getSchema(connection, primary.getMetadata().getName()).orElse(null); + } catch (SQLException e) { + throw new RuntimeException("Error while trying to delete Schema", e); + } + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java index 8c6cd31b70..6db1c9b03b 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java @@ -29,7 +29,7 @@ public Optional getSchema(String name) { } } - public static void createSchemaAndRelatedUser(Connection connection, String schemaName, + public static Schema createSchemaAndRelatedUser(Connection connection, String schemaName, String encoding, String userName, String password) { @@ -49,6 +49,8 @@ public static void createSchemaAndRelatedUser(Connection connection, String sche statement.execute( format("GRANT ALL ON `%1$s`.* TO '%2$s'", schemaName, userName)); } + + return new Schema(schemaName, encoding); } catch (SQLException e) { throw new IllegalStateException(e); } diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index f4c529426b..31b5304028 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -37,7 +37,7 @@ public void test() throws IOException { if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { Operator operator = new Operator(client, DefaultConfigurationService.instance()); MySQLDbConfig dbConfig = new MySQLDbConfig("mysql", null, "root", "password"); - operator.register(new MySQLSchemaReconciler(client, dbConfig)); + operator.register(new MySQLSchemaReconciler(dbConfig)); operator.start(); } diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 9877aaef0d..e5819f32a8 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ffcd7038e7..5c75031b16 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java new file mode 100644 index 0000000000..245011a1cf --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.KubernetesDependent; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; + +@KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") +public class DeploymentDependentResource + implements DependentResource, Builder, + Updater { + + @Override + public Deployment buildFor(Tomcat tomcat, Context context) { + Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); + final ObjectMeta tomcatMetadata = tomcat.getMetadata(); + final String tomcatName = tomcatMetadata.getName(); + deployment = new DeploymentBuilder(deployment) + .editMetadata() + .withName(tomcatName) + .withNamespace(tomcatMetadata.getNamespace()) + .addToLabels("app", tomcatName) + .addToLabels("app.kubernetes.io/part-of", tomcatName) + .addToLabels("app.kubernetes.io/managed-by", "tomcat-operator") + .endMetadata() + .editSpec() + .editSelector().addToMatchLabels("app", tomcatName).endSelector() + .withReplicas(tomcat.getSpec().getReplicas()) + // set tomcat version + .editTemplate() + // make sure label selector matches label (which has to be matched by service selector too) + .editMetadata().addToLabels("app", tomcatName).endMetadata() + .editSpec() + .editFirstContainer().withImage("tomcat:" + tomcat.getSpec().getVersion()).endContainer() + .endSpec() + .endTemplate() + .endSpec() + .build(); + return deployment; + } + + @Override + public Deployment update(Deployment fetched, Tomcat tomcat, Context context) { + return new DeploymentBuilder(fetched).editSpec().editTemplate().editSpec().editFirstContainer() + .withImage("tomcat:" + tomcat.getSpec().getVersion()) + .endContainer().endSpec().endTemplate().endSpec().build(); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java new file mode 100644 index 0000000000..b90bd0e6e2 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; + +public class ServiceDependentResource + implements DependentResource, Builder { + + @Override + public Service buildFor(Tomcat tomcat, Context context) { + final ObjectMeta tomcatMetadata = tomcat.getMetadata(); + final Service service = + new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + .editMetadata() + .withName(tomcatMetadata.getName()) + .withNamespace(tomcatMetadata.getNamespace()) + .endMetadata() + .editSpec() + .addToSelector("app", tomcatMetadata.getName()) + .endSpec() + .build(); + return service; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java new file mode 100644 index 0000000000..9dd49466ab --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.Set; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; + +public class TomcatDependentResource + implements DependentResource, PrimaryResourcesRetriever, + AssociatedSecondaryResourceIdentifier, EventSourceContextAware { + + private ResourceCache primaryCache; + + @Override + public void initWith(EventSourceContext context) { + this.primaryCache = context.getPrimaryCache(); + } + + @Override + public Set associatedPrimaryResources(Tomcat t) { + // To create an event to a related WebApp resource and trigger the reconciliation + // we need to find which WebApp this Tomcat custom resource is related to. + // To find the related customResourceId of the WebApp resource we traverse the cache to + // and identify it based on naming convention. + return primaryCache + .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(ResourceID::fromResource) + .collect(Collectors.toSet()); + } + + @Override + public ResourceID associatedSecondaryID(Webapp primary) { + return new ResourceID(primary.getSpec().getTomcat(), primary.getMetadata().getNamespace()); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index 487183dfe5..09a7394e5b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -25,7 +25,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new TomcatReconciler(client)); + operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index bb67128f9e..32f7927f44 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -2,28 +2,20 @@ import java.io.IOException; import java.io.InputStream; -import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.config.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -31,45 +23,27 @@ * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also * creates a Service over which the Pods can be accessed. */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class TomcatReconciler implements Reconciler, EventSourceInitializer { +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + dependents = { + @Dependent(resourceType = Deployment.class, type = DeploymentDependentResource.class), + @Dependent(resourceType = Service.class, type = ServiceDependentResource.class) + }) +public class TomcatReconciler implements Reconciler { private final Logger log = LoggerFactory.getLogger(getClass()); - private final KubernetesClient kubernetesClient; - - public TomcatReconciler(KubernetesClient client) { - this.kubernetesClient = client; - } - - @Override - public List prepareEventSources(EventSourceContext context) { - SharedIndexInformer deploymentInformer = - kubernetesClient.apps().deployments().inAnyNamespace() - .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") - .runnableInformer(0); - - return List.of(new InformerEventSource<>( - deploymentInformer, Mappers.fromOwnerReference())); - } - @Override public UpdateControl reconcile(Tomcat tomcat, Context context) { - createOrUpdateDeployment(tomcat); - createOrUpdateService(tomcat); - - return context.getSecondaryResource(Deployment.class) - .map(deployment -> { - Tomcat updatedTomcat = - updateTomcatStatus(tomcat, deployment); - log.info( - "Updating status of Tomcat {} in namespace {} to {} ready replicas", - tomcat.getMetadata().getName(), - tomcat.getMetadata().getNamespace(), - tomcat.getStatus().getReadyReplicas()); - return UpdateControl.updateStatus(updatedTomcat); - }) - .orElse(UpdateControl.noUpdate()); + return context.getSecondaryResource(Deployment.class).map(deployment -> { + Tomcat updatedTomcat = updateTomcatStatus(tomcat, deployment); + log.info( + "Updating status of Tomcat {} in namespace {} to {} ready replicas", + tomcat.getMetadata().getName(), + tomcat.getMetadata().getNamespace(), + tomcat.getStatus().getReadyReplicas()); + return UpdateControl.updateStatus(updatedTomcat); + }).orElse(UpdateControl.noUpdate()); } private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { @@ -82,78 +56,8 @@ private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { return tomcat; } - private void createOrUpdateDeployment(Tomcat tomcat) { - String ns = tomcat.getMetadata().getNamespace(); - Deployment existingDeployment = - kubernetesClient - .apps() - .deployments() - .inNamespace(ns) - .withName(tomcat.getMetadata().getName()) - .get(); - if (existingDeployment == null) { - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(tomcat.getMetadata().getName()); - deployment.getMetadata().setNamespace(ns); - deployment.getMetadata().getLabels().put("app.kubernetes.io/part-of", - tomcat.getMetadata().getName()); - deployment.getMetadata().getLabels().put("app.kubernetes.io/managed-by", "tomcat-operator"); - // set tomcat version - deployment - .getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .setImage("tomcat:" + tomcat.getSpec().getVersion()); - deployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); - - // make sure label selector matches label (which has to be matched by service selector too) - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", tomcat.getMetadata().getName()); - deployment - .getSpec() - .getSelector() - .getMatchLabels() - .put("app", tomcat.getMetadata().getName()); - - OwnerReference ownerReference = deployment.getMetadata().getOwnerReferences().get(0); - ownerReference.setName(tomcat.getMetadata().getName()); - ownerReference.setUid(tomcat.getMetadata().getUid()); - - log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); - kubernetesClient.apps().deployments().inNamespace(ns).create(deployment); - } else { - existingDeployment - .getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .setImage("tomcat:" + tomcat.getSpec().getVersion()); - existingDeployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); - kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(existingDeployment); - } - } - - private void createOrUpdateService(Tomcat tomcat) { - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(tomcat.getMetadata().getName()); - String ns = tomcat.getMetadata().getNamespace(); - service.getMetadata().setNamespace(ns); - service.getMetadata().getOwnerReferences().get(0).setName(tomcat.getMetadata().getName()); - service.getMetadata().getOwnerReferences().get(0).setUid(tomcat.getMetadata().getUid()); - service.getSpec().getSelector().put("app", tomcat.getMetadata().getName()); - log.info("Creating or updating Service {} in {}", service.getMetadata().getName(), ns); - kubernetesClient.services().inNamespace(ns).createOrReplace(service); - } - - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { + static T loadYaml(Class clazz, String yaml) { + try (InputStream is = TomcatReconciler.class.getResourceAsStream(yaml)) { return Serialization.unmarshal(is, clazz); } catch (IOException ex) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 0f785feb02..9e1f6f677f 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -25,15 +25,18 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @ControllerConfiguration public class WebappReconciler implements Reconciler, EventSourceInitializer { - private KubernetesClient kubernetesClient; + private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); - private final Logger log = LoggerFactory.getLogger(getClass()); + private final KubernetesClient kubernetesClient; public WebappReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; @@ -41,20 +44,32 @@ public WebappReconciler(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { - return List.of(new InformerEventSource<>( - kubernetesClient, Tomcat.class, t -> { - // To create an event to a related WebApp resource and trigger the reconciliation - // we need to find which WebApp this Tomcat custom resource is related to. - // To find the related customResourceId of the WebApp resource we traverse the cache to - // and identify it based on naming convention. - return context.getPrimaryCache() - .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(ResourceID::fromResource) - .collect(Collectors.toSet()); - }, - (Webapp webapp) -> new ResourceID(webapp.getSpec().getTomcat(), - webapp.getMetadata().getNamespace()), - true)); + /* + * To create an event to a related WebApp resource and trigger the reconciliation we need to + * find which WebApp this Tomcat custom resource is related to. To find the related + * customResourceId of the WebApp resource we traverse the cache and identify it based on naming + * convention. + */ + final PrimaryResourcesRetriever webappsMatchingTomcatName = + (Tomcat t) -> context.getPrimaryCache() + .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(ResourceID::fromResource) + .collect(Collectors.toSet()); + + /* + * We retrieve the Tomcat instance associated with out Webapp from its spec + */ + final AssociatedSecondaryResourceIdentifier tomcatFromWebAppSpec = + (Webapp webapp) -> new ResourceID( + webapp.getSpec().getTomcat(), + webapp.getMetadata().getNamespace()); + + InformerConfiguration configuration = + InformerConfiguration.from(context, Tomcat.class) + .withPrimaryResourcesRetriever(webappsMatchingTomcatName) + .withAssociatedSecondaryResourceIdentifier(tomcatFromWebAppSpec) + .build(); + return List.of(new InformerEventSource<>(configuration, context)); } /** @@ -144,7 +159,7 @@ private String[] executeCommandInAllPods( CompletableFuture data = new CompletableFuture<>(); try (ExecWatch execWatch = execCmd(pod, data, command)) { - status[i] = "" + pod.getMetadata().getName() + ":" + data.get(30, TimeUnit.SECONDS);; + status[i] = "" + pod.getMetadata().getName() + ":" + data.get(30, TimeUnit.SECONDS); } catch (ExecutionException e) { status[i] = "" + pod.getMetadata().getName() + ": ExecutionException - " + e.getMessage(); } catch (InterruptedException e) { @@ -194,7 +209,7 @@ public void onFailure(Throwable t, Response response) { @Override public void onClose(int code, String reason) { - log.debug("Exit with: " + code + " and with reason: " + reason); + log.debug("Exit with: {} and with reason: {}", code, reason); data.complete(baos.toString()); } } diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml index 55d1a6be7d..aa38eb3619 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -5,11 +5,6 @@ metadata: labels: app.kubernetes.io/part-of: "" app.kubernetes.io/managed-by: "" # used for filtering of Deployments created by the controller - ownerReferences: # used for finding which Tomcat does this Deployment belong to - - apiVersion: apps/v1 - kind: Tomcat - name: "" - uid: "" spec: selector: matchLabels: diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml index a807d277a7..ab198643ed 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -2,11 +2,6 @@ apiVersion: v1 kind: Service metadata: name: "" - ownerReferences: # used for finding which Tomcat does this Deployment belong to - - apiVersion: apps/v1 - kind: Tomcat - name: "" - uid: "" spec: selector: app: "" diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index e803b70aba..9f80f31c5b 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -33,7 +33,7 @@ public void test() { // Use this if you want to run the test without deploying the Operator to Kubernetes if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new TomcatReconciler(client)); + operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); operator.start(); } diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 90522733af..94e4dbcb83 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT webpage @@ -25,7 +25,6 @@ io.javaoperatorsdk operator-framework - 2.0.3-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index db2926c74b..96cd79f960 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 3dcdb530c7..02aae34d54 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 87ccbbf48d..0b26c7de42 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index b652916960..c41e3c1cfc 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 3986343f2398663b25dac8033aa52fd6bc3a62b5 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 25 Jan 2022 13:47:13 +0100 Subject: [PATCH 0002/1319] feat: add @Ignore annotation to mark internal Reconciler implementations (#863) This allows downstream tools to safely ignore the annotated Reconciler when considering which implementations to process. Fixes #862 --- .../operator/api/reconciler/Ignore.java | 17 +++++++++++++++++ .../operator/processing/Controller.java | 2 ++ .../dependent/DependentResourceManager.java | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java new file mode 100644 index 0000000000..ad2a755db8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation for downstream tooling to ignore the annotated {@link Reconciler}. This allows to + * mark some implementations as not provided by user and should therefore be ignored by processes + * external to the SDK itself. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Ignore { + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index e0b9efae77..be819c5621 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -27,6 +27,7 @@ import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.dependent.DependentResourceManager; @@ -34,6 +35,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) +@Ignore public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 655113d251..30e92e3d63 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; @@ -23,6 +24,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) +@Ignore public class DependentResourceManager implements EventSourceInitializer, EventSourceContextInjector, Reconciler { From d38dfb79ed02db3929dbfffc15f8cdd6c3f16df6 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 28 Jan 2022 13:04:31 +0100 Subject: [PATCH 0003/1319] refactor: clean up configurations (#876) --- .../DefaultControllerConfiguration.java | 43 +++---------------- .../config/DefaultResourceConfiguration.java | 3 +- ...ernetesDependentResourceConfiguration.java | 38 ++++++++++++++++ ...KubernetesDependentResourceController.java | 12 +++--- .../informer/InformerConfiguration.java | 2 +- .../runtime/AnnotationConfiguration.java | 4 +- 6 files changed, 55 insertions(+), 47 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index bc10002ebd..0a3c68c70b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -8,6 +8,7 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration + extends DefaultResourceConfiguration implements ControllerConfiguration { private final String associatedControllerClassName; @@ -15,14 +16,9 @@ public class DefaultControllerConfiguration private final String crdName; private final String finalizer; private final boolean generationAware; - private final Set namespaces; - private final boolean watchAllNamespaces; private final RetryConfiguration retryConfiguration; - private final String labelSelector; private final ResourceEventFilter resourceEventFilter; - private final Class resourceClass; private final List dependents; - private ConfigurationService service; // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( @@ -38,23 +34,18 @@ public DefaultControllerConfiguration( Class resourceClass, ConfigurationService service, List dependents) { + super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; this.finalizer = finalizer; this.generationAware = generationAware; - this.namespaces = - namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); - this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; - this.labelSelector = labelSelector; this.resourceEventFilter = resourceEventFilter; - this.resourceClass = - resourceClass == null ? ControllerConfiguration.super.getResourceClass() - : resourceClass; + setConfigurationService(service); this.dependents = dependents != null ? dependents : Collections.emptyList(); } @@ -84,43 +75,19 @@ public String getAssociatedReconcilerClassName() { return associatedControllerClassName; } - @Override - public Set getNamespaces() { - return namespaces; - } - - @Override - public boolean watchAllNamespaces() { - return watchAllNamespaces; - } - @Override public RetryConfiguration getRetryConfiguration() { return retryConfiguration; } - @Override - public ConfigurationService getConfigurationService() { - return service; - } @Override public void setConfigurationService(ConfigurationService service) { - if (this.service != null) { + if (getConfigurationService() != null) { throw new IllegalStateException("A ConfigurationService is already associated with '" + name + "' ControllerConfiguration. Cannot change it once set!"); } - this.service = service; - } - - @Override - public String getLabelSelector() { - return labelSelector; - } - - @Override - public Class getResourceClass() { - return resourceClass; + super.setConfigurationService(service); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java index d7b5f76815..7b80416433 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -23,7 +23,8 @@ public DefaultResourceConfiguration(String labelSelector, Class resourceClass public DefaultResourceConfiguration(String labelSelector, Class resourceClass, Set namespaces) { this.labelSelector = labelSelector; - this.resourceClass = resourceClass; + this.resourceClass = resourceClass == null ? ResourceConfiguration.super.getResourceClass() + : resourceClass; this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); this.watchAllNamespaces = this.namespaces.isEmpty(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java new file mode 100644 index 0000000000..dad9411d2d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; + +public class KubernetesDependentResourceConfiguration + extends InformerConfiguration { + + private final boolean owned; + + protected KubernetesDependentResourceConfiguration( + ConfigurationService service, + String labelSelector, Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned) { + super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, + skipUpdateEventPropagationIfNoChange, namespaces); + this.owned = owned; + } + + public static KubernetesDependentResourceConfiguration from( + InformerConfiguration cfg, boolean owned) { + return new KubernetesDependentResourceConfiguration<>(cfg.getConfigurationService(), + cfg.getLabelSelector(), cfg.getResourceClass(), cfg.getPrimaryResourcesRetriever(), + cfg.getAssociatedResourceIdentifier(), cfg.isSkipUpdateEventPropagationIfNoChange(), + cfg.getNamespaces(), owned); + } + + public boolean isOwned() { + return owned; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java index da11c4f938..015b60f4bb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java @@ -13,14 +13,13 @@ public class KubernetesDependentResourceController extends DependentResourceController { - private final InformerConfiguration configuration; - private final boolean owned; + private final KubernetesDependentResourceConfiguration configuration; private KubernetesClient client; private InformerEventSource informer; public KubernetesDependentResourceController(DependentResource delegate, - InformerConfiguration configuration, boolean owned) { + KubernetesDependentResourceConfiguration configuration) { super(delegate); // todo: check if we can validate that types actually match properly final var associatedPrimaries = @@ -32,11 +31,12 @@ public KubernetesDependentResourceController(DependentResource delegate, ? (AssociatedSecondaryResourceIdentifier

) delegate : configuration.getAssociatedResourceIdentifier(); - this.configuration = InformerConfiguration.from(configuration) + final var augmented = InformerConfiguration.from(configuration) .withPrimaryResourcesRetriever(associatedPrimaries) .withAssociatedSecondaryResourceIdentifier(associatedSecondary) .build(); - this.owned = owned; + this.configuration = + KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned()); } @Override @@ -69,6 +69,6 @@ public R getFor(P primary, Context context) { } public boolean owned() { - return owned; + return configuration.isOwned(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java index 10bfdcdcb8..0c06698327 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -19,7 +19,7 @@ public class InformerConfiguration private final AssociatedSecondaryResourceIdentifier

associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; - private InformerConfiguration(ConfigurationService service, String labelSelector, + protected InformerConfiguration(ConfigurationService service, String labelSelector, Class resourceClass, PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, AssociatedSecondaryResourceIdentifier

associatedWith, diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 89a100741f..30d2600cfe 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.api.config.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -153,7 +154,8 @@ public List getDependentResources() { .skippingEventPropagationIfUnchanged(skipIfUnchanged) .withNamespaces(namespaces) .build(); - dependent = new KubernetesDependentResourceController(dependent, configuration, owned); + dependent = new KubernetesDependentResourceController(dependent, + KubernetesDependentResourceConfiguration.from(configuration, owned)); } dependents.add(dependent); From eeea472d487762a5cde340433dcac1e4c7201ae2 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 31 Jan 2022 18:21:31 +0100 Subject: [PATCH 0004/1319] fix: also run full tests on `next` branch pushes and PRs (#884) --- .github/workflows/e2e-test-mysql.yml | 3 ++- .github/workflows/e2e-test-tomcat.yml | 3 ++- .github/workflows/pr.yml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml index 07fe6e3189..91edea16cb 100644 --- a/.github/workflows/e2e-test-mysql.yml +++ b/.github/workflows/e2e-test-mysql.yml @@ -3,10 +3,11 @@ name: MySQL Schema Operator End to End test on: pull_request: - branches: [ main, v1 ] + branches: [ main, v1, next ] push: branches: - main + - next jobs: mysql_e2e_test: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-test-tomcat.yml b/.github/workflows/e2e-test-tomcat.yml index 70e01660d0..4f5e5e820b 100644 --- a/.github/workflows/e2e-test-tomcat.yml +++ b/.github/workflows/e2e-test-tomcat.yml @@ -3,10 +3,11 @@ name: Tomcat Operator End to End test on: pull_request: - branches: [ main, v1 ] + branches: [ main, v1, next ] push: branches: - main + - next jobs: tomcat_e2e_test: runs-on: ubuntu-latest diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f46bd258f7..f21457e61d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: [ main, v1 ] + branches: [ main, v1, next ] workflow_dispatch: jobs: build: From 45b187dc302ccfea098215266a80b494c3942954 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 31 Jan 2022 22:21:49 +0100 Subject: [PATCH 0005/1319] fix: decouple DependentResource creation from configuration (#883) The idea is to delay instantiation until needed and rely on DependentResourceControllerFactory for this purpose instead. Not sure if we actually need to use an interface there or not. --- .../api/config/ControllerConfiguration.java | 2 +- .../DefaultControllerConfiguration.java | 6 +- .../config/DefaultResourceConfiguration.java | 3 +- .../DependentResourceConfiguration.java | 10 ++ .../DependentResourceController.java | 11 ++- .../DependentResourceControllerFactory.java | 24 +++-- ...ernetesDependentResourceConfiguration.java | 65 +++++++++---- ...KubernetesDependentResourceController.java | 10 +- .../dependent/DependentResourceManager.java | 9 +- .../informer/InformerConfiguration.java | 92 +++++++++++-------- .../runtime/AnnotationConfiguration.java | 55 +++++------ 11 files changed, 181 insertions(+), 106 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index d4739f8267..7e44a45de9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -48,7 +48,7 @@ default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } - default List getDependentResources() { + default List getDependentResources() { return Collections.emptyList(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 0a3c68c70b..25ff5a5315 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -18,7 +18,7 @@ public class DefaultControllerConfiguration private final boolean generationAware; private final RetryConfiguration retryConfiguration; private final ResourceEventFilter resourceEventFilter; - private final List dependents; + private final List dependents; // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( @@ -33,7 +33,7 @@ public DefaultControllerConfiguration( ResourceEventFilter resourceEventFilter, Class resourceClass, ConfigurationService service, - List dependents) { + List dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -96,7 +96,7 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { + public List getDependentResources() { return dependents; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java index 7b80416433..d7b5f76815 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -23,8 +23,7 @@ public DefaultResourceConfiguration(String labelSelector, Class resourceClass public DefaultResourceConfiguration(String labelSelector, Class resourceClass, Set namespaces) { this.labelSelector = labelSelector; - this.resourceClass = resourceClass == null ? ResourceConfiguration.super.getResourceClass() - : resourceClass; + this.resourceClass = resourceClass; this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); this.watchAllNamespaces = this.namespaces.isEmpty(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java new file mode 100644 index 0000000000..15911e0418 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface DependentResourceConfiguration { + + Class> getDependentResourceClass(); + + Class getResourceClass(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java index 043aec2f53..21cd16a32f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java @@ -2,11 +2,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -public class DependentResourceController +public class DependentResourceController> implements DependentResource, Builder, Updater, Persister, Cleaner { @@ -15,14 +16,16 @@ public class DependentResourceController private final Cleaner cleaner; private final Persister persister; private final DependentResource delegate; + private final C configuration; @SuppressWarnings("unchecked") - public DependentResourceController(DependentResource delegate) { + public DependentResourceController(DependentResource delegate, C configuration) { this.delegate = delegate; builder = (delegate instanceof Builder) ? (Builder) delegate : null; updater = (delegate instanceof Updater) ? (Updater) delegate : null; cleaner = (delegate instanceof Cleaner) ? (Cleaner) delegate : null; persister = initPersister(delegate); + this.configuration = configuration; } @SuppressWarnings("unchecked") @@ -84,4 +87,8 @@ public void createOrReplace(R dependentResource, Context context) { public R getFor(P primary, Context context) { return persister.getFor(primary, context); } + + public C getConfiguration() { + return configuration; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java index f814cd7a1a..dd11a7a706 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java @@ -1,14 +1,26 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import java.lang.reflect.InvocationTargetException; + import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; public interface DependentResourceControllerFactory

{ - default DependentResourceController from(DependentResource dependent) { - // todo: this needs to be cleaned-up / redesigned - return dependent instanceof DependentResourceController - ? (DependentResourceController) dependent - : new DependentResourceController<>(dependent); + default , R, C extends DependentResourceConfiguration> T from( + C config) { + try { + final var dependentResource = config.getDependentResourceClass().getConstructor() + .newInstance(); + if (config instanceof KubernetesDependentResourceConfiguration) { + return (T) new KubernetesDependentResourceController(dependentResource, + (KubernetesDependentResourceConfiguration) config); + } else { + return (T) new DependentResourceController(dependentResource, config); + } + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException + | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java index dad9411d2d..23d37881b4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java @@ -4,35 +4,64 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; -public class KubernetesDependentResourceConfiguration - extends InformerConfiguration { +public interface KubernetesDependentResourceConfiguration + extends InformerConfiguration, DependentResourceConfiguration { - private final boolean owned; + class DefaultKubernetesDependentResourceConfiguration + extends DefaultInformerConfiguration + implements KubernetesDependentResourceConfiguration { - protected KubernetesDependentResourceConfiguration( - ConfigurationService service, - String labelSelector, Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned) { - super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, - skipUpdateEventPropagationIfNoChange, namespaces); - this.owned = owned; + private final boolean owned; + private final Class> dependentResourceClass; + + protected DefaultKubernetesDependentResourceConfiguration( + ConfigurationService service, + String labelSelector, Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned, + Class> dependentResourceClass) { + super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, + skipUpdateEventPropagationIfNoChange, namespaces); + this.owned = owned; + this.dependentResourceClass = dependentResourceClass; + } + + public boolean isOwned() { + return owned; + } + + @Override + public Class> getDependentResourceClass() { + return dependentResourceClass; + } + + @Override + public Class getResourceClass() { + return super.getResourceClass(); + } } - public static KubernetesDependentResourceConfiguration from( - InformerConfiguration cfg, boolean owned) { - return new KubernetesDependentResourceConfiguration<>(cfg.getConfigurationService(), + static KubernetesDependentResourceConfiguration from( + InformerConfiguration cfg, boolean owned, + Class dependentResourceClass) { + return new DefaultKubernetesDependentResourceConfiguration(cfg.getConfigurationService(), cfg.getLabelSelector(), cfg.getResourceClass(), cfg.getPrimaryResourcesRetriever(), cfg.getAssociatedResourceIdentifier(), cfg.isSkipUpdateEventPropagationIfNoChange(), - cfg.getNamespaces(), owned); + cfg.getNamespaces(), owned, + (Class>) dependentResourceClass); } - public boolean isOwned() { - return owned; + boolean isOwned(); + + @Override + default Class getResourceClass() { + return InformerConfiguration.super.getResourceClass(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java index 015b60f4bb..172e9cb0d9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java @@ -12,7 +12,8 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; public class KubernetesDependentResourceController - extends DependentResourceController { + extends DependentResourceController> { + private final KubernetesDependentResourceConfiguration configuration; private KubernetesClient client; private InformerEventSource informer; @@ -20,7 +21,7 @@ public class KubernetesDependentResourceController delegate, KubernetesDependentResourceConfiguration configuration) { - super(delegate); + super(delegate, configuration); // todo: check if we can validate that types actually match properly final var associatedPrimaries = (delegate instanceof PrimaryResourcesRetriever) @@ -36,7 +37,8 @@ public KubernetesDependentResourceController(DependentResource delegate, .withAssociatedSecondaryResourceIdentifier(associatedSecondary) .build(); this.configuration = - KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned()); + KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), + configuration.getDependentResourceClass()); } @Override @@ -69,6 +71,6 @@ public R getFor(P primary, Context context) { } public boolean owned() { - return configuration.isOwned(); + return getConfiguration().isOwned(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 30e92e3d63..911805b396 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -8,7 +8,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -42,13 +42,14 @@ public DependentResourceManager(Controller controller) { @Override public List prepareEventSources(EventSourceContext context) { - final List configured = configuration.getDependentResources(); + final List configured = configuration.getDependentResources(); dependents = new ArrayList<>(configured.size()); List sources = new ArrayList<>(configured.size() + 5); configured.forEach(dependent -> { - dependents.add(configuration.dependentFactory().from(dependent)); - sources.add(dependent.initEventSource(context)); + final var dependentResourceController = configuration.dependentFactory().from(dependent); + dependents.add(dependentResourceController); + sources.add(dependentResourceController.initEventSource(context)); }); return sources; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java index 0c06698327..49b7ae7654 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -7,45 +7,57 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -public class InformerConfiguration - extends DefaultResourceConfiguration { - - private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private final AssociatedSecondaryResourceIdentifier

associatedWith; - private final boolean skipUpdateEventPropagationIfNoChange; - - protected InformerConfiguration(ConfigurationService service, String labelSelector, - Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { - super(labelSelector, resourceClass, namespaces); - setConfigurationService(service); - this.secondaryToPrimaryResourcesIdSet = - Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, Mappers.fromOwnerReference()); - this.associatedWith = - Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; - } +public interface InformerConfiguration + extends ResourceConfiguration { + + class DefaultInformerConfiguration extends + DefaultResourceConfiguration implements InformerConfiguration { + + private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private final AssociatedSecondaryResourceIdentifier

associatedWith; + private final boolean skipUpdateEventPropagationIfNoChange; + + protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, + Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { + super(labelSelector, resourceClass, namespaces); + setConfigurationService(service); + this.secondaryToPrimaryResourcesIdSet = + Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, + Mappers.fromOwnerReference()); + this.associatedWith = + Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); + this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; + } - public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { - return secondaryToPrimaryResourcesIdSet; - } + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + return secondaryToPrimaryResourcesIdSet; + } - public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { - return associatedWith; - } + public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { + return associatedWith; + } - public boolean isSkipUpdateEventPropagationIfNoChange() { - return skipUpdateEventPropagationIfNoChange; + public boolean isSkipUpdateEventPropagationIfNoChange() { + return skipUpdateEventPropagationIfNoChange; + } } - public static class InformerConfigurationBuilder { + PrimaryResourcesRetriever getPrimaryResourcesRetriever(); + + AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier(); + + boolean isSkipUpdateEventPropagationIfNoChange(); + + class InformerConfigurationBuilder { private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; private AssociatedSecondaryResourceIdentifier

associatedWith; @@ -101,32 +113,32 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector } public InformerConfiguration build() { - return new InformerConfiguration<>(configurationService, labelSelector, resourceClass, + return new DefaultInformerConfiguration<>(configurationService, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, skipUpdateEventPropagationIfNoChange, namespaces); } } - public static InformerConfigurationBuilder from( + static InformerConfigurationBuilder from( EventSourceContext

context, Class resourceClass) { return new InformerConfigurationBuilder<>(resourceClass, context.getConfigurationService()); } - public static InformerConfigurationBuilder from(ConfigurationService configurationService, + static InformerConfigurationBuilder from(ConfigurationService configurationService, Class resourceClass) { return new InformerConfigurationBuilder<>(resourceClass, configurationService); } - public static InformerConfigurationBuilder from( + static InformerConfigurationBuilder from( InformerConfiguration configuration) { return new InformerConfigurationBuilder(configuration.getResourceClass(), configuration.getConfigurationService()) - .withNamespaces(configuration.getNamespaces()) - .withLabelSelector(configuration.getLabelSelector()) - .skippingEventPropagationIfUnchanged( - configuration.isSkipUpdateEventPropagationIfNoChange()) - .withAssociatedSecondaryResourceIdentifier( - configuration.getAssociatedResourceIdentifier()) - .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .skippingEventPropagationIfUnchanged( + configuration.isSkipUpdateEventPropagationIfNoChange()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 30d2600cfe..1ffde6de36 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.config.runtime; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -12,11 +11,11 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Dependent; import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; @@ -28,7 +27,7 @@ public class AnnotationConfiguration private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; - private List dependents; + private List dependentConfigurations; public AnnotationConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -121,27 +120,20 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { - if (dependents == null) { - final var dependentConfigs = valueOrDefault(annotation, - ControllerConfiguration::dependents, new Dependent[] {}); - if (dependentConfigs.length > 0) { - dependents = new ArrayList<>(dependentConfigs.length); - for (Dependent dependentConfig : dependentConfigs) { - final Class dependentType = dependentConfig.type(); - DependentResource dependent; - try { - dependent = dependentType.getConstructor().newInstance(); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException - | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } + public List getDependentResources() { + if (dependentConfigurations == null) { + final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, + new Dependent[]{}); + if (dependents.length > 0) { + dependentConfigurations = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + final var resourceType = dependent.resourceType(); - final var resourceType = dependentConfig.resourceType(); if (HasMetadata.class.isAssignableFrom(resourceType)) { final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[]{}); final var labelSelector = valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, @@ -154,17 +146,28 @@ public List getDependentResources() { .skippingEventPropagationIfUnchanged(skipIfUnchanged) .withNamespaces(namespaces) .build(); - dependent = new KubernetesDependentResourceController(dependent, - KubernetesDependentResourceConfiguration.from(configuration, owned)); - } - dependents.add(dependent); + dependentConfigurations.add( + KubernetesDependentResourceConfiguration.from(configuration, owned, dependentType)); + } else { + dependentConfigurations.add(new DependentResourceConfiguration() { + @Override + public Class getDependentResourceClass() { + return dependentType; + } + + @Override + public Class getResourceClass() { + return resourceType; + } + }); + } } } else { - dependents = Collections.emptyList(); + dependentConfigurations = Collections.emptyList(); } } - return dependents; + return dependentConfigurations; } private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { From eeed3237936ebced5a53f0e65140afb01659a53c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Feb 2022 09:43:02 +0100 Subject: [PATCH 0006/1319] fix: NPE when no namespace is provided in kube config (#899) * fix: fail explicitly if current namespace is requested but not available Fixes #897 * fix: retain annotation for reflective access --- .../api/config/KubernetesDependent.java | 3 ++ .../api/config/ResourceConfiguration.java | 8 ++++- .../operator/processing/Controller.java | 35 ++++++++----------- .../informer/InformerConfiguration.java | 14 ++++---- .../runtime/AnnotationConfiguration.java | 4 +-- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java index 587fd58f85..5b9fbd38f3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java @@ -1,10 +1,13 @@ package io.javaoperatorsdk.operator.api.config; import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; +@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface KubernetesDependent { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java index 1e1d58b917..3ebc3ae546 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -4,6 +4,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -67,7 +68,12 @@ default Set getEffectiveNamespaces() { throw new IllegalStateException( "Parent ConfigurationService must be set before calling this method"); } - targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); + String namespace = parent.getClientConfiguration().getNamespace(); + if (namespace == null) { + throw new OperatorException( + "Couldn't retrieve the currently connected namespace. Make sure it's correctly set in your ~/.kube/config file, using, e.g. 'kubectl config set-context --namespace='"); + } + targetNamespaces = Collections.singleton(namespace); } return targetNamespaces; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index be819c5621..f3ad2dd8ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -2,7 +2,6 @@ import java.util.LinkedList; import java.util.List; -import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +33,7 @@ import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -@SuppressWarnings({"rawtypes", "unchecked"}) +@SuppressWarnings({"unchecked"}) @Ignore public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { @@ -195,6 +194,10 @@ public void start() throws OperatorException { final var specVersion = "v1"; log.info("Starting '{}' controller for reconciler: {}, resource: {}", controllerName, reconciler.getClass().getCanonicalName(), resClass.getCanonicalName()); + + // fail early if we're missing the current namespace information + failOnMissingCurrentNS(); + try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config @@ -210,13 +213,6 @@ public void start() throws OperatorException { CustomResourceUtils.assertCustomResource(resClass, crd); } - if (failOnMissingCurrentNS()) { - throw new OperatorException( - "Controller '" - + controllerName - + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); - } - final var context = new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource().getResourceCache(), configurationService(), kubernetesClient); @@ -263,19 +259,18 @@ private void throwMissingCRDException(String crdName, String specVersion, String } /** - * Determines whether we should fail because the current namespace is request as target namespace - * but is missing - * - * @return {@code true} if the current namespace is requested but is missing, {@code false} - * otherwise + * Throws an {@link OperatorException} if the controller is configured to watch the current + * namespace but it's absent from the configuration. */ - private boolean failOnMissingCurrentNS() { - if (configuration.watchCurrentNamespace()) { - final var effectiveNamespaces = configuration.getEffectiveNamespaces(); - return effectiveNamespaces.size() == 1 - && effectiveNamespaces.stream().allMatch(Objects::isNull); + private void failOnMissingCurrentNS() { + try { + configuration.getEffectiveNamespaces(); + } catch (OperatorException e) { + throw new OperatorException( + "Controller '" + + configuration.getName() + + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } - return false; } public EventSourceManager getEventSourceManager() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java index 49b7ae7654..087e206f8b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -133,12 +133,12 @@ static InformerConfigurationBuild InformerConfiguration configuration) { return new InformerConfigurationBuilder(configuration.getResourceClass(), configuration.getConfigurationService()) - .withNamespaces(configuration.getNamespaces()) - .withLabelSelector(configuration.getLabelSelector()) - .skippingEventPropagationIfUnchanged( - configuration.isSkipUpdateEventPropagationIfNoChange()) - .withAssociatedSecondaryResourceIdentifier( - configuration.getAssociatedResourceIdentifier()) - .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .skippingEventPropagationIfUnchanged( + configuration.isSkipUpdateEventPropagationIfNoChange()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 1ffde6de36..3e0721b8a0 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -123,7 +123,7 @@ public ResourceEventFilter getEventFilter() { public List getDependentResources() { if (dependentConfigurations == null) { final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, - new Dependent[]{}); + new Dependent[] {}); if (dependents.length > 0) { dependentConfigurations = new ArrayList<>(dependents.length); for (Dependent dependent : dependents) { @@ -133,7 +133,7 @@ public List getDependentResources() { if (HasMetadata.class.isAssignableFrom(resourceType)) { final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[]{}); + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); final var labelSelector = valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, From b282d4e5783247e055a7c2a3630fd1062953e274 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 4 Feb 2022 13:38:38 +0100 Subject: [PATCH 0007/1319] fix: version update to 2.2.0-SNAPSHOT --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 73c8e7ed7d..11f70ef4ca 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a1804086d6..79ccc716df 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 6f66b97810..9f8f4e5c01 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 8e7ff3fe8f..a624af4e23 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index fbca5b3ae2..6d7905118c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 64be615e03..c8932f902f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index e5819f32a8..666e471240 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 5c75031b16..cee6935745 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 94e4dbcb83..566ec8b314 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT webpage diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 96cd79f960..6dcdded08b 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 02aae34d54..ffbe7c659b 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 0b26c7de42..21b8162183 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index c41e3c1cfc..51860f7af8 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From b5098c429c421a2e5ddedca760540b96f3e08917 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 4 Feb 2022 14:10:15 +0100 Subject: [PATCH 0008/1319] fix: merge issues from master --- .../operator/api/config/ControllerConfiguration.java | 2 -- .../operator/api/config/DefaultControllerConfiguration.java | 5 +---- .../io/javaoperatorsdk/operator/ControllerManagerTest.java | 2 +- .../processing/event/source/ResourceEventFilterTest.java | 6 +----- .../controller/ControllerResourceEventSourceTest.java | 2 +- .../operator/config/runtime/AnnotationConfiguration.java | 2 +- 6 files changed, 5 insertions(+), 14 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 51218effa0..717e339514 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -1,11 +1,9 @@ package io.javaoperatorsdk.operator.api.config; -import java.lang.reflect.ParameterizedType; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 82db2a4af7..41bacbbbce 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -2,8 +2,8 @@ import java.time.Duration; import java.util.Collections; -import java.util.Optional; import java.util.List; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -44,10 +44,7 @@ public DefaultControllerConfiguration( this.crdName = crdName; this.finalizer = finalizer; this.generationAware = generationAware; - this.namespaces = - namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); this.reconciliationMaxInterval = reconciliationMaxInterval; - this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null ? ControllerConfiguration.super.getRetryConfiguration() diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index ca02094d46..910492a47f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -63,7 +63,7 @@ private static class TestControllerConfiguration public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, - null, null); + null, null, null); this.controller = controller; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index 734a89434e..0dfb2d09d7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -7,11 +7,7 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -144,7 +140,7 @@ public ControllerConfig(String finalizer, boolean generationAware, eventFilter, customResourceClass, null, - null); + null, null); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index f67ea44a5d..83528256df 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -163,7 +163,7 @@ public TestConfiguration(boolean generationAware) { null, TestCustomResource.class, null, - null); + null, null); } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index d8b9e99dae..5d10762f35 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.time.Duration; import java.util.Optional; import java.util.Set; import java.util.function.Function; From 6ee3173854381d9aeb51fa111c6abbdcd3478dc1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Feb 2022 15:18:33 +0100 Subject: [PATCH 0009/1319] refactor: clean-up dependent implementation (#906) * feat: add desired and matches methods on DependentResource This removes the need for Builder and Updater interfaces. * refactor: move classes to more coherent locations * refactor: remove DependentResourceControllerFactory * refactor: remove Cleaner interface * refactor: put the reconciling logic in DependentResourceController * refactor: return DependentResource.desired now returns Optional * chore: add @Ignore to mark internal Reconciler implementations --- .../api/config/ControllerConfiguration.java | 6 +- .../DefaultControllerConfiguration.java | 1 + .../operator/api/config/Dependent.java | 8 -- .../api/config/DependentResource.java | 15 -- .../api/config/dependent/Dependent.java | 10 ++ .../DependentResourceConfiguration.java | 3 +- .../{ => dependent}/KubernetesDependent.java | 2 +- ...ernetesDependentResourceConfiguration.java | 7 +- .../informer/InformerConfiguration.java | 3 +- .../reconciler/ControllerConfiguration.java | 4 +- .../api/reconciler/dependent/Builder.java | 9 -- .../api/reconciler/dependent/Cleaner.java | 9 -- .../dependent/DependentResource.java | 62 ++++++++ .../DependentResourceController.java | 94 ------------ .../DependentResourceControllerFactory.java | 26 ---- .../api/reconciler/dependent/Updater.java | 10 -- .../DependentResourceController.java | 134 ++++++++++++++++++ .../dependent/DependentResourceManager.java | 110 +++++--------- ...KubernetesDependentResourceController.java | 20 ++- .../source/informer/InformerEventSource.java | 1 + .../runtime/AnnotationConfiguration.java | 15 +- ...formerEventSourceTestCustomReconciler.java | 4 +- .../sample/MySQLSchemaReconciler.java | 23 ++- .../sample/SchemaDependentResource.java | 12 +- .../sample/DeploymentDependentResource.java | 28 ++-- .../sample/ServiceDependentResource.java | 30 ++-- .../sample/TomcatDependentResource.java | 2 +- .../operator/sample/TomcatReconciler.java | 2 +- .../operator/sample/WebappReconciler.java | 2 +- 29 files changed, 327 insertions(+), 325 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/{ => dependent}/DependentResourceConfiguration.java (63%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/{ => dependent}/KubernetesDependent.java (95%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{reconciler => config}/dependent/KubernetesDependentResourceConfiguration.java (90%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{processing/event/source => api/config}/informer/InformerConfiguration.java (97%) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/KubernetesDependentResourceController.java (80%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 717e339514..c077a7876d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -7,8 +7,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceControllerFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -54,10 +54,6 @@ default List getDependentResources() { return Collections.emptyList(); } - default DependentResourceControllerFactory dependentFactory() { - return new DependentResourceControllerFactory<>() {}; - } - default Optional reconciliationMaxInterval() { return Optional.of(Duration.ofHours(10L)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 41bacbbbce..188311cfdb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -7,6 +7,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java deleted file mode 100644 index ed013fafb0..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -public @interface Dependent { - - Class resourceType(); - - Class type(); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java deleted file mode 100644 index f4d5df89fb..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -public interface DependentResource { - default EventSource initEventSource(EventSourceContext

context) { - throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); - }; - - default Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java new file mode 100644 index 0000000000..017f4363a6 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +public @interface Dependent { + + Class resourceType(); + + Class type(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java similarity index 63% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java index 15911e0418..c0f428c12d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java @@ -1,6 +1,7 @@ -package io.javaoperatorsdk.operator.api.config; +package io.javaoperatorsdk.operator.api.config.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; public interface DependentResourceConfiguration { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java similarity index 95% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java index 5b9fbd38f3..e7d2ad3158 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.config; +package io.javaoperatorsdk.operator.api.config.dependent; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java similarity index 90% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java index 23d37881b4..6872322e4f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java @@ -1,14 +1,13 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.config.dependent; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; public interface KubernetesDependentResourceConfiguration extends InformerConfiguration, DependentResourceConfiguration { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java similarity index 97% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 087e206f8b..c4d747a990 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source.informer; +package io.javaoperatorsdk.operator.api.config.informer; import java.util.Collections; import java.util.Objects; @@ -12,6 +12,7 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public interface InformerConfiguration extends ResourceConfiguration { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index 4db09c9cc8..85eed38025 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -6,8 +6,8 @@ import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.dependent.DependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java deleted file mode 100644 index 4dfaf91e64..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -@FunctionalInterface -public interface Builder { - R buildFor(P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java deleted file mode 100644 index a6a3d691be..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface Cleaner { - - void delete(R fetched, P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java new file mode 100644 index 0000000000..df144ade1f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public interface DependentResource { + default EventSource initEventSource(EventSourceContext

context) { + throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); + } + + @SuppressWarnings("unchecked") + default Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } + + default void delete(R fetched, P primary, Context context) {} + + /** + * Computes the desired state of the dependent based on the state provided by the specified + * primary resource. + * + * The default implementation returns {@code empty} which corresponds to the case where the + * associated dependent should never be created by the associated reconciler or that the global + * state of the cluster doesn't allow for the resource to be created at this point. + * + * @param primary the primary resource associated with the reconciliation process + * @param context the {@link Context} associated with the reconciliation process + * @return an instance of the dependent resource matching the desired state specified by the + * primary resource or {@code empty} if the dependent shouldn't be created at this point + * (or ever) + */ + default Optional desired(P primary, Context context) { + return Optional.empty(); + } + + /** + * Checks whether the actual resource as fetched from the cluster matches the desired state + * expressed by the specified primary resource. + * + * The default implementation always return {@code true}, which corresponds to the behavior where + * the dependent never needs to be updated after it's been created. + * + * Note that failure to properly implement this method will lead to infinite loops. In particular, + * for typical Kubernetes resource implementations, simply calling + * {@code desired(primary, context).equals(actual)} is not enough because metadata will usually be + * different. + * + * @param actual the current state of the resource as fetched from the cluster + * @param primary the primary resource associated with the reconciliation request + * @param context the {@link Context} associated with the reconciliation request + * @return {@code true} if the actual state of the resource matches the desired state expressed by + * the specified primary resource, {@code false} otherwise + */ + default boolean match(R actual, P primary, Context context) { + return true; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java deleted file mode 100644 index 21cd16a32f..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -public class DependentResourceController> - implements DependentResource, Builder, Updater, Persister, - Cleaner { - - private final Builder builder; - private final Updater updater; - private final Cleaner cleaner; - private final Persister persister; - private final DependentResource delegate; - private final C configuration; - - @SuppressWarnings("unchecked") - public DependentResourceController(DependentResource delegate, C configuration) { - this.delegate = delegate; - builder = (delegate instanceof Builder) ? (Builder) delegate : null; - updater = (delegate instanceof Updater) ? (Updater) delegate : null; - cleaner = (delegate instanceof Cleaner) ? (Cleaner) delegate : null; - persister = initPersister(delegate); - this.configuration = configuration; - } - - @SuppressWarnings("unchecked") - protected Persister initPersister(DependentResource delegate) { - if (delegate instanceof Persister) { - return (Persister) delegate; - } else { - throw new IllegalArgumentException( - "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); - } - } - - public String descriptionFor(R resource) { - return resource.toString(); - } - - @Override - public R buildFor(P primary, Context context) { - return builder.buildFor(primary, context); - } - - @Override - public R update(R fetched, P primary, Context context) { - return updater.update(fetched, primary, context); - } - - @Override - public void delete(R fetched, P primary, Context context) { - cleaner.delete(fetched, primary, context); - } - - public Class getResourceType() { - return delegate.resourceType(); - } - - @Override - public EventSource initEventSource(EventSourceContext

context) { - return delegate.initEventSource(context); - } - - public boolean creatable() { - return builder != null; - } - - public boolean updatable() { - return updater != null; - } - - public boolean deletable() { - return cleaner != null; - } - - @Override - public void createOrReplace(R dependentResource, Context context) { - persister.createOrReplace(dependentResource, context); - } - - @Override - public R getFor(P primary, Context context) { - return persister.getFor(primary, context); - } - - public C getConfiguration() { - return configuration; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java deleted file mode 100644 index dd11a7a706..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import java.lang.reflect.InvocationTargetException; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; - -public interface DependentResourceControllerFactory

{ - - default , R, C extends DependentResourceConfiguration> T from( - C config) { - try { - final var dependentResource = config.getDependentResourceClass().getConstructor() - .newInstance(); - if (config instanceof KubernetesDependentResourceConfiguration) { - return (T) new KubernetesDependentResourceController(dependentResource, - (KubernetesDependentResourceConfiguration) config); - } else { - return (T) new DependentResourceController(dependentResource, config); - } - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException - | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java deleted file mode 100644 index c92f3c5ced..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -@FunctionalInterface -public interface Updater { - - R update(R fetched, P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java new file mode 100644 index 0000000000..d0004111af --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java @@ -0,0 +1,134 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +@Ignore +public class DependentResourceController> + implements DependentResource, Persister, Reconciler

{ + + private static final Logger log = LoggerFactory.getLogger(DependentResourceController.class); + + private final Persister persister; + private final DependentResource delegate; + private final C configuration; + + public DependentResourceController(DependentResource delegate, C configuration) { + this.delegate = delegate; + persister = initPersister(delegate); + this.configuration = configuration; + } + + @Override + public Class resourceType() { + return delegate.resourceType(); + } + + @Override + public boolean match(R actual, P primary, Context context) { + return delegate.match(actual, primary, context); + } + + @Override + public Optional desired(P primary, Context context) { + return delegate.desired(primary, context); + } + + @Override + public void delete(R fetched, P primary, Context context) { + delegate.delete(fetched, primary, context); + } + + @SuppressWarnings("unchecked") + protected Persister initPersister(DependentResource delegate) { + if (delegate instanceof Persister) { + return (Persister) delegate; + } else { + throw new IllegalArgumentException( + "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); + } + } + + public String descriptionFor(R resource) { + return resource.toString(); + } + + public Class getResourceType() { + return delegate.resourceType(); + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + return delegate.initEventSource(context); + } + + @Override + public void createOrReplace(R dependentResource, Context context) { + persister.createOrReplace(dependentResource, context); + } + + @Override + public R getFor(P primary, Context context) { + return persister.getFor(primary, context); + } + + public C getConfiguration() { + return configuration; + } + + @Override + public UpdateControl

reconcile(P resource, Context context) { + var actual = getFor(resource, context); + if (actual == null || !match(actual, resource, context)) { + final var desired = desired(resource, context); + desired.ifPresent(d -> createOrReplaceDependent(resource, d, context)); + } + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(P primary, Context context) { + var dependent = getFor(primary, context); + if (dependent != null) { + delete(dependent, primary, context); + logOperationInfo(primary, dependent, "Deleting"); + } else { + log.info("Ignoring already deleted {} for '{}' {}", + getResourceType().getName(), + primary.getMetadata().getName(), + primary.getKind()); + } + return Reconciler.super.cleanup(primary, context); + } + + protected void createOrReplaceDependent(P primary, R dependent, Context context) { + logOperationInfo(primary, dependent, "Reconciling"); + + // commit the changes + // todo: add metrics timing for dependent resource + createOrReplace(dependent, context); + } + + private void logOperationInfo(P resource, R dependentResource, String operationDescription) { + if (log.isInfoEnabled()) { + log.info("{} {} for '{}' {}", operationDescription, + descriptionFor(dependentResource), + resource.getMetadata().getName(), + resource.getKind()); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 911805b396..8ab04d7d09 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -1,14 +1,13 @@ package io.javaoperatorsdk.operator.processing.dependent; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -18,36 +17,32 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) @Ignore -public class DependentResourceManager implements EventSourceInitializer, - EventSourceContextInjector, Reconciler { - - private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); - - private final Reconciler reconciler; - private final ControllerConfiguration configuration; +public class DependentResourceManager

implements EventSourceInitializer

, + EventSourceContextInjector, Reconciler

{ + private final Reconciler

reconciler; + private final ControllerConfiguration

configuration; private List dependents; - public DependentResourceManager(Controller controller) { + public DependentResourceManager(Controller

controller) { this.reconciler = controller.getReconciler(); this.configuration = controller.getConfiguration(); } @Override - public List prepareEventSources(EventSourceContext context) { + public List prepareEventSources(EventSourceContext

context) { final List configured = configuration.getDependentResources(); dependents = new ArrayList<>(configured.size()); List sources = new ArrayList<>(configured.size() + 5); configured.forEach(dependent -> { - final var dependentResourceController = configuration.dependentFactory().from(dependent); + final var dependentResourceController = from(dependent); dependents.add(dependentResourceController); sources.add(dependentResourceController.initEventSource(context)); }); @@ -64,80 +59,41 @@ public void injectInto(EventSourceContext context) { } @Override - public UpdateControl reconcile(R resource, Context context) { + public UpdateControl

reconcile(P resource, Context context) { initContextIfNeeded(resource, context); - - dependents.stream() - .filter(dependent -> dependent.creatable() || dependent.updatable()) - .forEach(dependent -> { - var dependentResource = dependent.getFor(resource, context); - if (dependent.creatable() && dependentResource == null) { - // we need to create the dependent - dependentResource = dependent.buildFor(resource, context); - createOrReplaceDependent(resource, context, dependent, dependentResource, "Creating"); - } else if (dependent.updatable()) { - dependentResource = dependent.update(dependentResource, resource, context); - createOrReplaceDependent(resource, context, dependent, dependentResource, "Updating"); - } else { - logOperationInfo(resource, dependent, dependentResource, "Ignoring"); - } - }); - + dependents.forEach(dependent -> dependent.reconcile(resource, context)); return UpdateControl.noUpdate(); } @Override - public DeleteControl cleanup(R resource, Context context) { + public DeleteControl cleanup(P resource, Context context) { initContextIfNeeded(resource, context); - - dependents.stream() - .filter(DependentResourceController::deletable) - .forEach(dependent -> { - var dependentResource = dependent.getFor(resource, context); - if (dependentResource != null) { - dependent.delete(dependentResource, resource, context); - logOperationInfo(resource, dependent, dependentResource, "Deleting"); - } else { - log.info("Ignoring already deleted {} for '{}' {}", - dependent.getResourceType().getName(), - resource.getMetadata().getName(), - configuration.getResourceTypeName()); - } - }); - + dependents.forEach(dependent -> dependent.cleanup(resource, context)); return Reconciler.super.cleanup(resource, context); } - private void createOrReplaceDependent(R primaryResource, - Context context, DependentResourceController dependentController, - Object dependentResource, String operationDescription) { - // add owner reference if needed - if (dependentResource instanceof HasMetadata - && ((KubernetesDependentResourceController) dependentController).owned()) { - ((HasMetadata) dependentResource).addOwnerReference(primaryResource); - } - logOperationInfo(primaryResource, dependentController, dependentResource, operationDescription); - - // commit the changes - // todo: add metrics timing for dependent resource - dependentController.createOrReplace(dependentResource, context); - } - - private void logOperationInfo(R resource, DependentResourceController dependent, - Object dependentResource, String operationDescription) { - if (log.isInfoEnabled()) { - log.info("{} {} for '{}' {}", operationDescription, - dependent.descriptionFor(dependentResource), - resource.getMetadata().getName(), - configuration.getResourceTypeName()); + private void initContextIfNeeded(P resource, Context context) { + if (reconciler instanceof ContextInitializer) { + final var initializer = (ContextInitializer

) reconciler; + initializer.initContext(resource, context); } } - private void initContextIfNeeded(R resource, Context context) { - if (reconciler instanceof ContextInitializer) { - final var initializer = (ContextInitializer) reconciler; - initializer.initContext(resource, context); + private DependentResourceController from(DependentResourceConfiguration config) { + try { + final var dependentResource = + (DependentResource) config.getDependentResourceClass().getConstructor() + .newInstance(); + if (config instanceof KubernetesDependentResourceConfiguration) { + return new KubernetesDependentResourceController(dependentResource, + (KubernetesDependentResourceConfiguration) config); + } else { + return new DependentResourceController(dependentResource, config); + } + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException + | IllegalAccessException e) { + throw new IllegalArgumentException(e); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java similarity index 80% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java index 172e9cb0d9..63d533193b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java @@ -1,16 +1,20 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +@Ignore public class KubernetesDependentResourceController extends DependentResourceController> { @@ -19,6 +23,7 @@ public class KubernetesDependentResourceController informer; + @SuppressWarnings("unchecked") public KubernetesDependentResourceController(DependentResource delegate, KubernetesDependentResourceConfiguration configuration) { super(delegate, configuration); @@ -41,6 +46,7 @@ public KubernetesDependentResourceController(DependentResource delegate, configuration.getDependentResourceClass()); } + @SuppressWarnings("unchecked") @Override protected Persister initPersister(DependentResource delegate) { return (delegate instanceof Persister) ? (Persister) delegate : this; @@ -73,4 +79,12 @@ public R getFor(P primary, Context context) { public boolean owned() { return getConfiguration().isOwned(); } + + @Override + protected void createOrReplaceDependent(P primary, R dependent, Context context) { + if (owned()) { + dependent.addOwnerReference(primary); + } + super.createOrReplaceDependent(primary, dependent, context); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index fa9e516585..3def733bd8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 5d10762f35..19e31e932b 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -11,16 +11,16 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.KubernetesDependent; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; @SuppressWarnings("rawtypes") public class AnnotationConfiguration @@ -159,7 +159,8 @@ public List getDependentResources() { if (HasMetadata.class.isAssignableFrom(resourceType)) { final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); final var labelSelector = valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 2a04b85d19..33128a6b8c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -8,12 +8,12 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index ecc2aa9be7..ff4fcad131 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -9,8 +9,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -20,7 +19,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; @@ -51,12 +50,15 @@ public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { this.mysqlDbConfig = mysqlDbConfig; } - public static class SecretDependentResource - implements DependentResource, Builder { + public static class SecretDependentResource implements DependentResource { + + private static String encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } @Override - public Secret buildFor(MySQLSchema schema, Context context) { - return new SecretBuilder() + public Optional desired(MySQLSchema schema, Context context) { + return Optional.of(new SecretBuilder() .withNewMetadata() .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) .withNamespace(schema.getMetadata().getNamespace()) @@ -65,14 +67,11 @@ public Secret buildFor(MySQLSchema schema, Context context) { context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) .addToData("MYSQL_PASSWORD", encode( context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) - .build(); - } - - private static String encode(String value) { - return Base64.getEncoder().encodeToString(value.getBytes()); + .build()); } } + @SuppressWarnings("rawtypes") @Override public void injectInto(EventSourceContext context) { context.put(MYSQL_DB_CONFIG, mysqlDbConfig); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index de4b0ece58..8ba9612029 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -3,12 +3,11 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Optional; -import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; @@ -18,8 +17,7 @@ import static java.lang.String.format; public class SchemaDependentResource - implements DependentResource, Builder, - Cleaner, Persister { + implements DependentResource, Persister { private static final int POLL_PERIOD = 500; private MySQLDbConfig dbConfig; @@ -33,7 +31,7 @@ public EventSource initEventSource(EventSourceContext context) { } @Override - public Schema buildFor(MySQLSchema primary, Context context) { + public Optional desired(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { final var schema = SchemaService.createSchemaAndRelatedUser( connection, @@ -44,7 +42,7 @@ public Schema buildFor(MySQLSchema primary, Context context) { // put the newly built schema in the context to let the reconciler know we just built it context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); - return schema; + return Optional.of(schema); } catch (SQLException e) { MySQLSchemaReconciler.log.error("Error while creating Schema", e); throw new IllegalStateException(e); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 245011a1cf..58200df988 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -1,21 +1,20 @@ package io.javaoperatorsdk.operator.sample; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.KubernetesDependent; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") public class DeploymentDependentResource - implements DependentResource, Builder, - Updater { + implements DependentResource { @Override - public Deployment buildFor(Tomcat tomcat, Context context) { + public Optional desired(Tomcat tomcat, Context context) { Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); @@ -35,18 +34,21 @@ public Deployment buildFor(Tomcat tomcat, Context context) { // make sure label selector matches label (which has to be matched by service selector too) .editMetadata().addToLabels("app", tomcatName).endMetadata() .editSpec() - .editFirstContainer().withImage("tomcat:" + tomcat.getSpec().getVersion()).endContainer() + .editFirstContainer().withImage(tomcatImage(tomcat)).endContainer() .endSpec() .endTemplate() .endSpec() .build(); - return deployment; + return Optional.of(deployment); + } + + private String tomcatImage(Tomcat tomcat) { + return "tomcat:" + tomcat.getSpec().getVersion(); } @Override - public Deployment update(Deployment fetched, Tomcat tomcat, Context context) { - return new DeploymentBuilder(fetched).editSpec().editTemplate().editSpec().editFirstContainer() - .withImage("tomcat:" + tomcat.getSpec().getVersion()) - .endContainer().endSpec().endTemplate().endSpec().build(); + public boolean match(Deployment fetched, Tomcat tomcat, Context context) { + return fetched.getSpec().getTemplate().getSpec().getContainers().stream() + .findFirst().map(c -> tomcatImage(tomcat).equals(c.getImage())).orElse(false); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index b90bd0e6e2..52d043203b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -1,28 +1,26 @@ package io.javaoperatorsdk.operator.sample; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -public class ServiceDependentResource - implements DependentResource, Builder { +public class ServiceDependentResource implements DependentResource { @Override - public Service buildFor(Tomcat tomcat, Context context) { + public Optional desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); - final Service service = - new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) - .editMetadata() - .withName(tomcatMetadata.getName()) - .withNamespace(tomcatMetadata.getNamespace()) - .endMetadata() - .editSpec() - .addToSelector("app", tomcatMetadata.getName()) - .endSpec() - .build(); - return service; + return Optional.of(new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + .editMetadata() + .withName(tomcatMetadata.getName()) + .withNamespace(tomcatMetadata.getNamespace()) + .endMetadata() + .editSpec() + .addToSelector("app", tomcatMetadata.getName()) + .endSpec() + .build()); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java index 9dd49466ab..abb4e62358 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java @@ -3,8 +3,8 @@ import java.util.Set; import java.util.stream.Collectors; -import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 32f7927f44..d2d2a6c96c 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -11,7 +11,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 9e1f6f677f..8f7b682d62 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -28,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @ControllerConfiguration From 78a305b48a6e34df6c46a568e02313090cf69f47 Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 9 Feb 2022 15:58:08 +0100 Subject: [PATCH 0010/1319] fix: merge main to next - fixed conflicts --- sample-operators/mysql-schema/pom.xml | 6 ++++++ .../operator/sample/MySQLSchemaOperatorE2E.java | 4 ++-- sample-operators/tomcat-operator/pom.xml | 6 ++++++ .../javaoperatorsdk/operator/sample/TomcatOperatorE2E.java | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index c8932f902f..ca2d4e8cb5 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -72,6 +72,12 @@ jackson-dataformat-yaml 2.13.1 + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index cf8284f179..6049627f94 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -65,8 +65,8 @@ boolean isLocal() { @RegisterExtension AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new MySQLSchemaReconciler(client, - new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) + .withReconciler( + new MySQLSchemaReconciler(new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) .withInfrastructure(infrastructure) .build() : E2EOperatorExtension.builder() diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index cee6935745..7e0af474c5 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -57,6 +57,12 @@ 4.1.1 test + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index 042f4973cf..aa1a2b81ff 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -44,7 +44,7 @@ boolean isLocal() { AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new TomcatReconciler(client)) + .withReconciler(new TomcatReconciler()) .withReconciler(new WebappReconciler(client)) .build() : E2EOperatorExtension.builder() From d3ad7e50ff040620b32f6e63359b73aee3d11ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 9 Feb 2022 17:34:41 +0100 Subject: [PATCH 0011/1319] fix: run e2e tests on next (#919) --- .github/workflows/e2e-test.yml | 3 ++- .../operator/sample/MySQLSchemaReconciler.java | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 31b89e1b20..774df82b48 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -3,10 +3,11 @@ name: Integration & End to End tests on: pull_request: - branches: [ main ] + branches: [ main, next ] push: branches: - main + - next jobs: sample_operators_tests: diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index ff4fcad131..096f3ed4a7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -23,9 +23,11 @@ import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static java.lang.String.format; -@ControllerConfiguration( +// todo handle this, should work with finalizer +@ControllerConfiguration(finalizerName = NO_FINALIZER, dependents = { @Dependent(resourceType = Secret.class, type = SecretDependentResource.class), @Dependent(resourceType = Schema.class, type = SchemaDependentResource.class) From 57dcbc57f9d00df3aff08cbdd217f652311e3296 Mon Sep 17 00:00:00 2001 From: Jose Carvajal Date: Thu, 10 Feb 2022 14:43:44 +0100 Subject: [PATCH 0012/1319] doc: add groupId in Maven query (#921) When querying Maven central using the artifactId `operator-framework`, there were multiple matches: - One using the groupId: `com.github.containersolutions` - Another one using: `io.javaoperatorsdk` While is obvious that the right one is the one using the groupId `io.javaoperatorsdk`, we can avoid confusions by querying Maven central using both the artifactId and the group Id fields. --- docs/documentation/use-samples.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/documentation/use-samples.md b/docs/documentation/use-samples.md index 616a782181..1bc32851a7 100644 --- a/docs/documentation/use-samples.md +++ b/docs/documentation/use-samples.md @@ -26,14 +26,14 @@ examples: # Implementing a Sample Operator -Add [dependency](https://search.maven.org/search?q=a:operator-framework) to your project with Maven: +Add [dependency](https://search.maven.org/search?q=a:operator-framework%20AND%20g:io.javaoperatorsdk) to your project with Maven: ```xml io.javaoperatorsdk operator-framework - {see https://search.maven.org/search?q=a:operator-framework for latest version} + {see https://search.maven.org/search?q=a:operator-framework%20AND%20g:io.javaoperatorsdk for latest version} ``` @@ -188,14 +188,14 @@ public class QuarkusOperator implements QuarkusApplication { You can also let Spring Boot wire your application together and automatically register the controllers. -Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to your project: +Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk) to your project: ```xml io.javaoperatorsdk operator-framework-spring-boot-starter - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk for latest version} @@ -224,7 +224,7 @@ necessary, but it doesn't need real access to a Kubernetes cluster. io.javaoperatorsdk operator-framework-spring-boot-starter-test - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk for latest version} From 6a05ebc52f683e77238db9ce14765876b7fcc36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Feb 2022 10:01:14 +0100 Subject: [PATCH 0013/1319] Dependent resources standalone mode (#914) --- .../operator/ReconcilerUtils.java | 17 ++ .../dependent/AbstractDependentResource.java | 31 +++ .../dependent/DependentResource.java | 50 +--- .../reconciler/dependent/DesiredSupplier.java | 10 + .../KubernetesDependentResource.java | 150 ++++++++++ .../api/reconciler/dependent/Persister.java | 11 - ...StandaloneKubernetesDependentResource.java | 63 +++++ .../DependentResourceController.java | 108 ++------ .../dependent/DependentResourceManager.java | 25 +- ...KubernetesDependentResourceController.java | 72 ++--- .../operator/ReconcilerUtilsTest.java | 43 +++ .../StandaloneDependentResourceIT.java | 54 ++++ ...formerEventSourceTestCustomReconciler.java | 42 ++- ...StandaloneDependentTestCustomResource.java | 15 + ...loneDependentTestCustomResourceStatus.java | 5 + .../StandaloneDependentTestReconciler.java | 77 ++++++ .../standalonedependent/nginx-deployment.yaml | 21 ++ .../sample/MySQLSchemaReconciler.java | 39 +-- .../sample/SchemaDependentResource.java | 54 ++-- .../sample/SecretDependentResource.java | 54 ++++ .../sample/MySQLSchemaOperatorE2E.java | 6 +- .../sample/DeploymentDependentResource.java | 18 +- .../sample/ServiceDependentResource.java | 14 +- .../sample/TomcatDependentResource.java | 41 --- sample-operators/webpage/pom.xml | 5 + .../operator/sample/WebPageReconciler.java | 256 +++++++++--------- 26 files changed, 821 insertions(+), 460 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java create mode 100644 operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java delete mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index e3a6da1e5a..6f4e19692b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Locale; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -98,4 +100,19 @@ public static String getDefaultReconcilerName(String reconcilerClassName) { } return reconcilerClassName.toLowerCase(Locale.ROOT); } + + public static boolean specsEqual(HasMetadata r1, HasMetadata r2) { + return getSpec(r1).equals(getSpec(r2)); + } + + // will be replaced with: https://github.com/fabric8io/kubernetes-client/issues/3816 + public static Object getSpec(HasMetadata resource) { + try { + Method getSpecMethod = resource.getClass().getMethod("getSpec"); + return getSpecMethod.invoke(resource); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java new file mode 100644 index 0000000000..ea8b8bc06b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public abstract class AbstractDependentResource + implements DependentResource { + + @Override + public void reconcile(P primary, Context context) { + var actual = getResource(primary); + var desired = desired(primary, context); + if (actual.isEmpty()) { + create(desired, primary, context); + } else { + if (!match(actual.get(), desired, context)) { + update(actual.get(), desired, primary, context); + } + } + } + + protected abstract R desired(P primary, Context context); + + protected abstract boolean match(R actual, R target, Context context); + + protected abstract R create(R target, P primary, Context context); + + // the actual needed to copy/preserve new labels or annotations + protected abstract R update(R actual, R target, P primary, Context context); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index df144ade1f..b097554107 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -9,54 +9,18 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; public interface DependentResource { - default EventSource initEventSource(EventSourceContext

context) { - throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); - } + + Optional eventSource(EventSourceContext

context); @SuppressWarnings("unchecked") default Class resourceType() { return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); } - default void delete(R fetched, P primary, Context context) {} - - /** - * Computes the desired state of the dependent based on the state provided by the specified - * primary resource. - * - * The default implementation returns {@code empty} which corresponds to the case where the - * associated dependent should never be created by the associated reconciler or that the global - * state of the cluster doesn't allow for the resource to be created at this point. - * - * @param primary the primary resource associated with the reconciliation process - * @param context the {@link Context} associated with the reconciliation process - * @return an instance of the dependent resource matching the desired state specified by the - * primary resource or {@code empty} if the dependent shouldn't be created at this point - * (or ever) - */ - default Optional desired(P primary, Context context) { - return Optional.empty(); - } + void reconcile(P primary, Context context); + + void delete(P primary, Context context); + + Optional getResource(P primaryResource); - /** - * Checks whether the actual resource as fetched from the cluster matches the desired state - * expressed by the specified primary resource. - * - * The default implementation always return {@code true}, which corresponds to the behavior where - * the dependent never needs to be updated after it's been created. - * - * Note that failure to properly implement this method will lead to infinite loops. In particular, - * for typical Kubernetes resource implementations, simply calling - * {@code desired(primary, context).equals(actual)} is not enough because metadata will usually be - * different. - * - * @param actual the current state of the resource as fetched from the cluster - * @param primary the primary resource associated with the reconciliation request - * @param context the {@link Context} associated with the reconciliation request - * @return {@code true} if the actual state of the resource matches the desired state expressed by - * the specified primary resource, {@code false} otherwise - */ - default boolean match(R actual, P primary, Context context) { - return true; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java new file mode 100644 index 0000000000..b93d75950b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@FunctionalInterface +public interface DesiredSupplier { + + R getDesired(P primary, Context context); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java new file mode 100644 index 0000000000..e8a4a6779e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java @@ -0,0 +1,150 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; + +public abstract class KubernetesDependentResource + extends AbstractDependentResource { + + private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); + + protected KubernetesClient client; + private boolean explicitDelete = false; + private boolean owned = true; + private InformerEventSource informerEventSource; + + public KubernetesDependentResource() { + this(null); + } + + public KubernetesDependentResource(KubernetesClient client) { + this.client = client; + } + + protected void beforeCreateOrUpdate(R desired, P primary) { + if (owned) { + desired.addOwnerReference(primary); + } + } + + @Override + protected boolean match(R actual, R target, Context context) { + return ReconcilerUtils.specsEqual(actual, target); + } + + @SuppressWarnings("unchecked") + @Override + protected R create(R target, P primary, Context context) { + log.debug("Creating target resource with type: " + + "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); + beforeCreateOrUpdate(target, primary); + Class targetClass = (Class) target.getClass(); + return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) + .create(target); + } + + @SuppressWarnings("unchecked") + @Override + protected R update(R actual, R target, P primary, Context context) { + log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), + ResourceID.fromResource(target)); + beforeCreateOrUpdate(target, primary); + Class targetClass = (Class) target.getClass(); + return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) + .replace(target); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Optional eventSource(EventSourceContext

context) { + if (informerEventSource != null) { + return Optional.of(informerEventSource); + } + var informerConfig = initInformerConfiguration(context); + informerEventSource = new InformerEventSource(informerConfig, context); + return Optional.of(informerEventSource); + } + + @SuppressWarnings("unchecked") + private InformerConfiguration initInformerConfiguration(EventSourceContext

context) { + PrimaryResourcesRetriever associatedPrimaries = + (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this + : getDefaultPrimaryResourcesRetriever(); + + AssociatedSecondaryResourceIdentifier

associatedSecondary = + (this instanceof AssociatedSecondaryResourceIdentifier) + ? (AssociatedSecondaryResourceIdentifier

) this + : getDefaultAssociatedSecondaryResourceIdentifier(); + + return InformerConfiguration.from(context, resourceType()) + .withPrimaryResourcesRetriever(associatedPrimaries) + .withAssociatedSecondaryResourceIdentifier(associatedSecondary) + .build(); + } + + protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { + return ResourceID::fromResource; + } + + protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { + return Mappers.fromOwnerReference(); + } + + public KubernetesDependentResource setInformerEventSource( + InformerEventSource informerEventSource) { + this.informerEventSource = informerEventSource; + return this; + } + + @Override + public void delete(P primary, Context context) { + if (explicitDelete) { + var resource = getResource(primary); + resource.ifPresent(r -> client.resource(r).delete()); + } + } + + @Override + public Optional getResource(P primaryResource) { + return informerEventSource.getAssociated(primaryResource); + } + + public KubernetesDependentResource setClient(KubernetesClient client) { + this.client = client; + return this; + } + + + public KubernetesDependentResource setExplicitDelete(boolean explicitDelete) { + this.explicitDelete = explicitDelete; + return this; + } + + public boolean isExplicitDelete() { + return explicitDelete; + } + + public boolean isOwned() { + return owned; + } + + public KubernetesDependentResource setOwned(boolean owned) { + this.owned = owned; + return this; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java deleted file mode 100644 index ab34372917..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface Persister { - - void createOrReplace(R dependentResource, Context context); - - R getFor(P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java new file mode 100644 index 0000000000..d4d1e00dc2 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java @@ -0,0 +1,63 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; + +// todo shorter name +public class StandaloneKubernetesDependentResource + extends KubernetesDependentResource { + + private final DesiredSupplier desiredSupplier; + private final Class resourceType; + private AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier = + ResourceID::fromResource; + private PrimaryResourcesRetriever primaryResourcesRetriever = Mappers.fromOwnerReference(); + + public StandaloneKubernetesDependentResource( + Class resourceType, DesiredSupplier desiredSupplier) { + this(null, resourceType, desiredSupplier); + } + + public StandaloneKubernetesDependentResource( + KubernetesClient client, Class resourceType, DesiredSupplier desiredSupplier) { + super(client); + this.desiredSupplier = desiredSupplier; + this.resourceType = resourceType; + } + + @Override + protected R desired(P primary, Context context) { + return desiredSupplier.getDesired(primary, context); + } + + public Class resourceType() { + return resourceType; + } + + public StandaloneKubernetesDependentResource setAssociatedSecondaryResourceIdentifier( + AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier) { + this.associatedSecondaryResourceIdentifier = associatedSecondaryResourceIdentifier; + return this; + } + + public StandaloneKubernetesDependentResource setPrimaryResourcesRetriever( + PrimaryResourcesRetriever primaryResourcesRetriever) { + this.primaryResourcesRetriever = primaryResourcesRetriever; + return this; + } + + @Override + protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { + return this.associatedSecondaryResourceIdentifier; + } + + @Override + protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { + return this.primaryResourcesRetriever; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java index d0004111af..e49a55a73a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java @@ -2,133 +2,65 @@ import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @Ignore -public class DependentResourceController> - implements DependentResource, Persister, Reconciler

{ - - private static final Logger log = LoggerFactory.getLogger(DependentResourceController.class); +public class DependentResourceController, D extends DependentResource> + implements DependentResource { - private final Persister persister; - private final DependentResource delegate; + private final D delegate; private final C configuration; - public DependentResourceController(DependentResource delegate, C configuration) { + public DependentResourceController(D delegate, C configuration) { this.delegate = delegate; - persister = initPersister(delegate); - this.configuration = configuration; + this.configuration = initConfiguration(delegate, configuration); } - @Override - public Class resourceType() { - return delegate.resourceType(); + protected C initConfiguration(D delegate, C configuration) { + // default implementation just returns the specified one + return configuration; } @Override - public boolean match(R actual, P primary, Context context) { - return delegate.match(actual, primary, context); + public Class resourceType() { + return delegate.resourceType(); } @Override - public Optional desired(P primary, Context context) { - return delegate.desired(primary, context); + public void delete(P primary, Context context) { + delegate.delete(primary, context); } @Override - public void delete(R fetched, P primary, Context context) { - delegate.delete(fetched, primary, context); - } - - @SuppressWarnings("unchecked") - protected Persister initPersister(DependentResource delegate) { - if (delegate instanceof Persister) { - return (Persister) delegate; - } else { - throw new IllegalArgumentException( - "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); - } + public Optional getResource(P primaryResource) { + return delegate.getResource(primaryResource); } - public String descriptionFor(R resource) { - return resource.toString(); - } - - public Class getResourceType() { - return delegate.resourceType(); - } @Override - public EventSource initEventSource(EventSourceContext

context) { - return delegate.initEventSource(context); + public Optional eventSource(EventSourceContext

context) { + return delegate.eventSource(context); } - @Override - public void createOrReplace(R dependentResource, Context context) { - persister.createOrReplace(dependentResource, context); - } - - @Override - public R getFor(P primary, Context context) { - return persister.getFor(primary, context); - } public C getConfiguration() { return configuration; } - @Override - public UpdateControl

reconcile(P resource, Context context) { - var actual = getFor(resource, context); - if (actual == null || !match(actual, resource, context)) { - final var desired = desired(resource, context); - desired.ifPresent(d -> createOrReplaceDependent(resource, d, context)); - } - return UpdateControl.noUpdate(); + protected D delegate() { + return delegate; } @Override - public DeleteControl cleanup(P primary, Context context) { - var dependent = getFor(primary, context); - if (dependent != null) { - delete(dependent, primary, context); - logOperationInfo(primary, dependent, "Deleting"); - } else { - log.info("Ignoring already deleted {} for '{}' {}", - getResourceType().getName(), - primary.getMetadata().getName(), - primary.getKind()); - } - return Reconciler.super.cleanup(primary, context); + public void reconcile(P resource, Context context) { + delegate.reconcile(resource, context); } - protected void createOrReplaceDependent(P primary, R dependent, Context context) { - logOperationInfo(primary, dependent, "Reconciling"); - // commit the changes - // todo: add metrics timing for dependent resource - createOrReplace(dependent, context); - } - - private void logOperationInfo(P resource, R dependentResource, String operationDescription) { - if (log.isInfoEnabled()) { - log.info("{} {} for '{}' {}", operationDescription, - descriptionFor(dependentResource), - resource.getMetadata().getName(), - resource.getKind()); - } - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 8ab04d7d09..ac791526fc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -5,6 +5,7 @@ import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; @@ -18,6 +19,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -42,9 +44,11 @@ public List prepareEventSources(EventSourceContext

context) { List sources = new ArrayList<>(configured.size() + 5); configured.forEach(dependent -> { - final var dependentResourceController = from(dependent); + final var dependentResourceController = from(dependent, context.getClient()); dependents.add(dependentResourceController); - sources.add(dependentResourceController.initEventSource(context)); + dependentResourceController.eventSource(context) + .ifPresent(es -> sources.add((EventSource) es)); + }); return sources; @@ -68,7 +72,7 @@ public UpdateControl

reconcile(P resource, Context context) { @Override public DeleteControl cleanup(P resource, Context context) { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.cleanup(resource, context)); + dependents.forEach(dependent -> dependent.delete(resource, context)); return Reconciler.super.cleanup(resource, context); } @@ -80,14 +84,23 @@ private void initContextIfNeeded(P resource, Context context) { } } - private DependentResourceController from(DependentResourceConfiguration config) { + private DependentResourceController from(DependentResourceConfiguration config, + KubernetesClient client) { try { final var dependentResource = (DependentResource) config.getDependentResourceClass().getConstructor() .newInstance(); if (config instanceof KubernetesDependentResourceConfiguration) { - return new KubernetesDependentResourceController(dependentResource, - (KubernetesDependentResourceConfiguration) config); + if (dependentResource instanceof KubernetesDependentResource) { + final var kubeDependentResource = (KubernetesDependentResource) dependentResource; + kubeDependentResource.setClient(client); + return new KubernetesDependentResourceController(kubeDependentResource, + (KubernetesDependentResourceConfiguration) config); + } else { + throw new IllegalArgumentException("A " + + KubernetesDependentResourceConfiguration.class.getCanonicalName() + + " must be associated to a " + KubernetesDependentResource.class.getCanonicalName()); + } } else { return new DependentResourceController(dependentResource, config); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java index 63d533193b..733460ef31 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java @@ -1,14 +1,13 @@ package io.javaoperatorsdk.operator.processing.dependent; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; @@ -16,17 +15,19 @@ @Ignore public class KubernetesDependentResourceController - extends DependentResourceController> { - - private final KubernetesDependentResourceConfiguration configuration; - private KubernetesClient client; - private InformerEventSource informer; + extends + DependentResourceController, KubernetesDependentResource> { + public KubernetesDependentResourceController(KubernetesDependentResource delegate, + KubernetesDependentResourceConfiguration configuration) { + super(delegate, configuration); + } @SuppressWarnings("unchecked") - public KubernetesDependentResourceController(DependentResource delegate, + @Override + protected KubernetesDependentResourceConfiguration initConfiguration( + KubernetesDependentResource delegate, KubernetesDependentResourceConfiguration configuration) { - super(delegate, configuration); // todo: check if we can validate that types actually match properly final var associatedPrimaries = (delegate instanceof PrimaryResourcesRetriever) @@ -41,50 +42,15 @@ public KubernetesDependentResourceController(DependentResource delegate, .withPrimaryResourcesRetriever(associatedPrimaries) .withAssociatedSecondaryResourceIdentifier(associatedSecondary) .build(); - this.configuration = - KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), - configuration.getDependentResourceClass()); - } - - @SuppressWarnings("unchecked") - @Override - protected Persister initPersister(DependentResource delegate) { - return (delegate instanceof Persister) ? (Persister) delegate : this; - } - - @Override - public String descriptionFor(R resource) { - return String.format("'%s' %s dependent in namespace %s", resource.getMetadata().getName(), - resource.getFullResourceName(), - resource.getMetadata().getNamespace()); - } - - @Override - public EventSource initEventSource(EventSourceContext

context) { - this.client = context.getClient(); - informer = new InformerEventSource<>(configuration, context); - return informer; - } - - @Override - public void createOrReplace(R dependentResource, Context context) { - client.resource(dependentResource).createOrReplace(); - } - - @Override - public R getFor(P primary, Context context) { - return informer.getAssociated(primary).orElse(null); - } - - public boolean owned() { - return getConfiguration().isOwned(); + return KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), + configuration.getDependentResourceClass()); } @Override - protected void createOrReplaceDependent(P primary, R dependent, Context context) { - if (owned()) { - dependent.addOwnerReference(primary); - } - super.createOrReplaceDependent(primary, dependent, context); + public Optional eventSource(EventSourceContext

context) { + var informer = new InformerEventSource<>(getConfiguration(), context); + // todo have this implemented with nicer abstractions + delegate().setInformerEventSource(informer); + return super.eventSource(context); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index d46936b3d5..be82caa36b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -3,6 +3,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -11,6 +15,7 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultNameFor; import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultReconcilerName; import static io.javaoperatorsdk.operator.ReconcilerUtils.isFinalizerValid; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -39,4 +44,42 @@ void defaultFinalizerShouldWork() { void noFinalizerMarkerShouldWork() { assertTrue(isFinalizerValid(Constants.NO_FINALIZER)); } + + @Test + void equalsSpecObject() { + var d1 = createTestDeployment(); + var d2 = createTestDeployment(); + + assertThat(ReconcilerUtils.specsEqual(d1, d2)).isTrue(); + } + + @Test + void equalArbitraryDifferentSpecsOfObjects() { + var d1 = createTestDeployment(); + var d2 = createTestDeployment(); + d2.getSpec().getTemplate().getSpec().setHostname("otherhost"); + + assertThat(ReconcilerUtils.specsEqual(d1, d2)).isFalse(); + } + + @Test + void getsSpecWithReflection() { + Deployment deployment = new Deployment(); + deployment.setSpec(new DeploymentSpec()); + deployment.getSpec().setReplicas(5); + + DeploymentSpec spec = (DeploymentSpec) ReconcilerUtils.getSpec(deployment); + assertThat(spec.getReplicas()).isEqualTo(5); + } + + private Deployment createTestDeployment() { + Deployment deployment = new Deployment(); + deployment.setSpec(new DeploymentSpec()); + deployment.getSpec().setReplicas(5); + PodTemplateSpec podTemplateSpec = new PodTemplateSpec(); + deployment.getSpec().setTemplate(podTemplateSpec); + podTemplateSpec.setSpec(new PodSpec()); + podTemplateSpec.getSpec().setHostname("localhost"); + return deployment; + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java new file mode 100644 index 0000000000..20c0f20b80 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.dependent; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestReconciler; + +import static org.awaitility.Awaitility.await; + +class StandaloneDependentResourceIT { + + public static final String DEPENDENT_TEST_NAME = "dependent-test1"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new StandaloneDependentTestReconciler()) + .build(); + + @Test + void dependentResourceManagesDeployment() { + StandaloneDependentTestCustomResource customResource = + new StandaloneDependentTestCustomResource(); + customResource.setMetadata(new ObjectMeta()); + customResource.getMetadata().setName(DEPENDENT_TEST_NAME); + var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + + await() + .pollInterval(Duration.ofMillis(300)) + .atMost(Duration.ofSeconds(50)) + .until( + () -> { + var deployment = + operator + .getKubernetesClient() + .resources(Deployment.class) + .inNamespace(createdCR.getMetadata().getNamespace()) + .withName(DEPENDENT_TEST_NAME) + .get(); + return deployment != null + && deployment.getStatus() != null + && deployment.getStatus().getReadyReplicas() != null + && deployment.getStatus().getReadyReplicas() > 0; + }); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 33128a6b8c..d61aa7f0f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -1,23 +1,18 @@ package io.javaoperatorsdk.operator.sample.informereventsource; +import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.ConfigMapDR; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -25,10 +20,10 @@ * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@ControllerConfiguration(finalizerName = NO_FINALIZER, - dependents = @Dependent(resourceType = ConfigMap.class, type = ConfigMapDR.class)) +@ControllerConfiguration(finalizerName = NO_FINALIZER) public class InformerEventSourceTestCustomReconciler - implements Reconciler { + implements Reconciler, + EventSourceInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); @@ -39,22 +34,21 @@ public class InformerEventSourceTestCustomReconciler private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - public static class ConfigMapDR - implements DependentResource, - PrimaryResourcesRetriever { - private final PrimaryResourcesRetriever retriever = Mappers.fromAnnotation( - RELATED_RESOURCE_NAME); + @Override + public List prepareEventSources( + EventSourceContext context) { - @Override - public Set associatedPrimaryResources(ConfigMap dependentResource) { - return retriever.associatedPrimaryResources(dependentResource); - } + InformerConfiguration config = + InformerConfiguration.from(context, ConfigMap.class) + .withPrimaryResourcesRetriever(Mappers.fromAnnotation(RELATED_RESOURCE_NAME)) + .build(); + + return List.of(new InformerEventSource<>(config, context)); } @Override public UpdateControl reconcile( - InformerEventSourceTestCustomResource resource, - Context context) { + InformerEventSourceTestCustomResource resource, Context context) { numberOfExecutions.incrementAndGet(); resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java new file mode 100644 index 0000000000..3e6737c83c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("sdt") +public class StandaloneDependentTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java new file mode 100644 index 0000000000..5f12f1ef72 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +public class StandaloneDependentTestCustomResourceStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java new file mode 100644 index 0000000000..695cc30a31 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class StandaloneDependentTestReconciler + implements Reconciler, + EventSourceInitializer, + KubernetesClientAware { + + private KubernetesClient kubernetesClient; + + StandaloneKubernetesDependentResource configMapDependent; + + public StandaloneDependentTestReconciler() { + configMapDependent = + new StandaloneKubernetesDependentResource<>(Deployment.class, (primary, context) -> { + Deployment deployment = loadYaml(Deployment.class, "nginx-deployment.yaml"); + deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + return deployment; + }) { + @Override + protected boolean match(Deployment actual, Deployment target, Context context) { + return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && + actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } + }; + } + + @Override + public List prepareEventSources( + EventSourceContext context) { + return List.of(configMapDependent.eventSource(context).get()); + } + + @Override + public UpdateControl reconcile( + StandaloneDependentTestCustomResource resource, Context context) { + configMapDependent.reconcile(resource, context); + return UpdateControl.noUpdate(); + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + configMapDependent.setClient(kubernetesClient); + } + + @Override + public KubernetesClient getKubernetesClient() { + return this.kubernetesClient; + } + + private T loadYaml(Class clazz, String yaml) { + try (InputStream is = getClass().getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); + } + } +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml new file mode 100644 index 0000000000..cea0f3c9d6 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: "" +spec: + progressDeadlineSeconds: 600 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: "test-dependent" + replicas: 1 + template: + metadata: + labels: + app: "test-dependent" + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 096f3ed4a7..63c37ac3d7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Base64; import java.util.Optional; import org.apache.commons.lang3.RandomStringUtils; @@ -8,7 +7,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; @@ -19,8 +17,6 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -36,14 +32,14 @@ public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, ContextInitializer, EventSourceContextInjector { - private static final String SECRET_FORMAT = "%s-secret"; - private static final String USERNAME_FORMAT = "%s-user"; + static final String SECRET_FORMAT = "%s-secret"; + static final String USERNAME_FORMAT = "%s-user"; - protected static final String MYSQL_SECRET_NAME = "mysql.secret.name"; - protected static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; - protected static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; - protected static final String MYSQL_DB_CONFIG = "mysql.db.config"; - protected static final String BUILT_SCHEMA = "built schema"; + static final String MYSQL_SECRET_NAME = "mysql.secret.name"; + static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; + static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; + static final String MYSQL_DB_CONFIG = "mysql.db.config"; + static final String BUILT_SCHEMA = "built schema"; static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); private final MySQLDbConfig mysqlDbConfig; @@ -52,27 +48,6 @@ public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { this.mysqlDbConfig = mysqlDbConfig; } - public static class SecretDependentResource implements DependentResource { - - private static String encode(String value) { - return Base64.getEncoder().encodeToString(value.getBytes()); - } - - @Override - public Optional desired(MySQLSchema schema, Context context) { - return Optional.of(new SecretBuilder() - .withNewMetadata() - .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) - .withNamespace(schema.getMetadata().getNamespace()) - .endMetadata() - .addToData("MYSQL_USERNAME", encode( - context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) - .addToData("MYSQL_PASSWORD", encode( - context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) - .build()); - } - } - @SuppressWarnings("rawtypes") @Override public void injectInto(EventSourceContext context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 8ba9612029..957f0b34ff 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -7,8 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; import io.javaoperatorsdk.operator.sample.schema.Schema; @@ -16,39 +15,53 @@ import static java.lang.String.format; -public class SchemaDependentResource - implements DependentResource, Persister { +public class SchemaDependentResource extends AbstractDependentResource { private static final int POLL_PERIOD = 500; private MySQLDbConfig dbConfig; @Override - public EventSource initEventSource(EventSourceContext context) { + public Optional eventSource(EventSourceContext context) { dbConfig = context.getMandatory(MySQLSchemaReconciler.MYSQL_DB_CONFIG, MySQLDbConfig.class); - return new PerResourcePollingEventSource<>( + return Optional.of(new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), POLL_PERIOD, - Schema.class); + Schema.class)); } @Override - public Optional desired(MySQLSchema primary, Context context) { + public Schema desired(MySQLSchema primary, Context context) { + return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); + } + + @Override + protected boolean match(Schema actual, Schema target, Context context) { + return actual.equals(target); + } + + @Override + protected Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { final var schema = SchemaService.createSchemaAndRelatedUser( connection, - primary.getMetadata().getName(), - primary.getSpec().getEncoding(), + target.getName(), + target.getCharacterSet(), context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class), context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class)); // put the newly built schema in the context to let the reconciler know we just built it context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); - return Optional.of(schema); + return schema; } catch (SQLException e) { MySQLSchemaReconciler.log.error("Error while creating Schema", e); throw new IllegalStateException(e); } } + @Override + protected Schema update(Schema actual, Schema target, MySQLSchema mySQLSchema, Context context) { + throw new IllegalStateException("Target schema should not be changed: " + mySQLSchema); + } + private Connection getConnection() throws SQLException { String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); @@ -58,7 +71,7 @@ private Connection getConnection() throws SQLException { } @Override - public void delete(Schema fetched, MySQLSchema primary, Context context) { + public void delete(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; SchemaService.deleteSchemaAndRelatedUser(connection, primary.getMetadata().getName(), @@ -68,19 +81,16 @@ public void delete(Schema fetched, MySQLSchema primary, Context context) { } } + // todo this should read the resource from event source? @Override - public void createOrReplace(Schema dependentResource, Context context) { - // this is actually implemented in buildFor, the cleaner way to do this would be to have all - // the needed information in Schema instead of creating both the schema and user from - // heterogeneous information - } - - @Override - public Schema getFor(MySQLSchema primary, Context context) { + public Optional getResource(MySQLSchema primaryResource) { try (Connection connection = getConnection()) { - return SchemaService.getSchema(connection, primary.getMetadata().getName()).orElse(null); + var schema = + SchemaService.getSchema(connection, primaryResource.getMetadata().getName()).orElse(null); + return Optional.ofNullable(schema); } catch (SQLException e) { - throw new RuntimeException("Error while trying to delete Schema", e); + throw new RuntimeException("Error while trying read Schema", e); } } + } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java new file mode 100644 index 0000000000..4f6bd1759c --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.Base64; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; + +import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; + +public class SecretDependentResource extends KubernetesDependentResource + implements AssociatedSecondaryResourceIdentifier { + + private static String encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } + + @Override + public Secret desired(MySQLSchema schema, Context context) { + return new SecretBuilder() + .withNewMetadata() + .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) + .withNamespace(schema.getMetadata().getNamespace()) + .endMetadata() + .addToData( + "MYSQL_USERNAME", encode(context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) + .addToData( + "MYSQL_PASSWORD", encode(context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) + .build(); + } + + // An alternative would be to override reconcile() method and exclude the update part. + @Override + protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { + throw new IllegalStateException( + "Secret should not be updated. Secret: " + target + " for custom resource: " + + primary); + } + + @Override + protected boolean match(Secret actual, Secret target, Context context) { + return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); + } + + @Override + public ResourceID associatedSecondaryID(MySQLSchema primary) { + return new ResourceID( + String.format(SECRET_FORMAT, primary.getMetadata().getName()), + primary.getMetadata().getNamespace()); + } +} diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index 6049627f94..0c1be5efc0 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -29,7 +29,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; -public class MySQLSchemaOperatorE2E { +class MySQLSchemaOperatorE2E { final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); @@ -113,8 +113,8 @@ public void test() throws IOException { log.info("Creating test MySQLSchema object: {}", testSchema); client.resource(testSchema).createOrReplace(); - log.info("Waiting 5 minutes for expected resources to be created and updated"); - await().atMost(1, MINUTES).ignoreExceptions().untilAsserted(() -> { + log.info("Waiting 2 minutes for expected resources to be created and updated"); + await().atMost(2, MINUTES).ignoreExceptions().untilAsserted(() -> { MySQLSchema updatedSchema = client.resources(MySQLSchema.class).inNamespace(operator.getNamespace()) .withName(testSchema.getMetadata().getName()).get(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 58200df988..b681ccf6b6 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -1,20 +1,20 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Optional; - import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") public class DeploymentDependentResource - implements DependentResource { + extends KubernetesDependentResource { + + public DeploymentDependentResource() {} @Override - public Optional desired(Tomcat tomcat, Context context) { + public Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); @@ -39,7 +39,7 @@ public Optional desired(Tomcat tomcat, Context context) { .endTemplate() .endSpec() .build(); - return Optional.of(deployment); + return deployment; } private String tomcatImage(Tomcat tomcat) { @@ -47,8 +47,8 @@ private String tomcatImage(Tomcat tomcat) { } @Override - public boolean match(Deployment fetched, Tomcat tomcat, Context context) { - return fetched.getSpec().getTemplate().getSpec().getContainers().stream() - .findFirst().map(c -> tomcatImage(tomcat).equals(c.getImage())).orElse(false); + public boolean match(Deployment fetched, Deployment target, Context context) { + return fetched.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals(target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 52d043203b..16a6aaff2e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -1,19 +1,19 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Optional; - import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; + +public class ServiceDependentResource extends KubernetesDependentResource { -public class ServiceDependentResource implements DependentResource { + public ServiceDependentResource() {} @Override - public Optional desired(Tomcat tomcat, Context context) { + public Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); - return Optional.of(new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + return new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) .editMetadata() .withName(tomcatMetadata.getName()) .withNamespace(tomcatMetadata.getNamespace()) @@ -21,6 +21,6 @@ public Optional desired(Tomcat tomcat, Context context) { .editSpec() .addToSelector("app", tomcatMetadata.getName()) .endSpec() - .build()); + .build(); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java deleted file mode 100644 index abb4e62358..0000000000 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import java.util.Set; -import java.util.stream.Collectors; - -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; - -public class TomcatDependentResource - implements DependentResource, PrimaryResourcesRetriever, - AssociatedSecondaryResourceIdentifier, EventSourceContextAware { - - private ResourceCache primaryCache; - - @Override - public void initWith(EventSourceContext context) { - this.primaryCache = context.getPrimaryCache(); - } - - @Override - public Set associatedPrimaryResources(Tomcat t) { - // To create an event to a related WebApp resource and trigger the reconciliation - // we need to find which WebApp this Tomcat custom resource is related to. - // To find the related customResourceId of the WebApp resource we traverse the cache to - // and identify it based on naming convention. - return primaryCache - .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(ResourceID::fromResource) - .collect(Collectors.toSet()); - } - - @Override - public ResourceID associatedSecondaryID(Webapp primary) { - return new ResourceID(primary.getSpec().getTomcat(), primary.getMetadata().getNamespace()); - } -} diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 566ec8b314..40101a7413 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -40,6 +40,11 @@ crd-generator-apt provided + + org.awaitility + awaitility + compile + diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index e02c438417..28d6ed3037 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -2,9 +2,8 @@ import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.time.Duration; +import java.util.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -13,22 +12,40 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.dsl.RollableScalableResource; -import io.fabric8.kubernetes.client.dsl.ServiceResource; import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; -@ControllerConfiguration -public class WebPageReconciler implements Reconciler, ErrorStatusHandler { +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; +import static org.awaitility.Awaitility.await; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class WebPageReconciler + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { private final Logger log = LoggerFactory.getLogger(getClass()); private final KubernetesClient kubernetesClient; + private StandaloneKubernetesDependentResource configMapDR; + private StandaloneKubernetesDependentResource deploymentDR; + private StandaloneKubernetesDependentResource serviceDR; + public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + List eventSources = new ArrayList<>(3); + configMapDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + deploymentDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + serviceDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + return eventSources; } @Override @@ -37,81 +54,14 @@ public UpdateControl reconcile(WebPage webPage, Context context) { throw new ErrorSimulationException("Simulating error"); } - String ns = webPage.getMetadata().getNamespace(); - String configMapName = configMapName(webPage); - String deploymentName = deploymentName(webPage); - - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - - ConfigMap htmlConfigMap = - new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(configMapName) - .withNamespace(ns) - .build()) - .withData(data) - .build(); - - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(ns); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName).build()); - - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(ns); - service.getSpec().setSelector(deployment.getSpec().getTemplate().getMetadata().getLabels()); - - ConfigMap existingConfigMap = - kubernetesClient - .configMaps() - .inNamespace(htmlConfigMap.getMetadata().getNamespace()) - .withName(htmlConfigMap.getMetadata().getName()) - .get(); - - log.info("Creating or updating ConfigMap {} in {}", htmlConfigMap.getMetadata().getName(), ns); - kubernetesClient.configMaps().inNamespace(ns).createOrReplace(htmlConfigMap); - log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); - kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(deployment); - - if (kubernetesClient.services().inNamespace(ns).withName(service.getMetadata().getName()) - .get() == null) { - log.info("Creating Service {} in {}", service.getMetadata().getName(), ns); - kubernetesClient.services().inNamespace(ns).createOrReplace(service); - } - - if (existingConfigMap != null) { - if (!StringUtils.equals( - existingConfigMap.getData().get("index.html"), - htmlConfigMap.getData().get("index.html"))) { - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(webPage)) - .delete(); - } - } + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); WebPageStatus status = new WebPageStatus(); - status.setHtmlConfigMap(htmlConfigMap.getMetadata().getName()); + + waitUntilConfigMapAvailable(webPage); + status.setHtmlConfigMap(configMapDR.getResource(webPage).get().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); webPage.setStatus(status); @@ -119,41 +69,112 @@ public UpdateControl reconcile(WebPage webPage, Context context) { return UpdateControl.updateStatus(webPage); } - @Override - public DeleteControl cleanup(WebPage nginx, Context context) { - log.info("Cleaning up for: {}", nginx.getMetadata().getName()); - - log.info("Deleting ConfigMap {}", configMapName(nginx)); - Resource configMap = - kubernetesClient - .configMaps() - .inNamespace(nginx.getMetadata().getNamespace()) - .withName(configMapName(nginx)); - if (configMap.get() != null) { - configMap.delete(); - } + // todo after implemented we can remove this method: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/924 + private void waitUntilConfigMapAvailable(WebPage webPage) { + await().atMost(Duration.ofSeconds(5)).until(() -> configMapDR.getResource(webPage).isPresent()); + } - log.info("Deleting Deployment {}", deploymentName(nginx)); - RollableScalableResource deployment = - kubernetesClient - .apps() - .deployments() - .inNamespace(nginx.getMetadata().getNamespace()) - .withName(deploymentName(nginx)); - if (deployment.get() != null) { - deployment.cascading(true).delete(); - } + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); + } - log.info("Deleting Service {}", serviceName(nginx)); - ServiceResource service = - kubernetesClient - .services() - .inNamespace(nginx.getMetadata().getNamespace()) - .withName(serviceName(nginx)); - if (service.get() != null) { - service.delete(); - } - return DeleteControl.defaultDelete(); + private void createDependentResources(KubernetesClient client) { + this.configMapDR = + new StandaloneKubernetesDependentResource<>( + client, + ConfigMap.class, + (WebPage webPage, Context context) -> { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + }) { + @Override + protected boolean match(ConfigMap actual, ConfigMap target, Context context) { + return StringUtils.equals( + actual.getData().get("index.html"), target.getData().get("index.html")); + } + + @Override + protected ConfigMap update( + ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + var cm = super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + return cm; + } + }; + configMapDR.setAssociatedSecondaryResourceIdentifier( + primary -> new ResourceID(configMapName(primary), primary.getMetadata().getNamespace())); + + this.deploymentDR = + new StandaloneKubernetesDependentResource<>( + client, + Deployment.class, + (webPage, context) -> { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + }) { + @Override + protected boolean match(Deployment actual, Deployment target, Context context) { + // todo comparator + return true; + } + }; + + this.serviceDR = + new StandaloneKubernetesDependentResource<>( + client, + Service.class, + (webPage, context) -> { + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + }) { + + protected boolean match(Service actual, Service target, Context context) { + // todo comparator + return true; + } + }; } private static String configMapName(WebPage nginx) { @@ -175,11 +196,4 @@ private T loadYaml(Class clazz, String yaml) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } - - @Override - public Optional updateErrorStatus(WebPage resource, RetryInfo retryInfo, - RuntimeException e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); - } } From 4e50fc1b086fa73ef19bf0667fe65f93f5eb3727 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 08:55:22 +0100 Subject: [PATCH 0014/1319] chore(deps): bump nexus-staging-maven-plugin from 1.6.8 to 1.6.10 (#930) Bumps nexus-staging-maven-plugin from 1.6.8 to 1.6.10. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4dc1a81a7c..920686da5c 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.1.0 3.0.1 - 1.6.8 + 1.6.10 2.8.2 2.5.2 5.0.0 From 05fdf02264398b571dc1e94a9d199edb7e7c7049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:13:53 +0100 Subject: [PATCH 0015/1319] chore(deps): bump maven-javadoc-plugin from 3.3.1 to 3.3.2 (#929) Bumps [maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.3.1...maven-javadoc-plugin-3.3.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 920686da5c..dfb5330fc5 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ 2.11 3.9.0 3.0.0-M5 - 3.3.1 + 3.3.2 3.2.0 3.2.1 3.2.2 From 088cebe0f996406509ca2d1e8dcfaa5b2fafb3d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 08:17:42 +0100 Subject: [PATCH 0016/1319] chore(deps): bump maven-compiler-plugin from 3.9.0 to 3.10.0 (#933) Bumps [maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.9.0 to 3.10.0. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.9.0...maven-compiler-plugin-3.10.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index dfb5330fc5..0719d0fb6e 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.8.2 2.11 - 3.9.0 + 3.10.0 3.0.0-M5 3.3.2 3.2.0 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index bd73c6390f..7d25dc2f5f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -105,7 +105,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.10.0 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 21c3fbc405..4381948214 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -90,7 +90,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.10.0 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 4ec7713b93..e79e07ac4f 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.10.0 From b1fa1e7a98984beb4c6b6c37cf1d54f76c001721 Mon Sep 17 00:00:00 2001 From: Junfan Zhang Date: Tue, 15 Feb 2022 15:19:01 +0800 Subject: [PATCH 0017/1319] Fix the resources type in the webpage example (#934) --- sample-operators/webpage/k8s/operator.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml index 926b2c31e2..cfdb1fe47c 100644 --- a/sample-operators/webpage/k8s/operator.yaml +++ b/sample-operators/webpage/k8s/operator.yaml @@ -92,7 +92,7 @@ rules: - apiGroups: - "sample.javaoperatorsdk" resources: - - webservers - - webservers/status + - webpages + - webpages/status verbs: - '*' From 110e64b19f09b4cee4f69b9c6f5f6c061b35629f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 15 Feb 2022 11:54:10 +0100 Subject: [PATCH 0018/1319] fix: logging error level and stack trace for k8s client (#936) --- .../processing/event/ReconciliationDispatcher.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 02c8f8cbb3..14ba12636a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -6,7 +6,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; @@ -49,13 +48,6 @@ public ReconciliationDispatcher(Controller controller) { public PostExecutionControl handleExecution(ExecutionScope executionScope) { try { return handleDispatch(executionScope); - } catch (KubernetesClientException e) { - log.info( - "Kubernetes exception {} {} during event processing, {} failed", - e.getCode(), - e.getMessage(), - executionScope); - return PostExecutionControl.exceptionDuringExecution(e); } catch (RuntimeException e) { log.error("Error during event processing {} failed.", executionScope, e); return PostExecutionControl.exceptionDuringExecution(e); From 323372a2e7b5b05f25a3af1162b420d5d95c19e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 08:24:35 +0100 Subject: [PATCH 0019/1319] chore(deps): bump nexus-staging-maven-plugin from 1.6.10 to 1.6.11 (#941) Bumps nexus-staging-maven-plugin from 1.6.10 to 1.6.11. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0719d0fb6e..fac0ce7e01 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.1.0 3.0.1 - 1.6.10 + 1.6.11 2.8.2 2.5.2 5.0.0 From 983ce46334e26babb8d53d83f719fa550b8b0c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 08:57:27 +0100 Subject: [PATCH 0020/1319] Configuration polish (#926) Co-authored-by: Chris Laprun --- .../api/config/ControllerConfiguration.java | 4 +- .../ControllerConfigurationOverrider.java | 43 ++++- .../DefaultControllerConfiguration.java | 8 +- .../operator/api/config/Utils.java | 11 ++ .../api/config/dependent/Dependent.java | 10 - .../DependentResourceConfiguration.java | 11 -- .../dependent/DependentResourceSpec.java | 29 +++ ...ernetesDependentResourceConfiguration.java | 66 ------- .../informer/InformerConfiguration.java | 24 +-- .../reconciler/ControllerConfiguration.java | 25 ++- .../EventSourceContextInjector.java | 5 - .../dependent/AbstractDependentResource.java | 4 +- .../api/reconciler/dependent/Dependent.java | 6 + .../dependent/DependentResource.java | 9 +- .../reconciler/dependent/DesiredSupplier.java | 10 - .../dependent/KubernetesClientAware.java | 7 + ...StandaloneKubernetesDependentResource.java | 63 ------ .../operator/processing/Controller.java | 1 - .../DependentResourceController.java | 66 ------- .../dependent/DependentResourceManager.java | 91 ++++----- ...KubernetesDependentResourceController.java | 56 ------ .../kubernetes}/KubernetesDependent.java | 13 +- .../KubernetesDependentResource.java | 121 ++++++------ .../KubernetesDependentResourceConfig.java | 62 ++++++ .../event/source/EventSourceContextAware.java | 8 - .../source/informer/InformerEventSource.java | 22 +-- .../informer/ManagedInformerEventSource.java | 1 - .../operator/api/config/UtilsTest.java | 30 +++ .../operator/junit/OperatorExtension.java | 33 +++- ...=> AnnotationControllerConfiguration.java} | 129 ++++++------- .../runtime/DefaultConfigurationService.java | 2 +- .../StandaloneDependentTestReconciler.java | 44 +++-- .../operator/sample/MySQLSchemaOperator.java | 15 +- .../sample/MySQLSchemaReconciler.java | 24 +-- .../operator/sample/ResourcePollerConfig.java | 21 ++ .../sample/SchemaDependentResource.java | 14 +- .../sample/SecretDependentResource.java | 20 +- .../sample/MySQLSchemaOperatorE2E.java | 115 +++++------ .../sample/DeploymentDependentResource.java | 21 +- .../sample/ServiceDependentResource.java | 7 +- .../operator/sample/TomcatReconciler.java | 7 +- .../operator/sample/WebPageReconciler.java | 181 ++++++++++-------- 42 files changed, 671 insertions(+), 768 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/config/dependent => processing/dependent/kubernetes}/KubernetesDependent.java (73%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler/dependent => processing/dependent/kubernetes}/KubernetesDependentResource.java (55%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java rename operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/{AnnotationConfiguration.java => AnnotationControllerConfiguration.java} (50%) create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index c077a7876d..40b32043af 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -7,7 +7,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -50,7 +50,7 @@ default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } - default List getDependentResources() { + default List getDependentResources() { return Collections.emptyList(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index b21ca22b46..141b58ded0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -3,9 +3,12 @@ import java.time.Duration; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class ControllerConfigurationOverrider { @@ -18,6 +21,7 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; + private List dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -27,8 +31,8 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { labelSelector = original.getLabelSelector(); customResourcePredicate = original.getEventFilter(); reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null); + dependentResourceSpecs = original.getDependentResources(); this.original = original; - } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -84,6 +88,41 @@ public ControllerConfigurationOverrider withReconciliationMaxInterval( return this; } + /** + * If a {@link DependentResourceSpec} already exists with the same dependentResourceClass it will + * be replaced. Otherwise, an exception is thrown; + * + * @param dependentResourceSpec to add or replace + */ + public void replaceDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { + var currentConfig = + findConfigForDependentResourceClass(dependentResourceSpec.getDependentResourceClass()); + if (currentConfig.isEmpty()) { + throw new IllegalStateException("Cannot find DependentResource config for class: " + + dependentResourceSpec.getDependentResourceClass()); + } + dependentResourceSpecs.remove(currentConfig.get()); + dependentResourceSpecs.add(dependentResourceSpec); + } + + public void addNewDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { + var currentConfig = + findConfigForDependentResourceClass(dependentResourceSpec.getDependentResourceClass()); + if (currentConfig.isPresent()) { + throw new IllegalStateException( + "Config already present for class: " + + dependentResourceSpec.getDependentResourceClass()); + } + dependentResourceSpecs.add(dependentResourceSpec); + } + + private Optional findConfigForDependentResourceClass( + Class dependentResourceClass) { + return dependentResourceSpecs.stream() + .filter(dc -> dc.getDependentResourceClass().equals(dependentResourceClass)) + .findFirst(); + } + public ControllerConfiguration build() { return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), @@ -98,7 +137,7 @@ public ControllerConfiguration build() { original.getResourceClass(), reconciliationMaxInterval, original.getConfigurationService(), - original.getDependentResources()); + dependentResourceSpecs); } public static ControllerConfigurationOverrider override( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 188311cfdb..8ce59dd01e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -7,7 +7,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration @@ -21,7 +21,7 @@ public class DefaultControllerConfiguration private final boolean generationAware; private final RetryConfiguration retryConfiguration; private final ResourceEventFilter resourceEventFilter; - private final List dependents; + private final List dependents; private final Duration reconciliationMaxInterval; // NOSONAR constructor is meant to provide all information @@ -38,7 +38,7 @@ public DefaultControllerConfiguration( Class resourceClass, Duration reconciliationMaxInterval, ConfigurationService service, - List dependents) { + List dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -102,7 +102,7 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { + public List getDependentResources() { return dependents; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index 3944cd3ecc..8d9990ee9c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -2,10 +2,12 @@ import java.io.IOException; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; import java.util.Properties; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,8 +71,17 @@ public static boolean debugThreadPool() { return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); } + public static Class getFirstTypeArgumentFromExtendedClass(Class clazz) { + Type type = clazz.getGenericSuperclass(); + return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } + public static Class getFirstTypeArgumentFromInterface(Class clazz) { ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; return (Class) type.getActualTypeArguments()[0]; } + + public static T valueOrDefault(C annotation, Function mapper, T defaultValue) { + return annotation == null ? defaultValue : mapper.apply(annotation); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java deleted file mode 100644 index 017f4363a6..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -public @interface Dependent { - - Class resourceType(); - - Class type(); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java deleted file mode 100644 index c0f428c12d..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -public interface DependentResourceConfiguration { - - Class> getDependentResourceClass(); - - Class getResourceClass(); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java new file mode 100644 index 0000000000..aea90b5e36 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.util.Optional; + +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +public class DependentResourceSpec, C> { + + private final Class dependentResourceClass; + + private final C dependentResourceConfig; + + public DependentResourceSpec(Class dependentResourceClass) { + this(dependentResourceClass, null); + } + + public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig) { + this.dependentResourceClass = dependentResourceClass; + this.dependentResourceConfig = dependentResourceConfig; + } + + public Class getDependentResourceClass() { + return dependentResourceClass; + } + + public Optional getDependentResourceConfiguration() { + return Optional.ofNullable(dependentResourceConfig); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java deleted file mode 100644 index 6872322e4f..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -import java.util.Set; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; - -public interface KubernetesDependentResourceConfiguration - extends InformerConfiguration, DependentResourceConfiguration { - - class DefaultKubernetesDependentResourceConfiguration - extends DefaultInformerConfiguration - implements KubernetesDependentResourceConfiguration { - - private final boolean owned; - private final Class> dependentResourceClass; - - protected DefaultKubernetesDependentResourceConfiguration( - ConfigurationService service, - String labelSelector, Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned, - Class> dependentResourceClass) { - super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, - skipUpdateEventPropagationIfNoChange, namespaces); - this.owned = owned; - this.dependentResourceClass = dependentResourceClass; - } - - public boolean isOwned() { - return owned; - } - - @Override - public Class> getDependentResourceClass() { - return dependentResourceClass; - } - - @Override - public Class getResourceClass() { - return super.getResourceClass(); - } - } - - static KubernetesDependentResourceConfiguration from( - InformerConfiguration cfg, boolean owned, - Class dependentResourceClass) { - return new DefaultKubernetesDependentResourceConfiguration(cfg.getConfigurationService(), - cfg.getLabelSelector(), cfg.getResourceClass(), cfg.getPrimaryResourcesRetriever(), - cfg.getAssociatedResourceIdentifier(), cfg.isSkipUpdateEventPropagationIfNoChange(), - cfg.getNamespaces(), owned, - (Class>) dependentResourceClass); - } - - boolean isOwned(); - - @Override - default Class getResourceClass() { - return InformerConfiguration.super.getResourceClass(); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index c4d747a990..acb5bd23c4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -22,13 +22,12 @@ class DefaultInformerConfiguration private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; private final AssociatedSecondaryResourceIdentifier

associatedWith; - private final boolean skipUpdateEventPropagationIfNoChange; protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, Class resourceClass, PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { + Set namespaces) { super(labelSelector, resourceClass, namespaces); setConfigurationService(service); this.secondaryToPrimaryResourcesIdSet = @@ -36,7 +35,6 @@ protected DefaultInformerConfiguration(ConfigurationService service, String labe Mappers.fromOwnerReference()); this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; } public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { @@ -47,22 +45,16 @@ public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier( return associatedWith; } - public boolean isSkipUpdateEventPropagationIfNoChange() { - return skipUpdateEventPropagationIfNoChange; - } } PrimaryResourcesRetriever getPrimaryResourcesRetriever(); AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier(); - boolean isSkipUpdateEventPropagationIfNoChange(); - class InformerConfigurationBuilder { private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; private AssociatedSecondaryResourceIdentifier

associatedWith; - private boolean skipUpdateEventPropagationIfNoChange = true; private Set namespaces; private String labelSelector; private final Class resourceClass; @@ -86,16 +78,6 @@ public InformerConfigurationBuilder withAssociatedSecondaryResourceIdentif return this; } - public InformerConfigurationBuilder withoutSkippingEventPropagationIfUnchanged() { - this.skipUpdateEventPropagationIfNoChange = false; - return this; - } - - public InformerConfigurationBuilder skippingEventPropagationIfUnchanged( - boolean skipIfUnchanged) { - this.skipUpdateEventPropagationIfNoChange = skipIfUnchanged; - return this; - } public InformerConfigurationBuilder withNamespaces(String... namespaces) { this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet(); @@ -115,7 +97,7 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector public InformerConfiguration build() { return new DefaultInformerConfiguration<>(configurationService, labelSelector, resourceClass, - secondaryToPrimaryResourcesIdSet, associatedWith, skipUpdateEventPropagationIfNoChange, + secondaryToPrimaryResourcesIdSet, associatedWith, namespaces); } } @@ -136,8 +118,6 @@ static InformerConfigurationBuild configuration.getConfigurationService()) .withNamespaces(configuration.getNamespaces()) .withLabelSelector(configuration.getLabelSelector()) - .skippingEventPropagationIfUnchanged( - configuration.isSkipUpdateEventPropagationIfNoChange()) .withAssociatedSecondaryResourceIdentifier( configuration.getAssociatedResourceIdentifier()) .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index 85eed38025..7d4c1fd0e8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -4,10 +4,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.concurrent.TimeUnit; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; -import io.javaoperatorsdk.operator.processing.dependent.DependentResourceController; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @@ -51,26 +49,27 @@ */ String labelSelector() default Constants.EMPTY_STRING; - /** * Optional list of classes providing custom {@link ResourceEventFilter}. * * @return the list of event filters. */ - @SuppressWarnings("rawtypes") Class[] eventFilters() default {}; + /** + * Optional configuration of the maximal interval the SDK will wait for a reconciliation request + * to happen before one will be automatically triggered. + * + * @return the maximal interval configuration + */ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval( - interval = 10, timeUnit = TimeUnit.HOURS); - + interval = 10); /** - * Optional list of classes providing {@link DependentResourceController} implementations - * encapsulating logic to handle the associated - * {@link io.javaoperatorsdk.operator.processing.Controller}'s reconciliation of dependent - * resources - * - * @return the list of {@link DependentResourceController} implementations + * Optional list of {@link Dependent} configurations which associate a resource type to a + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation + * + * @return the list of {@link Dependent} configurations */ Dependent[] dependents() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java deleted file mode 100644 index af57fe1b32..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -public interface EventSourceContextInjector { - void injectInto(EventSourceContext context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index ea8b8bc06b..beac9ef4bb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -3,8 +3,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -public abstract class AbstractDependentResource - implements DependentResource { +public abstract class AbstractDependentResource + implements DependentResource { @Override public void reconcile(P primary, Context context) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java new file mode 100644 index 0000000000..21f0b4f55b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +public @interface Dependent { + + Class type(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index b097554107..5208480166 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -3,24 +3,19 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -public interface DependentResource { +public interface DependentResource { Optional eventSource(EventSourceContext

context); - @SuppressWarnings("unchecked") - default Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); - } - void reconcile(P primary, Context context); void delete(P primary, Context context); Optional getResource(P primaryResource); + default void configureWith(C config) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java deleted file mode 100644 index b93d75950b..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.javaoperatorsdk.operator.api.reconciler.Context; - -@FunctionalInterface -public interface DesiredSupplier { - - R getDesired(P primary, Context context); - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java new file mode 100644 index 0000000000..b8ef888ec3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.client.KubernetesClient; + +public interface KubernetesClientAware { + void setKubernetesClient(KubernetesClient kubernetesClient); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java deleted file mode 100644 index d4d1e00dc2..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; - -// todo shorter name -public class StandaloneKubernetesDependentResource - extends KubernetesDependentResource { - - private final DesiredSupplier desiredSupplier; - private final Class resourceType; - private AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier = - ResourceID::fromResource; - private PrimaryResourcesRetriever primaryResourcesRetriever = Mappers.fromOwnerReference(); - - public StandaloneKubernetesDependentResource( - Class resourceType, DesiredSupplier desiredSupplier) { - this(null, resourceType, desiredSupplier); - } - - public StandaloneKubernetesDependentResource( - KubernetesClient client, Class resourceType, DesiredSupplier desiredSupplier) { - super(client); - this.desiredSupplier = desiredSupplier; - this.resourceType = resourceType; - } - - @Override - protected R desired(P primary, Context context) { - return desiredSupplier.getDesired(primary, context); - } - - public Class resourceType() { - return resourceType; - } - - public StandaloneKubernetesDependentResource setAssociatedSecondaryResourceIdentifier( - AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier) { - this.associatedSecondaryResourceIdentifier = associatedSecondaryResourceIdentifier; - return this; - } - - public StandaloneKubernetesDependentResource setPrimaryResourcesRetriever( - PrimaryResourcesRetriever primaryResourcesRetriever) { - this.primaryResourcesRetriever = primaryResourcesRetriever; - return this; - } - - @Override - protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { - return this.associatedSecondaryResourceIdentifier; - } - - @Override - protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { - return this.primaryResourcesRetriever; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index f3ad2dd8ec..89aabe0d9b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -217,7 +217,6 @@ public void start() throws OperatorException { eventSourceManager.getControllerResourceEventSource().getResourceCache(), configurationService(), kubernetesClient); - dependents.injectInto(context); prepareEventSources(context).forEach(eventSourceManager::registerEventSource); eventSourceManager.start(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java deleted file mode 100644 index e49a55a73a..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -@Ignore -public class DependentResourceController, D extends DependentResource> - implements DependentResource { - - private final D delegate; - private final C configuration; - - public DependentResourceController(D delegate, C configuration) { - this.delegate = delegate; - this.configuration = initConfiguration(delegate, configuration); - } - - protected C initConfiguration(D delegate, C configuration) { - // default implementation just returns the specified one - return configuration; - } - - @Override - public Class resourceType() { - return delegate.resourceType(); - } - - @Override - public void delete(P primary, Context context) { - delegate.delete(primary, context); - } - - @Override - public Optional getResource(P primaryResource) { - return delegate.getResource(primaryResource); - } - - - @Override - public Optional eventSource(EventSourceContext

context) { - return delegate.eventSource(context); - } - - - public C getConfiguration() { - return configuration; - } - - protected D delegate() { - return delegate; - } - - @Override - public void reconcile(P resource, Context context) { - delegate.reconcile(resource, context); - } - - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index ac791526fc..6f3a98b4dd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -3,65 +3,63 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) @Ignore -public class DependentResourceManager

implements EventSourceInitializer

, - EventSourceContextInjector, Reconciler

{ - private final Reconciler

reconciler; - private final ControllerConfiguration

configuration; - private List dependents; +public class DependentResourceManager

+ implements EventSourceInitializer

, Reconciler

{ + + private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); + private final Reconciler

reconciler; + private final ControllerConfiguration

controllerConfiguration; + private List dependents; public DependentResourceManager(Controller

controller) { this.reconciler = controller.getReconciler(); - this.configuration = controller.getConfiguration(); + this.controllerConfiguration = controller.getConfiguration(); } @Override public List prepareEventSources(EventSourceContext

context) { - final List configured = configuration.getDependentResources(); - dependents = new ArrayList<>(configured.size()); - - List sources = new ArrayList<>(configured.size() + 5); - configured.forEach(dependent -> { - final var dependentResourceController = from(dependent, context.getClient()); - dependents.add(dependentResourceController); - dependentResourceController.eventSource(context) - .ifPresent(es -> sources.add((EventSource) es)); - - }); - + final var dependentResources = controllerConfiguration.getDependentResources(); + final var sources = new ArrayList(dependentResources.size()); + dependents = + dependentResources.stream() + .map( + drc -> { + final var dependentResource = + from(drc, context.getClient()); + dependentResource + .eventSource(context) + .ifPresent(es -> sources.add((EventSource) es)); + return dependentResource; + }) + .collect(Collectors.toList()); return sources; } - @Override - public void injectInto(EventSourceContext context) { - if (reconciler instanceof EventSourceContextInjector) { - EventSourceContextInjector injector = (EventSourceContextInjector) reconciler; - injector.injectInto(context); - } - } - @Override public UpdateControl

reconcile(P resource, Context context) { initContextIfNeeded(resource, context); @@ -76,7 +74,6 @@ public DeleteControl cleanup(P resource, Context context) { return Reconciler.super.cleanup(resource, context); } - private void initContextIfNeeded(P resource, Context context) { if (reconciler instanceof ContextInitializer) { final var initializer = (ContextInitializer

) reconciler; @@ -84,29 +81,21 @@ private void initContextIfNeeded(P resource, Context context) { } } - private DependentResourceController from(DependentResourceConfiguration config, + private DependentResource from(DependentResourceSpec dependentResourceSpec, KubernetesClient client) { try { - final var dependentResource = - (DependentResource) config.getDependentResourceClass().getConstructor() - .newInstance(); - if (config instanceof KubernetesDependentResourceConfiguration) { - if (dependentResource instanceof KubernetesDependentResource) { - final var kubeDependentResource = (KubernetesDependentResource) dependentResource; - kubeDependentResource.setClient(client); - return new KubernetesDependentResourceController(kubeDependentResource, - (KubernetesDependentResourceConfiguration) config); - } else { - throw new IllegalArgumentException("A " - + KubernetesDependentResourceConfiguration.class.getCanonicalName() - + " must be associated to a " + KubernetesDependentResource.class.getCanonicalName()); - } - } else { - return new DependentResourceController(dependentResource, config); + DependentResource dependentResource = + (DependentResource) dependentResourceSpec.getDependentResourceClass() + .getConstructor().newInstance(); + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException - | IllegalAccessException e) { - throw new IllegalArgumentException(e); + dependentResourceSpec.getDependentResourceConfiguration() + .ifPresent(dependentResource::configureWith); + return dependentResource; + } catch (InstantiationException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException e) { + throw new IllegalStateException(e); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java deleted file mode 100644 index 733460ef31..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; - -@Ignore -public class KubernetesDependentResourceController - extends - DependentResourceController, KubernetesDependentResource> { - - public KubernetesDependentResourceController(KubernetesDependentResource delegate, - KubernetesDependentResourceConfiguration configuration) { - super(delegate, configuration); - } - - @SuppressWarnings("unchecked") - @Override - protected KubernetesDependentResourceConfiguration initConfiguration( - KubernetesDependentResource delegate, - KubernetesDependentResourceConfiguration configuration) { - // todo: check if we can validate that types actually match properly - final var associatedPrimaries = - (delegate instanceof PrimaryResourcesRetriever) - ? (PrimaryResourcesRetriever) delegate - : configuration.getPrimaryResourcesRetriever(); - final var associatedSecondary = - (delegate instanceof AssociatedSecondaryResourceIdentifier) - ? (AssociatedSecondaryResourceIdentifier

) delegate - : configuration.getAssociatedResourceIdentifier(); - - final var augmented = InformerConfiguration.from(configuration) - .withPrimaryResourcesRetriever(associatedPrimaries) - .withAssociatedSecondaryResourceIdentifier(associatedSecondary) - .build(); - return KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), - configuration.getDependentResourceClass()); - } - - @Override - public Optional eventSource(EventSourceContext

context) { - var informer = new InformerEventSource<>(getConfiguration(), context); - // todo have this implemented with nicer abstractions - delegate().setInformerEventSource(informer); - return super.eventSource(context); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java similarity index 73% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java index e7d2ad3158..c0922f569f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.config.dependent; +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,16 +11,14 @@ @Target({ElementType.TYPE}) public @interface KubernetesDependent { - boolean OWNED_DEFAULT = true; - boolean SKIP_UPDATE_DEFAULT = true; + boolean ADD_OWNER_REFERENCE_DEFAULT = true; - boolean owned() default OWNED_DEFAULT; - - boolean skipUpdateIfUnchanged() default SKIP_UPDATE_DEFAULT; + boolean addOwnerReference() default ADD_OWNER_REFERENCE_DEFAULT; /** * Specified which namespaces this Controller monitors for custom resources events. If no - * namespace is specified then the controller will monitor all namespaces by default. + * namespace is specified then the controller will monitor the namespaces configured for the + * controller. * * @return the list of namespaces this controller monitors */ @@ -34,4 +32,5 @@ * @return the label selector */ String labelSelector() default EMPTY_STRING; + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java similarity index 55% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index e8a4a6779e..a805c12b11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -1,6 +1,7 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import java.util.Optional; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,9 +9,13 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -19,25 +24,56 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public abstract class KubernetesDependentResource - extends AbstractDependentResource { + extends AbstractDependentResource + implements KubernetesClientAware { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); protected KubernetesClient client; - private boolean explicitDelete = false; - private boolean owned = true; private InformerEventSource informerEventSource; + private boolean addOwnerReference; - public KubernetesDependentResource() { - this(null); + @Override + public void configureWith(KubernetesDependentResourceConfig config) { + configureWith(config.getConfigurationService(), config.labelSelector(), + Set.of(config.namespaces()), config.addOwnerReference()); } - public KubernetesDependentResource(KubernetesClient client) { - this.client = client; + @SuppressWarnings("unchecked") + private void configureWith(ConfigurationService service, String labelSelector, + Set namespaces, boolean addOwnerReference) { + final var primaryResourcesRetriever = + (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this + : Mappers.fromOwnerReference(); + final AssociatedSecondaryResourceIdentifier

secondaryResourceIdentifier = + (this instanceof AssociatedSecondaryResourceIdentifier) + ? (AssociatedSecondaryResourceIdentifier

) this + : ResourceID::fromResource; + InformerConfiguration ic = + InformerConfiguration.from(service, resourceType()) + .withLabelSelector(labelSelector) + .withNamespaces(namespaces) + .withPrimaryResourcesRetriever(primaryResourcesRetriever) + .withAssociatedSecondaryResourceIdentifier(secondaryResourceIdentifier) + .build(); + this.addOwnerReference = addOwnerReference; + informerEventSource = new InformerEventSource<>(ic, client); + } + + /** + * Use to share informers between event more resources. + * + * @param informerEventSource informer to use + * @param addOwnerReference to the created resource + */ + public void configureWith(InformerEventSource informerEventSource, + boolean addOwnerReference) { + this.informerEventSource = informerEventSource; + this.addOwnerReference = addOwnerReference; } protected void beforeCreateOrUpdate(R desired, P primary) { - if (owned) { + if (addOwnerReference) { desired.addOwnerReference(primary); } } @@ -69,42 +105,17 @@ protected R update(R actual, R target, P primary, Context context) { .replace(target); } - @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Optional eventSource(EventSourceContext

context) { - if (informerEventSource != null) { - return Optional.of(informerEventSource); + if (informerEventSource == null) { + configureWith(context.getConfigurationService(), null, null, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + log.warn("Using default configuration for " + resourceType().getSimpleName() + + " KubernetesDependentResource, call configureWith to provide configuration"); } - var informerConfig = initInformerConfiguration(context); - informerEventSource = new InformerEventSource(informerConfig, context); return Optional.of(informerEventSource); } - @SuppressWarnings("unchecked") - private InformerConfiguration initInformerConfiguration(EventSourceContext

context) { - PrimaryResourcesRetriever associatedPrimaries = - (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this - : getDefaultPrimaryResourcesRetriever(); - - AssociatedSecondaryResourceIdentifier

associatedSecondary = - (this instanceof AssociatedSecondaryResourceIdentifier) - ? (AssociatedSecondaryResourceIdentifier

) this - : getDefaultAssociatedSecondaryResourceIdentifier(); - - return InformerConfiguration.from(context, resourceType()) - .withPrimaryResourcesRetriever(associatedPrimaries) - .withAssociatedSecondaryResourceIdentifier(associatedSecondary) - .build(); - } - - protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { - return ResourceID::fromResource; - } - - protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { - return Mappers.fromOwnerReference(); - } - public KubernetesDependentResource setInformerEventSource( InformerEventSource informerEventSource) { this.informerEventSource = informerEventSource; @@ -113,38 +124,24 @@ public KubernetesDependentResource setInformerEventSource( @Override public void delete(P primary, Context context) { - if (explicitDelete) { + if (addOwnerReference) { var resource = getResource(primary); resource.ifPresent(r -> client.resource(r).delete()); } } + @SuppressWarnings("unchecked") + protected Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + } + @Override public Optional getResource(P primaryResource) { return informerEventSource.getAssociated(primaryResource); } - public KubernetesDependentResource setClient(KubernetesClient client) { - this.client = client; - return this; - } - - - public KubernetesDependentResource setExplicitDelete(boolean explicitDelete) { - this.explicitDelete = explicitDelete; - return this; - } - - public boolean isExplicitDelete() { - return explicitDelete; - } - - public boolean isOwned() { - return owned; - } - - public KubernetesDependentResource setOwned(boolean owned) { - this.owned = owned; - return this; + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java new file mode 100644 index 0000000000..4464f3fd7d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; +import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT; + +public class KubernetesDependentResourceConfig { + + private boolean addOwnerReference = ADD_OWNER_REFERENCE_DEFAULT; + private String[] namespaces = new String[0]; + private String labelSelector = EMPTY_STRING; + private ConfigurationService configurationService; + + public KubernetesDependentResourceConfig() {} + + public KubernetesDependentResourceConfig(boolean addOwnerReference, String[] namespaces, + String labelSelector, ConfigurationService configurationService) { + this.addOwnerReference = addOwnerReference; + this.namespaces = namespaces; + this.labelSelector = labelSelector; + this.configurationService = configurationService; + } + + public KubernetesDependentResourceConfig setAddOwnerReference( + boolean addOwnerReference) { + this.addOwnerReference = addOwnerReference; + return this; + } + + public KubernetesDependentResourceConfig setNamespaces(String[] namespaces) { + this.namespaces = namespaces; + return this; + } + + public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) { + this.labelSelector = labelSelector; + return this; + } + + public KubernetesDependentResourceConfig setConfigurationService( + ConfigurationService configurationService) { + this.configurationService = configurationService; + return this; + } + + public boolean addOwnerReference() { + return addOwnerReference; + } + + public String[] namespaces() { + return namespaces; + } + + public String labelSelector() { + return labelSelector; + } + + public ConfigurationService getConfigurationService() { + return configurationService; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java deleted file mode 100644 index 95042d0c41..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; - -public interface EventSourceContextAware

{ - void initWith(EventSourceContext

context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 3def733bd8..b29209f9e5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -5,12 +5,12 @@ import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; public class InformerEventSource @@ -23,17 +23,12 @@ public InformerEventSource(InformerConfiguration configuration, EventSourceContext

context) { super(context.getClient().resources(configuration.getResourceClass()), configuration); this.configuration = configuration; + } - // init mappers with context if needed - final var primaryResourcesRetriever = configuration.getPrimaryResourcesRetriever(); - if (primaryResourcesRetriever instanceof EventSourceContextAware) { - ((EventSourceContextAware) primaryResourcesRetriever).initWith(context); - } - - final var associatedResourceIdentifier = configuration.getAssociatedResourceIdentifier(); - if (associatedResourceIdentifier instanceof EventSourceContextAware) { - ((EventSourceContextAware) associatedResourceIdentifier).initWith(context); - } + public InformerEventSource(InformerConfiguration configuration, + KubernetesClient client) { + super(client.resources(configuration.getResourceClass()), configuration); + this.configuration = configuration; } @Override @@ -50,9 +45,8 @@ public void onUpdate(T oldObject, T newObject) { return; } - if (configuration.isSkipUpdateEventPropagationIfNoChange() && - oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { + if (oldObject.getMetadata().getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { return; } propagateEvent(newObject); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index f0a1a1185d..1dcfa2aa0e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -34,7 +34,6 @@ public void start() { super.start(); } - @Override public void stop() { super.stop(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java new file mode 100644 index 0000000000..88dd8db469 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class UtilsTest { + + @Test + void getsFirstTypeArgumentFromExtendedClass() { + Class res = + Utils.getFirstTypeArgumentFromExtendedClass(TestKubernetesDependentResource.class); + assertThat(res).isEqualTo(Deployment.class); + } + + public static class TestKubernetesDependentResource + extends KubernetesDependentResource { + + @Override + protected Deployment desired(TestCustomResource primary, Context context) { + return null; + } + } +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 76d848c896..c445acf67c 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -15,6 +16,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.retry.Retry; @@ -37,7 +39,11 @@ private OperatorExtension( boolean preserveNamespaceOnError, boolean waitForNamespaceDeletion, boolean oneNamespacePerClass) { - super(configurationService, infrastructure, infrastructureTimeout, oneNamespacePerClass, + super( + configurationService, + infrastructure, + infrastructureTimeout, + oneNamespacePerClass, preserveNamespaceOnError, waitForNamespaceDeletion); this.reconcilers = reconcilers; @@ -86,6 +92,9 @@ protected void before(ExtensionContext context) { if (ref.retry != null) { oconfig.withRetry(ref.retry); } + if (ref.controllerConfigurationOverrider != null) { + ref.controllerConfigurationOverrider.accept(oconfig); + } final var kubernetesClient = getKubernetesClient(); try (InputStream is = getClass().getResourceAsStream(path)) { @@ -127,6 +136,19 @@ protected Builder() { this.reconcilers = new ArrayList<>(); } + public Builder withReconciler( + Reconciler value, Consumer configurationOverrider) { + return withReconciler(value, null, configurationOverrider); + } + + public Builder withReconciler( + Reconciler value, + Retry retry, + Consumer configurationOverrider) { + reconcilers.add(new ReconcilerSpec(value, retry, configurationOverrider)); + return this; + } + @SuppressWarnings("rawtypes") public Builder withReconciler(Reconciler value) { reconcilers.add(new ReconcilerSpec(value, null)); @@ -165,10 +187,19 @@ public OperatorExtension build() { private static class ReconcilerSpec { final Reconciler reconciler; final Retry retry; + final Consumer controllerConfigurationOverrider; public ReconcilerSpec(Reconciler reconciler, Retry retry) { + this(reconciler, retry, null); + } + + public ReconcilerSpec( + Reconciler reconciler, + Retry retry, + Consumer controllerConfigurationOverrider) { this.reconciler = reconciler; this.retry = retry; + this.controllerConfigurationOverrider = controllerConfigurationOverrider; } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java similarity index 50% rename from operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java index 19e31e932b..dd1e005fc1 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java @@ -1,37 +1,34 @@ package io.javaoperatorsdk.operator.config.runtime; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @SuppressWarnings("rawtypes") -public class AnnotationConfiguration +public class AnnotationControllerConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; - private List dependentConfigurations; - public AnnotationConfiguration(Reconciler reconciler) { + public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); } @@ -50,16 +47,17 @@ public String getFinalizer() { if (ReconcilerUtils.isFinalizerValid(finalizer)) { return finalizer; } else { - throw new IllegalArgumentException(finalizer - + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); + throw new IllegalArgumentException( + finalizer + + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); } } } @Override public boolean isGenerationAware() { - return valueOrDefault(annotation, ControllerConfiguration::generationAwareEventProcessing, - true); + return valueOrDefault( + annotation, ControllerConfiguration::generationAwareEventProcessing, true); } @Override @@ -99,8 +97,7 @@ public ResourceEventFilter getEventFilter() { Class>[] filterTypes = (Class>[]) valueOrDefault(annotation, - ControllerConfiguration::eventFilters, - new Object[] {}); + ControllerConfiguration::eventFilters, new Object[] {}); if (filterTypes.length > 0) { for (var filterType : filterTypes) { try { @@ -116,9 +113,7 @@ public ResourceEventFilter getEventFilter() { } } } - return answer != null - ? answer - : ResourceEventFilters.passthrough(); + return answer != null ? answer : ResourceEventFilters.passthrough(); } @Override @@ -127,8 +122,10 @@ public Optional reconciliationMaxInterval() { if (annotation.reconciliationMaxInterval().interval() <= 0) { return Optional.empty(); } - return Optional.of(Duration.of(annotation.reconciliationMaxInterval().interval(), - annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); + return Optional.of( + Duration.of( + annotation.reconciliationMaxInterval().interval(), + annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); } else { return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); } @@ -146,59 +143,41 @@ public static T valueOrDefault( } @Override - public List getDependentResources() { - if (dependentConfigurations == null) { - final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, - new Dependent[] {}); - if (dependents.length > 0) { - dependentConfigurations = new ArrayList<>(dependents.length); - for (Dependent dependent : dependents) { - final Class dependentType = dependent.type(); - final var resourceType = dependent.resourceType(); - - if (HasMetadata.class.isAssignableFrom(resourceType)) { - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, - this.getNamespaces().toArray(new String[0])); - final var labelSelector = - valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); - final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, - KubernetesDependent.OWNED_DEFAULT); - final var skipIfUnchanged = - valueOrDefault(kubeDependent, KubernetesDependent::skipUpdateIfUnchanged, - KubernetesDependent.SKIP_UPDATE_DEFAULT); - final var configuration = InformerConfiguration.from(service, resourceType) - .withLabelSelector(labelSelector) - .skippingEventPropagationIfUnchanged(skipIfUnchanged) - .withNamespaces(namespaces) - .build(); - - dependentConfigurations.add( - KubernetesDependentResourceConfiguration.from(configuration, owned, dependentType)); - } else { - dependentConfigurations.add(new DependentResourceConfiguration() { - @Override - public Class getDependentResourceClass() { - return dependentType; - } - - @Override - public Class getResourceClass() { - return resourceType; - } - }); - } - } + public List getDependentResources() { + final var dependents = + valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } + + List resourceSpecs = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + + final Class dependentType = dependent.type(); + if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); + final var labelSelector = + Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var addOwnerReference = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::addOwnerReference, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + KubernetesDependentResourceConfig config = + new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); + resourceSpecs.add(new DependentResourceSpec(dependentType, config)); } else { - dependentConfigurations = Collections.emptyList(); + resourceSpecs.add(new DependentResourceSpec(dependentType)); } } - return dependentConfigurations; - } - - private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { - return annotation == null ? defaultValue : mapper.apply(annotation); + return Arrays.stream(dependents) + .map(d -> new DependentResourceSpec(d.type())) + .collect(Collectors.toList()); } } - diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java index 44fc674028..21af48cfd1 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java @@ -30,7 +30,7 @@ ControllerConfiguration getConfigurationFor( if (config == null) { if (createIfNeeded) { // create the configuration on demand and register it - config = new AnnotationConfiguration<>(reconciler); + config = new AnnotationControllerConfiguration<>(reconciler); register(config); getLogger().info( "Created configuration for reconciler {} with name {}", diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 695cc30a31..b168d8fb47 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -9,8 +9,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -23,24 +23,10 @@ public class StandaloneDependentTestReconciler private KubernetesClient kubernetesClient; - StandaloneKubernetesDependentResource configMapDependent; + KubernetesDependentResource configMapDependent; public StandaloneDependentTestReconciler() { - configMapDependent = - new StandaloneKubernetesDependentResource<>(Deployment.class, (primary, context) -> { - Deployment deployment = loadYaml(Deployment.class, "nginx-deployment.yaml"); - deployment.getMetadata().setName(primary.getMetadata().getName()); - deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); - return deployment; - }) { - @Override - protected boolean match(Deployment actual, Deployment target, Context context) { - return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && - actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() - .equals( - target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - } - }; + configMapDependent = new DeploymentDependentResource(); } @Override @@ -59,7 +45,7 @@ public UpdateControl reconcile( @Override public void setKubernetesClient(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - configMapDependent.setClient(kubernetesClient); + configMapDependent.setKubernetesClient(kubernetesClient); } @Override @@ -74,4 +60,26 @@ private T loadYaml(Class clazz, String yaml) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } + + private class DeploymentDependentResource extends + KubernetesDependentResource { + + @Override + protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { + Deployment deployment = StandaloneDependentTestReconciler.this.loadYaml(Deployment.class, + "nginx-deployment.yaml"); + deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + return deployment; + } + + @Override + protected boolean match(Deployment actual, Deployment target, Context context) { + return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && + actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } + + } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index d9012637cf..4fddf8cadc 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -15,6 +15,9 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.config.runtime.AnnotationControllerConfiguration; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; @@ -32,7 +35,17 @@ public static void main(String[] args) throws IOException { new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) .build()); - operator.register(new MySQLSchemaReconciler(MySQLDbConfig.loadFromEnvironmentVars())); + + MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); + ControllerConfigurationOverrider configOverrider = + ControllerConfigurationOverrider + .override(new AnnotationControllerConfiguration(schemaReconciler)); + + configOverrider.replaceDependentResourceConfig( + new DependentResourceSpec(SchemaDependentResource.class, + new ResourcePollerConfig(500, MySQLDbConfig.loadFromEnvironmentVars()))); + + operator.register(schemaReconciler, configOverrider.build()); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 63c37ac3d7..8965ae2a89 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -6,17 +6,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.sample.schema.Schema; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -25,12 +22,12 @@ // todo handle this, should work with finalizer @ControllerConfiguration(finalizerName = NO_FINALIZER, dependents = { - @Dependent(resourceType = Secret.class, type = SecretDependentResource.class), - @Dependent(resourceType = Schema.class, type = SchemaDependentResource.class) + @Dependent(type = SecretDependentResource.class), + @Dependent(type = SchemaDependentResource.class) }) public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, - ContextInitializer, EventSourceContextInjector { + ContextInitializer { static final String SECRET_FORMAT = "%s-secret"; static final String USERNAME_FORMAT = "%s-user"; @@ -38,21 +35,10 @@ public class MySQLSchemaReconciler static final String MYSQL_SECRET_NAME = "mysql.secret.name"; static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; - static final String MYSQL_DB_CONFIG = "mysql.db.config"; static final String BUILT_SCHEMA = "built schema"; static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); - private final MySQLDbConfig mysqlDbConfig; - - public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { - this.mysqlDbConfig = mysqlDbConfig; - } - - @SuppressWarnings("rawtypes") - @Override - public void injectInto(EventSourceContext context) { - context.put(MYSQL_DB_CONFIG, mysqlDbConfig); - } + public MySQLSchemaReconciler() {} @Override public void initContext(MySQLSchema primary, Context context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java new file mode 100644 index 0000000000..924c3978d1 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample; + +public class ResourcePollerConfig { + + private final int pollPeriod; + private final MySQLDbConfig mySQLDbConfig; + + + public ResourcePollerConfig(int pollPeriod, MySQLDbConfig mySQLDbConfig) { + this.pollPeriod = pollPeriod; + this.mySQLDbConfig = mySQLDbConfig; + } + + public int getPollPeriod() { + return pollPeriod; + } + + public MySQLDbConfig getMySQLDbConfig() { + return mySQLDbConfig; + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 957f0b34ff..62ce1356a2 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -15,16 +15,22 @@ import static java.lang.String.format; -public class SchemaDependentResource extends AbstractDependentResource { +public class SchemaDependentResource extends + AbstractDependentResource { - private static final int POLL_PERIOD = 500; private MySQLDbConfig dbConfig; + private int pollPeriod; + + @Override + public void configureWith(ResourcePollerConfig config) { + this.dbConfig = config.getMySQLDbConfig(); + this.pollPeriod = config.getPollPeriod(); + } @Override public Optional eventSource(EventSourceContext context) { - dbConfig = context.getMandatory(MySQLSchemaReconciler.MYSQL_DB_CONFIG, MySQLDbConfig.class); return Optional.of(new PerResourcePollingEventSource<>( - new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), POLL_PERIOD, + new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, Schema.class)); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java index 4f6bd1759c..884cc6d2c9 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -18,8 +18,16 @@ private static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); } + // An alternative would be to override reconcile() method and exclude the update part. + @Override + protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { + throw new IllegalStateException( + "Secret should not be updated. Secret: " + target + " for custom resource: " + + primary); + } + @Override - public Secret desired(MySQLSchema schema, Context context) { + protected Secret desired(MySQLSchema schema, Context context) { return new SecretBuilder() .withNewMetadata() .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) @@ -32,14 +40,6 @@ public Secret desired(MySQLSchema schema, Context context) { .build(); } - // An alternative would be to override reconcile() method and exclude the update part. - @Override - protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { - throw new IllegalStateException( - "Secret should not be updated. Secret: " + target + " for custom resource: " - + primary); - } - @Override protected boolean match(Secret actual, Secret target, Context context) { return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index 0c1be5efc0..f433671c08 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; @@ -31,25 +32,20 @@ class MySQLSchemaOperatorE2E { - final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); + static final Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); - final static KubernetesClient client = new DefaultKubernetesClient(); + static final KubernetesClient client = new DefaultKubernetesClient(); - final static String MY_SQL_NS = "mysql"; + static final String MY_SQL_NS = "mysql"; private static List infrastructure = new ArrayList<>(); + static { - infrastructure - .add(new NamespaceBuilder() - .withNewMetadata() - .withName(MY_SQL_NS) - .endMetadata() - .build()); + infrastructure.add( + new NamespaceBuilder().withNewMetadata().withName(MY_SQL_NS).endMetadata().build()); try { - infrastructure.addAll( - client.load(new FileInputStream("k8s/mysql-deployment.yaml")).get()); - infrastructure.addAll( - client.load(new FileInputStream("k8s/mysql-service.yaml")).get()); + infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-deployment.yaml")).get()); + infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-service.yaml")).get()); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -63,20 +59,26 @@ boolean isLocal() { } @RegisterExtension - AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler( - new MySQLSchemaReconciler(new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) - .withInfrastructure(infrastructure) - .build() - : E2EOperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withOperatorDeployment( - client.load(new FileInputStream("k8s/operator.yaml")).get()) - .withInfrastructure(infrastructure) - .build(); - - + AbstractOperatorExtension operator = + isLocal() + ? OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler( + new MySQLSchemaReconciler(), + c -> { + c.replaceDependentResourceConfig( + new DependentResourceSpec( + SchemaDependentResource.class, + new ResourcePollerConfig( + 700, new MySQLDbConfig("127.0.0.1", "3306", "root", "password")))); + }) + .withInfrastructure(infrastructure) + .build() + : E2EOperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) + .withInfrastructure(infrastructure) + .build(); public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @@ -85,28 +87,23 @@ public void test() throws IOException { // Opening a port-forward if running locally LocalPortForward portForward = null; if (isLocal()) { - String podName = client - .pods() - .inNamespace(MY_SQL_NS) - .withLabel("app", "mysql") - .list() - .getItems() - .get(0) - .getMetadata() - .getName(); - - portForward = client - .pods() - .inNamespace(MY_SQL_NS) - .withName(podName) - .portForward(3306, 3306); + String podName = + client + .pods() + .inNamespace(MY_SQL_NS) + .withLabel("app", "mysql") + .list() + .getItems() + .get(0) + .getMetadata() + .getName(); + + portForward = client.pods().inNamespace(MY_SQL_NS).withName(podName).portForward(3306, 3306); } MySQLSchema testSchema = new MySQLSchema(); - testSchema.setMetadata(new ObjectMetaBuilder() - .withName("mydb1") - .withNamespace(operator.getNamespace()) - .build()); + testSchema.setMetadata( + new ObjectMetaBuilder().withName("mydb1").withNamespace(operator.getNamespace()).build()); testSchema.setSpec(new SchemaSpec()); testSchema.getSpec().setEncoding("utf8"); @@ -114,19 +111,25 @@ public void test() throws IOException { client.resource(testSchema).createOrReplace(); log.info("Waiting 2 minutes for expected resources to be created and updated"); - await().atMost(2, MINUTES).ignoreExceptions().untilAsserted(() -> { - MySQLSchema updatedSchema = - client.resources(MySQLSchema.class).inNamespace(operator.getNamespace()) - .withName(testSchema.getMetadata().getName()).get(); - assertThat(updatedSchema.getStatus(), is(notNullValue())); - assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED")); - assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue())); - assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue())); - }); + await() + .atMost(2, MINUTES) + .ignoreExceptions() + .untilAsserted( + () -> { + MySQLSchema updatedSchema = + client + .resources(MySQLSchema.class) + .inNamespace(operator.getNamespace()) + .withName(testSchema.getMetadata().getName()) + .get(); + assertThat(updatedSchema.getStatus(), is(notNullValue())); + assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED")); + assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue())); + assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue())); + }); if (portForward != null) { portForward.close(); } } - } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index b681ccf6b6..a0c22ce9b1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -3,18 +3,19 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") -public class DeploymentDependentResource - extends KubernetesDependentResource { +public class DeploymentDependentResource extends KubernetesDependentResource { - public DeploymentDependentResource() {} + private static String tomcatImage(Tomcat tomcat) { + return "tomcat:" + tomcat.getSpec().getVersion(); + } @Override - public Deployment desired(Tomcat tomcat, Context context) { + protected Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); @@ -31,7 +32,8 @@ public Deployment desired(Tomcat tomcat, Context context) { .withReplicas(tomcat.getSpec().getReplicas()) // set tomcat version .editTemplate() - // make sure label selector matches label (which has to be matched by service selector too) + // make sure label selector matches label (which has to be matched by service selector + // too) .editMetadata().addToLabels("app", tomcatName).endMetadata() .editSpec() .editFirstContainer().withImage(tomcatImage(tomcat)).endContainer() @@ -42,13 +44,10 @@ public Deployment desired(Tomcat tomcat, Context context) { return deployment; } - private String tomcatImage(Tomcat tomcat) { - return "tomcat:" + tomcat.getSpec().getVersion(); - } - @Override public boolean match(Deployment fetched, Deployment target, Context context) { return fetched.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() .equals(target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); } + } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 16a6aaff2e..df690f6abd 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -4,14 +4,12 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; public class ServiceDependentResource extends KubernetesDependentResource { - public ServiceDependentResource() {} - @Override - public Service desired(Tomcat tomcat, Context context) { + protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); return new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) .editMetadata() @@ -23,4 +21,5 @@ public Service desired(Tomcat tomcat, Context context) { .endSpec() .build(); } + } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index d2d2a6c96c..c84ff61eb1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -7,15 +7,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -26,8 +25,8 @@ @ControllerConfiguration( finalizerName = NO_FINALIZER, dependents = { - @Dependent(resourceType = Deployment.class, type = DeploymentDependentResource.class), - @Dependent(resourceType = Service.class, type = ServiceDependentResource.class) + @Dependent(type = DeploymentDependentResource.class), + @Dependent(type = ServiceDependentResource.class) }) public class TomcatReconciler implements Reconciler { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 28d6ed3037..cc9bce6c6d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -15,8 +15,9 @@ import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -30,9 +31,9 @@ public class WebPageReconciler private final KubernetesClient kubernetesClient; - private StandaloneKubernetesDependentResource configMapDR; - private StandaloneKubernetesDependentResource deploymentDR; - private StandaloneKubernetesDependentResource serviceDR; + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; @@ -42,9 +43,9 @@ public WebPageReconciler(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { List eventSources = new ArrayList<>(3); - configMapDR.eventSource(context).ifPresent(es -> eventSources.add(es)); - deploymentDR.eventSource(context).ifPresent(es -> eventSources.add(es)); - serviceDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + configMapDR.eventSource(context).ifPresent(eventSources::add); + deploymentDR.eventSource(context).ifPresent(eventSources::add); + serviceDR.eventSource(context).ifPresent(eventSources::add); return eventSources; } @@ -83,97 +84,71 @@ public Optional updateErrorStatus( } private void createDependentResources(KubernetesClient client) { - this.configMapDR = - new StandaloneKubernetesDependentResource<>( - client, - ConfigMap.class, - (WebPage webPage, Context context) -> { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - }) { - @Override - protected boolean match(ConfigMap actual, ConfigMap target, Context context) { - return StringUtils.equals( - actual.getData().get("index.html"), target.getData().get("index.html")); - } + this.configMapDR = new ConfigMapDependentResource(); + + this.deploymentDR = + new KubernetesDependentResource<>() { @Override - protected ConfigMap update( - ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - var cm = super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - return cm; + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; } - }; - configMapDR.setAssociatedSecondaryResourceIdentifier( - primary -> new ResourceID(configMapName(primary), primary.getMetadata().getNamespace())); - this.deploymentDR = - new StandaloneKubernetesDependentResource<>( - client, - Deployment.class, - (webPage, context) -> { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - }) { @Override protected boolean match(Deployment actual, Deployment target, Context context) { // todo comparator return true; } + + @Override + protected Class resourceType() { + return Deployment.class; + } }; this.serviceDR = - new StandaloneKubernetesDependentResource<>( - client, - Service.class, - (webPage, context) -> { - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - }) { + new KubernetesDependentResource<>() { + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } protected boolean match(Service actual, Service target, Context context) { // todo comparator return true; } + + @Override + protected Class resourceType() { + return Service.class; + } }; } @@ -196,4 +171,48 @@ private T loadYaml(Class clazz, String yaml) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } + + private class ConfigMapDependentResource extends KubernetesDependentResource + implements + AssociatedSecondaryResourceIdentifier { + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(WebPageReconciler.configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + protected boolean match(ConfigMap actual, ConfigMap target, Context context) { + return StringUtils.equals( + actual.getData().get("index.html"), target.getData().get("index.html")); + } + + @Override + protected ConfigMap update( + ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + var cm = super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + return cm; + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } + } } From 3302b99f7ecce4bc162503953294ddc769794c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 14:16:30 +0100 Subject: [PATCH 0021/1319] fix: not delete if add owner reference (#946) --- .../dependent/kubernetes/KubernetesDependentResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index a805c12b11..7a1a2e7afb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -124,7 +124,7 @@ public KubernetesDependentResource setInformerEventSource( @Override public void delete(P primary, Context context) { - if (addOwnerReference) { + if (!addOwnerReference) { var resource = getResource(primary); resource.ifPresent(r -> client.resource(r).delete()); } From f205fd47851158620528460f8cac732c5331d644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 17:06:46 +0100 Subject: [PATCH 0022/1319] Nicer config override (#944) --- .../io/javaoperatorsdk/operator/Operator.java | 19 ++++++++++++++++++- .../ControllerConfigurationOverrider.java | 18 ++++++++---------- .../dependent/DependentResourceManager.java | 4 ++-- .../operator/OperatorTest.java | 2 +- .../operator/sample/MySQLSchemaOperator.java | 13 +++---------- .../sample/SchemaDependentResource.java | 5 ++++- .../sample/MySQLSchemaOperatorE2E.java | 8 +++----- 7 files changed, 39 insertions(+), 30 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 5f71ec2747..2b15ab5cda 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +15,7 @@ import io.fabric8.kubernetes.client.Version; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; @@ -118,7 +120,7 @@ public void register(Reconciler reconciler) * * @param reconciler part of the reconciler to register * @param configuration the configuration with which we want to register the reconciler - * @param the {@code CustomResource} type associated with the reconciler + * @param the {@code HasMetadata} type associated with the reconciler * @throws OperatorException if a problem occurred during the registration process */ public void register(Reconciler reconciler, @@ -147,6 +149,21 @@ public void register(Reconciler reconciler, watchedNS); } + /** + * Method to register operator and facilitate configuration override. + * + * @param reconciler part of the reconciler to register + * @param configOverrider consumer to use to change config values + * @param the {@code HasMetadata} type associated with the reconciler + */ + public void register(Reconciler reconciler, + Consumer configOverrider) { + final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration); + configOverrider.accept(configToOverride); + register(reconciler, configToOverride.build()); + } + static class ControllerManager implements LifecycleAware { private final Map controllers = new HashMap<>(); private boolean started = false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index 141b58ded0..7fb919abb1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -88,21 +88,19 @@ public ControllerConfigurationOverrider withReconciliationMaxInterval( return this; } - /** - * If a {@link DependentResourceSpec} already exists with the same dependentResourceClass it will - * be replaced. Otherwise, an exception is thrown; - * - * @param dependentResourceSpec to add or replace - */ - public void replaceDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { + public void replaceDependentResourceConfig( + Class> dependentResourceClass, + Object dependentResourceConfig) { + var currentConfig = - findConfigForDependentResourceClass(dependentResourceSpec.getDependentResourceClass()); + findConfigForDependentResourceClass(dependentResourceClass); if (currentConfig.isEmpty()) { throw new IllegalStateException("Cannot find DependentResource config for class: " - + dependentResourceSpec.getDependentResourceClass()); + + dependentResourceClass); } dependentResourceSpecs.remove(currentConfig.get()); - dependentResourceSpecs.add(dependentResourceSpec); + dependentResourceSpecs + .add(new DependentResourceSpec(dependentResourceClass, dependentResourceConfig)); } public void addNewDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 6f3a98b4dd..102ffef088 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -50,7 +50,7 @@ public List prepareEventSources(EventSourceContext

context) { .map( drc -> { final var dependentResource = - from(drc, context.getClient()); + createAndConfigureFrom(drc, context.getClient()); dependentResource .eventSource(context) .ifPresent(es -> sources.add((EventSource) es)); @@ -81,7 +81,7 @@ private void initContextIfNeeded(P resource, Context context) { } } - private DependentResource from(DependentResourceSpec dependentResourceSpec, + private DependentResource createAndConfigureFrom(DependentResourceSpec dependentResourceSpec, KubernetesClient client) { try { DependentResource dependentResource = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index f532756f58..99bd7e64b9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -47,7 +47,7 @@ public void shouldRegisterReconcilerToController() { @Test @DisplayName("should throw `OperationException` when Configuration is null") public void shouldThrowOperatorExceptionWhenConfigurationIsNull() { - Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler, null)); + Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler)); } private static class FooCustomResource extends CustomResource { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index 4fddf8cadc..5a39408ec8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -15,9 +15,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.config.runtime.AnnotationControllerConfiguration; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; @@ -37,15 +34,11 @@ public static void main(String[] args) throws IOException { .build()); MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); - ControllerConfigurationOverrider configOverrider = - ControllerConfigurationOverrider - .override(new AnnotationControllerConfiguration(schemaReconciler)); - configOverrider.replaceDependentResourceConfig( - new DependentResourceSpec(SchemaDependentResource.class, + operator.register(schemaReconciler, + configOverrider -> configOverrider.replaceDependentResourceConfig( + SchemaDependentResource.class, new ResourcePollerConfig(500, MySQLDbConfig.loadFromEnvironmentVars()))); - - operator.register(schemaReconciler, configOverrider.build()); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 62ce1356a2..338a420b65 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -19,7 +19,7 @@ public class SchemaDependentResource extends AbstractDependentResource { private MySQLDbConfig dbConfig; - private int pollPeriod; + private int pollPeriod = 500; @Override public void configureWith(ResourcePollerConfig config) { @@ -29,6 +29,9 @@ public void configureWith(ResourcePollerConfig config) { @Override public Optional eventSource(EventSourceContext context) { + if (dbConfig == null) { + dbConfig = MySQLDbConfig.loadFromEnvironmentVars(); + } return Optional.of(new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, Schema.class)); diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index f433671c08..2ebece8aa2 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -17,7 +17,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; @@ -67,10 +66,9 @@ boolean isLocal() { new MySQLSchemaReconciler(), c -> { c.replaceDependentResourceConfig( - new DependentResourceSpec( - SchemaDependentResource.class, - new ResourcePollerConfig( - 700, new MySQLDbConfig("127.0.0.1", "3306", "root", "password")))); + SchemaDependentResource.class, + new ResourcePollerConfig( + 700, new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))); }) .withInfrastructure(infrastructure) .build() From dbf6c2e001ff5bba701f231babc9affc8c7433a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 17:22:22 +0100 Subject: [PATCH 0023/1319] feat: generic matcher for Kubernetes dependent resource (#945) --- .../operator/ReconcilerUtils.java | 13 ++++- .../api/config/ConfigurationService.java | 8 ++- .../kubernetes/DesiredValueMatcher.java | 43 ++++++++++++++++ .../KubernetesDependentResource.java | 33 ++++++++---- .../kubernetes/ResourceComparators.java | 20 ++++++++ .../dependent/kubernetes/ResourceMatcher.java | 9 ++++ .../kubernetes/DesiredValueMatcherTest.java | 50 +++++++++++++++++++ .../kubernetes/nginx-deployment.yaml | 21 ++++++++ .../StandaloneDependentTestReconciler.java | 10 ---- .../sample/DeploymentDependentResource.java | 7 --- .../operator/sample/WebPageReconciler.java | 13 +---- 11 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java create mode 100644 operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 6f4e19692b..491910d13e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -1,11 +1,14 @@ package io.javaoperatorsdk.operator; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Locale; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -111,7 +114,15 @@ public static Object getSpec(HasMetadata resource) { Method getSpecMethod = resource.getClass().getMethod("getSpec"); return getSpecMethod.invoke(resource); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new IllegalStateException(e); + throw new IllegalStateException("No spec found on resource", e); + } + } + + public static T loadYaml(Class clazz, Class loader, String yaml) { + try (InputStream is = loader.getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 2e4679efd3..6e5f44ebea 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -16,9 +16,9 @@ /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { - Cloner DEFAULT_CLONER = new Cloner() { - private final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + Cloner DEFAULT_CLONER = new Cloner() { @Override public HasMetadata clone(HasMetadata object) { try { @@ -122,4 +122,8 @@ default ExecutorService getExecutorService() { default boolean closeClientOnStop() { return true; } + + default ObjectMapper getObjectMapper() { + return OBJECT_MAPPER; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java new file mode 100644 index 0000000000..b8c9b1c7a5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.zjsonpatch.JsonDiff; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class DesiredValueMatcher implements ResourceMatcher { + + private final ObjectMapper objectMapper; + + public DesiredValueMatcher(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context) { + if (actualResource instanceof Secret) { + return ResourceComparators.compareSecretData((Secret) desiredResource, + (Secret) actualResource); + } + if (actualResource instanceof ConfigMap) { + return ResourceComparators.compareConfigMapData((ConfigMap) desiredResource, + (ConfigMap) actualResource); + } + // reflection will be replaced by this: + // https://github.com/fabric8io/kubernetes-client/issues/3816 + var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource)); + var actualSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(actualResource)); + var diffJsonPatch = JsonDiff.asJson(desiredSpecNode, actualSpecNode); + for (int i = 0; i < diffJsonPatch.size(); i++) { + String operation = diffJsonPatch.get(i).get("op").asText(); + if (!operation.equals("add")) { + return false; + } + } + return true; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 7a1a2e7afb..3b73a5e97f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -32,6 +31,7 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; + protected ResourceMatcher resourceMatcher; @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -40,7 +40,7 @@ public void configureWith(KubernetesDependentResourceConfig config) { } @SuppressWarnings("unchecked") - private void configureWith(ConfigurationService service, String labelSelector, + private void configureWith(ConfigurationService configService, String labelSelector, Set namespaces, boolean addOwnerReference) { final var primaryResourcesRetriever = (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this @@ -50,26 +50,28 @@ private void configureWith(ConfigurationService service, String labelSelector, ? (AssociatedSecondaryResourceIdentifier

) this : ResourceID::fromResource; InformerConfiguration ic = - InformerConfiguration.from(service, resourceType()) + InformerConfiguration.from(configService, resourceType()) .withLabelSelector(labelSelector) .withNamespaces(namespaces) .withPrimaryResourcesRetriever(primaryResourcesRetriever) .withAssociatedSecondaryResourceIdentifier(secondaryResourceIdentifier) .build(); - this.addOwnerReference = addOwnerReference; - informerEventSource = new InformerEventSource<>(ic, client); + configureWith(configService, new InformerEventSource<>(ic, client), addOwnerReference); } /** * Use to share informers between event more resources. - * + * + * @param configurationService get configs * @param informerEventSource informer to use * @param addOwnerReference to the created resource */ - public void configureWith(InformerEventSource informerEventSource, + public void configureWith(ConfigurationService configurationService, + InformerEventSource informerEventSource, boolean addOwnerReference) { this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; + initResourceMatcherIfNotSet(configurationService); } protected void beforeCreateOrUpdate(R desired, P primary) { @@ -79,8 +81,8 @@ protected void beforeCreateOrUpdate(R desired, P primary) { } @Override - protected boolean match(R actual, R target, Context context) { - return ReconcilerUtils.specsEqual(actual, target); + protected boolean match(R actualResource, R desiredResource, Context context) { + return resourceMatcher.match(actualResource, desiredResource, context); } @SuppressWarnings("unchecked") @@ -107,6 +109,7 @@ protected R update(R actual, R target, P primary, Context context) { @Override public Optional eventSource(EventSourceContext

context) { + initResourceMatcherIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -144,4 +147,16 @@ public Optional getResource(P primaryResource) { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } + + /** + * Override this method to configure resource matcher + * + * @param configurationService config service to mainly access object mapper + */ + protected void initResourceMatcherIfNotSet(ConfigurationService configurationService) { + if (resourceMatcher == null) { + resourceMatcher = new DesiredValueMatcher(configurationService.getObjectMapper()); + } + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java new file mode 100644 index 0000000000..037cb597f4 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; + +public class ResourceComparators { + + public static boolean compareConfigMapData(ConfigMap c1, ConfigMap c2) { + return Objects.equals(c1.getData(), c2.getData()) && + Objects.equals(c1.getBinaryData(), c2.getBinaryData()); + } + + public static boolean compareSecretData(Secret s1, Secret s2) { + return Objects.equals(s1.getType(), s2.getType()) && + Objects.equals(s1.getData(), s2.getData()) && + Objects.equals(s1.getStringData(), s2.getStringData()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java new file mode 100644 index 0000000000..0d86709ca0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface ResourceMatcher { + + boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context); +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java new file mode 100644 index 0000000000..d3e013d54d --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.ReconcilerUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +class DesiredValueMatcherTest { + + DesiredValueMatcher desiredValueMatcher = new DesiredValueMatcher(new ObjectMapper()); + + @Test + void checksIfDesiredValuesAreTheSame() { + var target1 = createDeployment(); + var desired1 = createDeployment(); + assertThat(desiredValueMatcher.match(target1, desired1, null)).isTrue(); + + var target2 = createDeployment(); + var desired2 = createDeployment(); + target2.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + assertThat(desiredValueMatcher.match(target2, desired2, null)) + .withFailMessage("Additive changes should be ok") + .isTrue(); + + var target3 = createDeployment(); + var desired3 = createDeployment(); + desired3.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + assertThat(desiredValueMatcher.match(target3, desired3, null)) + .withFailMessage("Removed value should not be ok") + .isFalse(); + + var target4 = createDeployment(); + var desired4 = createDeployment(); + target4.getSpec().setReplicas(2); + assertThat(desiredValueMatcher.match(target4, desired4, null)) + .withFailMessage("Changed values are not ok") + .isFalse(); + } + + Deployment createDeployment() { + Deployment deployment = + ReconcilerUtils.loadYaml( + Deployment.class, DesiredValueMatcherTest.class, "nginx-deployment.yaml"); + return deployment; + } +} diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml new file mode 100644 index 0000000000..dcf90a8fc7 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: "test" +spec: + progressDeadlineSeconds: 600 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: "test-dependent" + replicas: 1 + template: + metadata: + labels: + app: "test-dependent" + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index b168d8fb47..f0ac52458d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.Objects; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; @@ -72,14 +71,5 @@ protected Deployment desired(StandaloneDependentTestCustomResource primary, Cont deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; } - - @Override - protected boolean match(Deployment actual, Deployment target, Context context) { - return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && - actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() - .equals( - target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - } - } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index a0c22ce9b1..91da4523ed 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -43,11 +43,4 @@ protected Deployment desired(Tomcat tomcat, Context context) { .build(); return deployment; } - - @Override - public boolean match(Deployment fetched, Deployment target, Context context) { - return fetched.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() - .equals(target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - } - } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index cc9bce6c6d..c8a39a6274 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -62,7 +62,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { WebPageStatus status = new WebPageStatus(); waitUntilConfigMapAvailable(webPage); - status.setHtmlConfigMap(configMapDR.getResource(webPage).get().getMetadata().getName()); + status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); webPage.setStatus(status); @@ -114,12 +114,6 @@ protected Deployment desired(WebPage webPage, Context context) { return deployment; } - @Override - protected boolean match(Deployment actual, Deployment target, Context context) { - // todo comparator - return true; - } - @Override protected Class resourceType() { return Deployment.class; @@ -140,11 +134,6 @@ protected Service desired(WebPage webPage, Context context) { return service; } - protected boolean match(Service actual, Service target, Context context) { - // todo comparator - return true; - } - @Override protected Class resourceType() { return Service.class; From 6fc3b50419a29e2c908f5c17c5826dafcdf7e334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:38:18 +0100 Subject: [PATCH 0024/1319] chore(deps): bump micrometer-core from 1.8.2 to 1.8.3 (#951) Bumps [micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.8.2 to 1.8.3. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.8.2...v1.8.3) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fac0ce7e01..f8ee53d5bc 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.22.0 4.1.1 2.6.3 - 1.8.2 + 1.8.3 2.11 3.10.0 From 56921fe25231b9098bb2fec28ab0a6babfa50e55 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 17 Feb 2022 08:57:13 +0100 Subject: [PATCH 0025/1319] refactor: remove multiple loadYaml implementations (#950) * refactor: remove multiple loadYaml implementations * fix: provide proper class loader! :facepalm: --- .../StandaloneDependentTestReconciler.java | 16 +++------------- .../sample/DeploymentDependentResource.java | 4 +++- .../sample/ServiceDependentResource.java | 3 ++- .../operator/sample/TomcatReconciler.java | 11 ----------- .../operator/sample/WebPageReconciler.java | 16 +++------------- 5 files changed, 11 insertions(+), 39 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index f0ac52458d..c08e18cfdf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -1,12 +1,10 @@ package io.javaoperatorsdk.operator.sample.standalonedependent; -import java.io.IOException; -import java.io.InputStream; import java.util.List; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -52,21 +50,13 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } - private class DeploymentDependentResource extends KubernetesDependentResource { @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { - Deployment deployment = StandaloneDependentTestReconciler.this.loadYaml(Deployment.class, - "nginx-deployment.yaml"); + Deployment deployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 91da4523ed..3a68973165 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -3,6 +3,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -16,7 +17,8 @@ private static String tomcatImage(Tomcat tomcat) { @Override protected Deployment desired(Tomcat tomcat, Context context) { - Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); + Deployment deployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); deployment = new DeploymentBuilder(deployment) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index df690f6abd..a77d13b345 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -3,6 +3,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -11,7 +12,7 @@ public class ServiceDependentResource extends KubernetesDependentResource T loadYaml(Class clazz, String yaml) { - try (InputStream is = TomcatReconciler.class.getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index c8a39a6274..52dc44ebed 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.io.IOException; -import java.io.InputStream; import java.time.Duration; import java.util.*; @@ -12,7 +10,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -20,6 +17,7 @@ import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static org.awaitility.Awaitility.await; @@ -92,7 +90,7 @@ private void createDependentResources(KubernetesClient client) { @Override protected Deployment desired(WebPage webPage, Context context) { var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); deployment.getMetadata().setName(deploymentName); deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); @@ -125,7 +123,7 @@ protected Class resourceType() { @Override protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, "service.yaml"); + Service service = loadYaml(Service.class, getClass(), "service.yaml"); service.getMetadata().setName(serviceName(webPage)); service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); Map labels = new HashMap<>(); @@ -153,14 +151,6 @@ private static String serviceName(WebPage nginx) { return nginx.getMetadata().getName(); } - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } - private class ConfigMapDependentResource extends KubernetesDependentResource implements AssociatedSecondaryResourceIdentifier { From 7d1ecca996f64df90e3aecfffcce4277642d34f1 Mon Sep 17 00:00:00 2001 From: Rishi Kumar Ray <87641376+RishiKumarRay@users.noreply.github.com> Date: Thu, 17 Feb 2022 15:01:57 +0530 Subject: [PATCH 0026/1319] Tweet on release! (#913) --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b36caff8f1..d250e8610a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,21 @@ jobs: gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} nexus_username: ${{ secrets.OSSRH_USERNAME }} nexus_password: ${{ secrets.OSSRH_TOKEN }} + - uses: Eomm/why-don-t-you-tweet@v1 + # We don't want to tweet if the repository is not a public one + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + tweet-message: "New ${{ github.event.repository.name }} release ${{ github.event.release.tag_name }}! Try it will it is HOT! ${{ github.event.release.html_url }} #release" + env: + # Get your tokens from https://developer.twitter.com/apps + TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + + # This is separate job because there were issues with git after release step, was not able to commit changes. See history. update-working-version: From 73dc44543ea5bdf8d6185afee3e98639913d98f7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 17 Feb 2022 11:17:44 +0100 Subject: [PATCH 0027/1319] feat: extract interfaces for optional behavior of DependentResource (#949) * feat: extract EventSourceProvider interface to clean up common use case * feat: extract DependentResourceConfigurator interface * feat: showcase configuration change --- .../io/javaoperatorsdk/operator/Operator.java | 5 +++-- .../ControllerConfigurationOverrider.java | 5 +++-- .../dependent/DependentResourceSpec.java | 2 +- .../dependent/AbstractDependentResource.java | 2 +- .../dependent/DependentResource.java | 9 +------- .../DependentResourceConfigurator.java | 5 +++++ .../dependent/EventSourceProvider.java | 10 +++++++++ .../dependent/DependentResourceManager.java | 21 ++++++++++++------- .../KubernetesDependentResource.java | 11 ++++++---- .../StandaloneDependentTestReconciler.java | 2 +- .../operator/sample/MySQLSchemaOperator.java | 3 ++- .../sample/SchemaDependentResource.java | 14 ++++++++----- .../operator/sample/WebPageReconciler.java | 9 ++++---- 13 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 2b15ab5cda..adc3410405 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -157,7 +157,7 @@ public void register(Reconciler reconciler, * @param the {@code HasMetadata} type associated with the reconciler */ public void register(Reconciler reconciler, - Consumer configOverrider) { + Consumer> configOverrider) { final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration); configOverrider.accept(configToOverride); @@ -195,7 +195,8 @@ public synchronized void stop() { started = false; } - public synchronized void add(Controller controller) { + @SuppressWarnings("unchecked") + synchronized void add(Controller controller) { final var configuration = controller.getConfiguration(); final var resourceTypeName = ReconcilerUtils .getResourceTypeNameWithVersion(configuration.getResourceClass()); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index 7fb919abb1..239265af12 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -11,6 +11,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) public class ControllerConfigurationOverrider { private String finalizer; @@ -21,7 +22,7 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; - private List dependentResourceSpecs; + private final List dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -89,7 +90,7 @@ public ControllerConfigurationOverrider withReconciliationMaxInterval( } public void replaceDependentResourceConfig( - Class> dependentResourceClass, + Class> dependentResourceClass, Object dependentResourceConfig) { var currentConfig = diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java index aea90b5e36..1c07a5c752 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -4,7 +4,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -public class DependentResourceSpec, C> { +public class DependentResourceSpec, C> { private final Class dependentResourceClass; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index beac9ef4bb..bf40654812 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -4,7 +4,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; public abstract class AbstractDependentResource - implements DependentResource { + implements DependentResource { @Override public void reconcile(P primary, Context context) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 5208480166..fff45fa50f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -4,18 +4,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -public interface DependentResource { - - Optional eventSource(EventSourceContext

context); +public interface DependentResource { void reconcile(P primary, Context context); void delete(P primary, Context context); Optional getResource(P primaryResource); - - default void configureWith(C config) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java new file mode 100644 index 0000000000..b51de9f35f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +public interface DependentResourceConfigurator { + void configureWith(C config); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java new file mode 100644 index 0000000000..c9a2ca35a2 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public interface EventSourceProvider

{ + + EventSource eventSource(EventSourceContext

context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 102ffef088..e860cbe577 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -21,6 +21,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -49,11 +51,11 @@ public List prepareEventSources(EventSourceContext

context) { dependentResources.stream() .map( drc -> { - final var dependentResource = - createAndConfigureFrom(drc, context.getClient()); - dependentResource - .eventSource(context) - .ifPresent(es -> sources.add((EventSource) es)); + final var dependentResource = createAndConfigureFrom(drc, context.getClient()); + if (dependentResource instanceof EventSourceProvider) { + EventSourceProvider provider = (EventSourceProvider) dependentResource; + sources.add(provider.eventSource(context)); + } return dependentResource; }) .collect(Collectors.toList()); @@ -87,11 +89,16 @@ private DependentResource createAndConfigureFrom(DependentResourceSpec dependent DependentResource dependentResource = (DependentResource) dependentResourceSpec.getDependentResourceClass() .getConstructor().newInstance(); + if (dependentResource instanceof KubernetesClientAware) { ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } - dependentResourceSpec.getDependentResourceConfiguration() - .ifPresent(dependentResource::configureWith); + + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + dependentResourceSpec.getDependentResourceConfiguration() + .ifPresent(configurator::configureWith); + } return dependentResource; } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 3b73a5e97f..182ff9b361 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -14,6 +14,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -24,7 +26,8 @@ public abstract class KubernetesDependentResource extends AbstractDependentResource - implements KubernetesClientAware { + implements KubernetesClientAware, EventSourceProvider

, + DependentResourceConfigurator { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -61,7 +64,7 @@ private void configureWith(ConfigurationService configService, String labelSelec /** * Use to share informers between event more resources. - * + * * @param configurationService get configs * @param informerEventSource informer to use * @param addOwnerReference to the created resource @@ -108,7 +111,7 @@ protected R update(R actual, R target, P primary, Context context) { } @Override - public Optional eventSource(EventSourceContext

context) { + public EventSource eventSource(EventSourceContext

context) { initResourceMatcherIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, @@ -116,7 +119,7 @@ public Optional eventSource(EventSourceContext

context) { log.warn("Using default configuration for " + resourceType().getSimpleName() + " KubernetesDependentResource, call configureWith to provide configuration"); } - return Optional.of(informerEventSource); + return informerEventSource; } public KubernetesDependentResource setInformerEventSource( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index c08e18cfdf..d529f19c3d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -29,7 +29,7 @@ public StandaloneDependentTestReconciler() { @Override public List prepareEventSources( EventSourceContext context) { - return List.of(configMapDependent.eventSource(context).get()); + return List.of(configMapDependent.eventSource(context)); } @Override diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index 5a39408ec8..150c72b391 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -35,10 +35,11 @@ public static void main(String[] args) throws IOException { MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); + // override the default configuration operator.register(schemaReconciler, configOverrider -> configOverrider.replaceDependentResourceConfig( SchemaDependentResource.class, - new ResourcePollerConfig(500, MySQLDbConfig.loadFromEnvironmentVars()))); + new ResourcePollerConfig(300, MySQLDbConfig.loadFromEnvironmentVars()))); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 338a420b65..67c553268f 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -8,6 +8,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; import io.javaoperatorsdk.operator.sample.schema.Schema; @@ -15,8 +17,10 @@ import static java.lang.String.format; -public class SchemaDependentResource extends - AbstractDependentResource { +public class SchemaDependentResource + extends AbstractDependentResource + implements EventSourceProvider, + DependentResourceConfigurator { private MySQLDbConfig dbConfig; private int pollPeriod = 500; @@ -28,13 +32,13 @@ public void configureWith(ResourcePollerConfig config) { } @Override - public Optional eventSource(EventSourceContext context) { + public EventSource eventSource(EventSourceContext context) { if (dbConfig == null) { dbConfig = MySQLDbConfig.loadFromEnvironmentVars(); } - return Optional.of(new PerResourcePollingEventSource<>( + return new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, - Schema.class)); + Schema.class); } @Override diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 52dc44ebed..b3200903d1 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -40,11 +40,10 @@ public WebPageReconciler(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { - List eventSources = new ArrayList<>(3); - configMapDR.eventSource(context).ifPresent(eventSources::add); - deploymentDR.eventSource(context).ifPresent(eventSources::add); - serviceDR.eventSource(context).ifPresent(eventSources::add); - return eventSources; + return List.of( + configMapDR.eventSource(context), + deploymentDR.eventSource(context), + serviceDR.eventSource(context)); } @Override From c03bb77af7e2ac88af6fecccaf07e071d4d1acdb Mon Sep 17 00:00:00 2001 From: Matteo Mortari Date: Mon, 21 Feb 2022 08:48:50 +0100 Subject: [PATCH 0028/1319] fix README instructions for webpage sample (#959) k8s/crd.yml no longer exists as the CRDs for Samples are automatically generated by the fabric8 maven plugin --- sample-operators/webpage/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sample-operators/webpage/README.md b/sample-operators/webpage/README.md index ba17b7d962..8cc20a40f8 100644 --- a/sample-operators/webpage/README.md +++ b/sample-operators/webpage/README.md @@ -28,7 +28,11 @@ spec: The quickest way to try the operator is to run it on your local machine, while it connects to a local or remote Kubernetes cluster. When you start it, it will use the current kubectl context on your machine to connect to the cluster. -Before you run it you have to install the CRD on your cluster by running `kubectl apply -f k8s/crd.yaml` +Before you run it you have to install the CRD on your cluster by running +`kubectl apply -f target/classes/META-INF/fabric8/webpages.sample.javaoperatorsdk-v1.yml`. + +The CRD is generated automatically from your code by simply adding the `crd-generator-apt` +dependency to your `pom.xml` file. When the Operator is running you can create some Webserver Custom Resources. You can find a sample custom resource in `k8s/webpage.yaml`. You can create it by running `kubectl apply -f k8s/webpage.yaml` @@ -42,6 +46,8 @@ page. Otherwise you can change the service to a LoadBalancer (e.g on a public cl You can also try to change the HTML code in `k8s/webpage.yaml` and do another `kubectl apply -f k8s/webpage.yaml`. This should update the actual NGINX deployment with the new configuration. +If you want the Operator to be running as a deployment in your cluster, follow the below steps. + ### Build You can build the sample using `mvn jib:dockerBuild` this will produce a Docker image you can push to the registry @@ -49,5 +55,5 @@ of your choice. The JAR file is built using your local Maven and JDK and then co ### Deployment -1. Deploy the CRD: `kubectl apply -f k8s/crd.yaml` +1. Deploy the CRD: `kubectl apply -f target/classes/META-INF/fabric8/webpages.sample.javaoperatorsdk-v1.yml` 2. Deploy the operator: `kubectl apply -f k8s/operator.yaml` From 2832fc2eab3de6ebcee5c8f705b68f479fa87a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Feb 2022 15:41:08 +0100 Subject: [PATCH 0029/1319] feature: update preserves metadata (#960) --- .../operator/ReconcilerUtils.java | 9 ++++ .../KubernetesDependentResource.java | 30 ++++++++++---- .../ResourceUpdatePreProcessor.java | 33 +++++++++++++++ .../operator/ReconcilerUtilsTest.java | 13 ++++++ .../ResourceUpdatePreProcessorTest.java | 41 +++++++++++++++++++ 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 491910d13e..98a43492f9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -118,6 +118,15 @@ public static Object getSpec(HasMetadata resource) { } } + public static Object setSpec(HasMetadata resource, Object spec) { + try { + Method setSpecMethod = resource.getClass().getMethod("setSpec", spec.getClass()); + return setSpecMethod.invoke(resource, spec); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException("No spec found on resource", e); + } + } + public static T loadYaml(Class clazz, Class loader, String yaml) { try (InputStream is = loader.getResourceAsStream(yaml)) { return Serialization.unmarshal(is, clazz); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 182ff9b361..1204a3b844 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -35,6 +35,7 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; protected ResourceMatcher resourceMatcher; + protected ResourceUpdatePreProcessor resourceUpdatePreProcessor; @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -74,10 +75,10 @@ public void configureWith(ConfigurationService configurationService, boolean addOwnerReference) { this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; - initResourceMatcherIfNotSet(configurationService); + initResourceMatcherAndUpdatePreProcessorIfNotSet(configurationService); } - protected void beforeCreateOrUpdate(R desired, P primary) { + protected void beforeCreate(R desired, P primary) { if (addOwnerReference) { desired.addOwnerReference(primary); } @@ -93,7 +94,7 @@ protected boolean match(R actualResource, R desiredResource, Context context) { protected R create(R target, P primary, Context context) { log.debug("Creating target resource with type: " + "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); - beforeCreateOrUpdate(target, primary); + beforeCreate(target, primary); Class targetClass = (Class) target.getClass(); return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) .create(target); @@ -104,15 +105,15 @@ protected R create(R target, P primary, Context context) { protected R update(R actual, R target, P primary, Context context) { log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), ResourceID.fromResource(target)); - beforeCreateOrUpdate(target, primary); Class targetClass = (Class) target.getClass(); + var updatedActual = resourceUpdatePreProcessor.replaceSpecOnActual(actual, target); return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .replace(target); + .replace(updatedActual); } @Override public EventSource eventSource(EventSourceContext

context) { - initResourceMatcherIfNotSet(context.getConfigurationService()); + initResourceMatcherAndUpdatePreProcessorIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -156,10 +157,25 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { * * @param configurationService config service to mainly access object mapper */ - protected void initResourceMatcherIfNotSet(ConfigurationService configurationService) { + protected void initResourceMatcherAndUpdatePreProcessorIfNotSet( + ConfigurationService configurationService) { if (resourceMatcher == null) { resourceMatcher = new DesiredValueMatcher(configurationService.getObjectMapper()); } + if (resourceUpdatePreProcessor == null) { + resourceUpdatePreProcessor = + new ResourceUpdatePreProcessor<>(configurationService.getResourceCloner()); + } + } + + public KubernetesDependentResource setResourceMatcher(ResourceMatcher resourceMatcher) { + this.resourceMatcher = resourceMatcher; + return this; } + public KubernetesDependentResource setResourceUpdatePreProcessor( + ResourceUpdatePreProcessor resourceUpdatePreProcessor) { + this.resourceUpdatePreProcessor = resourceUpdatePreProcessor; + return this; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java new file mode 100644 index 0000000000..c7a16381a9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.Cloner; + +public class ResourceUpdatePreProcessor { + + private final Cloner cloner; + + public ResourceUpdatePreProcessor(Cloner cloner) { + this.cloner = cloner; + } + + public R replaceSpecOnActual(R actual, R desired) { + var clonedActual = cloner.clone(actual); + if (desired instanceof ConfigMap) { + ((ConfigMap) clonedActual).setData(((ConfigMap) desired).getData()); + ((ConfigMap) clonedActual).setBinaryData((((ConfigMap) desired).getBinaryData())); + return clonedActual; + } else if (desired instanceof Secret) { + ((Secret) clonedActual).setData(((Secret) desired).getData()); + ((Secret) clonedActual).setStringData(((Secret) desired).getStringData()); + return clonedActual; + } else { + var desiredSpec = ReconcilerUtils.getSpec(desired); + ReconcilerUtils.setSpec(clonedActual, desiredSpec); + return clonedActual; + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index be82caa36b..e137a2ff11 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -72,6 +72,19 @@ void getsSpecWithReflection() { assertThat(spec.getReplicas()).isEqualTo(5); } + @Test + void setsSpecWithReflection() { + Deployment deployment = new Deployment(); + deployment.setSpec(new DeploymentSpec()); + deployment.getSpec().setReplicas(5); + DeploymentSpec newSpec = new DeploymentSpec(); + newSpec.setReplicas(1); + + ReconcilerUtils.setSpec(deployment, newSpec); + + assertThat(deployment.getSpec().getReplicas()).isEqualTo(1); + } + private Deployment createTestDeployment() { Deployment deployment = new Deployment(); deployment.setSpec(new DeploymentSpec()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java new file mode 100644 index 0000000000..a4907f13b7 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResourceUpdatePreProcessorTest { + + ResourceUpdatePreProcessor resourceUpdatePreProcessor = + new ResourceUpdatePreProcessor<>(ConfigurationService.DEFAULT_CLONER); + + @Test + void preservesValues() { + var desired = createDeployment(); + var actual = createDeployment(); + actual.getMetadata().setLabels(new HashMap<>()); + actual.getMetadata().getLabels().put("additionalActualKey", "value"); + actual.getMetadata().setResourceVersion("1234"); + actual.getSpec().setRevisionHistoryLimit(5); + + var result = resourceUpdatePreProcessor.replaceSpecOnActual(actual, desired); + + assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value"); + assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234"); + assertThat(result.getSpec().getRevisionHistoryLimit()).isEqualTo(10); + } + + Deployment createDeployment() { + Deployment deployment = + ReconcilerUtils.loadYaml( + Deployment.class, ResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml"); + return deployment; + } + +} From 776ea2186c760476fdf352f35533c8b57e2b53e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Feb 2022 15:41:18 +0100 Subject: [PATCH 0030/1319] fix: no sonar fix experiment (#962) * fix: no sonar fix experiment * Update sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java Co-authored-by: Chris Laprun * fix: format security issue Co-authored-by: Chris Laprun --- .../operator/sample/MySQLSchemaReconciler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 8965ae2a89..5952a1508f 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -43,8 +43,9 @@ public MySQLSchemaReconciler() {} @Override public void initContext(MySQLSchema primary, Context context) { final var name = primary.getMetadata().getName(); - // NOSONAR we don't need cryptographically-strong randomness here - final var password = RandomStringUtils.randomAlphanumeric(16); + final var password = RandomStringUtils + .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here + final var secretName = String.format(SECRET_FORMAT, name); final var userName = String.format(USERNAME_FORMAT, name); From 0c7110af8db1be7eb6949c562d3189e522c4b3b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 08:36:49 +0100 Subject: [PATCH 0031/1319] chore(deps): bump nexus-staging-maven-plugin from 1.6.11 to 1.6.12 (#967) Bumps nexus-staging-maven-plugin from 1.6.11 to 1.6.12. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8ee53d5bc..420bf65c12 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.1.0 3.0.1 - 1.6.11 + 1.6.12 2.8.2 2.5.2 5.0.0 From 2aa50648a91b620e7bbc2b59a556c52e41679c56 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Wed, 23 Feb 2022 08:42:11 +0000 Subject: [PATCH 0032/1319] Utility around curl in Cluster (#966) --- .../operator/junit/InClusterCurl.java | 54 +++++++++++++++++++ .../operator/sample/TomcatOperatorE2E.java | 33 ++---------- 2 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java new file mode 100644 index 0000000000..927723209c --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.junit; + +import java.util.UUID; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.awaitility.Awaitility.await; + +public class InClusterCurl { + + private final KubernetesClient client; + private final String namespace; + + public InClusterCurl(https://github.com/KubernetesClient%20client,%20String%20namespace) { + this.client = client; + this.namespace = namespace; + } + + public String checkUrl(String url) { + String podName = KubernetesResourceUtil.sanitizeName("curl-" + UUID.randomUUID()); + try { + Pod curlPod = client.run().inNamespace(namespace) + .withRunConfig(new RunConfigBuilder() + .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withName(podName) + .withImage("curlimages/curl:7.78.0") + .withRestartPolicy("Never") + .build()) + .done(); + await("wait-for-curl-pod-run").atMost(2, MINUTES) + .until(() -> { + String phase = + client.pods().inNamespace(namespace).withName(podName).get() + .getStatus().getPhase(); + return phase.equals("Succeeded") || phase.equals("Failed"); + }); + + String curlOutput = + client.pods().inNamespace(namespace) + .withName(curlPod.getMetadata().getName()).getLog(); + + return curlOutput; + } finally { + client.pods().inNamespace(namespace).withName(podName).delete(); + await("wait-for-curl-pod-stop").atMost(1, MINUTES) + .until(() -> client.pods().inNamespace(namespace).withName(podName) + .get() == null); + } + } +} diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index 042f4973cf..3fecdb710e 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -10,10 +10,10 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.*; -import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.InClusterCurl; import io.javaoperatorsdk.operator.junit.OperatorExtension; import static java.util.concurrent.TimeUnit.MINUTES; @@ -107,42 +107,15 @@ public void test() { String url = "http://" + tomcat.getMetadata().getName() + "/" + webapp1.getSpec().getContextPath() + "/"; + var inClusterCurl = new InClusterCurl(https://github.com/client,%20operator.getNamespace()); log.info("Starting curl Pod and waiting 5 minutes for GET of {} to return 200", url); await("wait-for-webapp").atMost(6, MINUTES).untilAsserted(() -> { try { - - log.info("Starting curl Pod to test if webapp was deployed correctly"); - Pod curlPod = client.run().inNamespace(operator.getNamespace()) - .withRunConfig(new RunConfigBuilder() - .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) - .withName("curl") - .withImage("curlimages/curl:7.78.0") - .withRestartPolicy("Never") - .build()) - .done(); - log.info("Waiting for curl Pod to finish running"); - await("wait-for-curl-pod-run").atMost(2, MINUTES) - .until(() -> { - String phase = - client.pods().inNamespace(operator.getNamespace()).withName("curl").get() - .getStatus().getPhase(); - return phase.equals("Succeeded") || phase.equals("Failed"); - }); - - String curlOutput = - client.pods().inNamespace(operator.getNamespace()) - .withName(curlPod.getMetadata().getName()).getLog(); - log.info("Output from curl: '{}'", curlOutput); + var curlOutput = inClusterCurl.checkUrl(url); assertThat(curlOutput, equalTo("200")); } catch (KubernetesClientException ex) { throw new AssertionError(ex); - } finally { - log.info("Deleting curl Pod"); - client.pods().inNamespace(operator.getNamespace()).withName("curl").delete(); - await("wait-for-curl-pod-stop").atMost(1, MINUTES) - .until(() -> client.pods().inNamespace(operator.getNamespace()).withName("curl") - .get() == null); } }); } From ae4ad56d7dd5be43ee04127a6e6d33401e5bbfcb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 24 Feb 2022 12:56:59 +0100 Subject: [PATCH 0033/1319] feat: dependents use traits to specify which features they support (#963) Also fixes an issue with dependent spec creation. --- .../operator/api/reconciler/Context.java | 4 + .../api/reconciler/DefaultContext.java | 8 ++ .../dependent/AbstractDependentResource.java | 82 +++++++++++--- .../api/reconciler/dependent/Creator.java | 12 ++ .../api/reconciler/dependent/Deleter.java | 12 ++ .../dependent/DependentResource.java | 2 +- .../api/reconciler/dependent/Matcher.java | 7 ++ .../dependent/ResourceUpdatePreProcessor.java | 9 ++ .../api/reconciler/dependent/Updater.java | 18 +++ ... => GenericKubernetesResourceMatcher.java} | 31 ++--- .../GenericResourceUpdatePreProcessor.java | 53 +++++++++ .../KubernetesDependentResource.java | 107 +++++++----------- .../dependent/kubernetes/ResourceMatcher.java | 9 -- .../ResourceUpdatePreProcessor.java | 33 ------ .../source/informer/InformerEventSource.java | 10 -- ...GenericKubernetesResourceMatcherTest.java} | 28 +++-- ...enericResourceUpdatePreProcessorTest.java} | 27 +++-- .../AnnotationControllerConfiguration.java | 66 +++++------ ...AnnotationControllerConfigurationTest.java | 58 ++++++++++ .../sample/readonly/ConfigMapReader.java | 11 ++ .../sample/readonly/ReadOnlyDependent.java | 7 ++ .../StandaloneDependentTestReconciler.java | 16 +-- .../sample/SchemaDependentResource.java | 21 ++-- .../sample/SecretDependentResource.java | 13 +-- .../sample/DeploymentDependentResource.java | 5 +- .../sample/ServiceDependentResource.java | 5 +- .../operator/sample/WebPageReconciler.java | 11 +- 27 files changed, 430 insertions(+), 235 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{DesiredValueMatcher.java => GenericKubernetesResourceMatcher.java} (50%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{DesiredValueMatcherTest.java => GenericKubernetesResourceMatcherTest.java} (53%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{ResourceUpdatePreProcessorTest.java => GenericResourceUpdatePreProcessorTest.java} (51%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 6870fef80e..6948d0f6a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,6 +2,8 @@ import java.util.Optional; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + public interface Context extends AttributeHolder { Optional getRetryInfo(); @@ -18,4 +20,6 @@ default T getMandatory(Object key, Class expectedType) { "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + ") is missing or not of the expected type")); } + + ConfigurationService getConfigurationService(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index c2553a5d57..46ee5a3fcf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.Controller; public class DefaultContext

extends MapAttributeHolder implements Context { @@ -10,11 +11,13 @@ public class DefaultContext

extends MapAttributeHolder im private final RetryInfo retryInfo; private final Controller

controller; private final P primaryResource; + private final ConfigurationService configurationService; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; + this.configurationService = controller.getConfiguration().getConfigurationService(); } @Override @@ -28,4 +31,9 @@ public Optional getSecondaryResource(Class expectedType, String eventS .getResourceEventSourceFor(expectedType, eventSourceName) .flatMap(es -> es.getAssociated(primaryResource)); } + + @Override + public ConfigurationService getConfigurationService() { + return configurationService; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index bf40654812..456e9339de 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -1,31 +1,87 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -public abstract class AbstractDependentResource +public abstract class AbstractDependentResource implements DependentResource { + private static final Logger log = LoggerFactory.getLogger(AbstractDependentResource.class); + + private final boolean creatable = this instanceof Creator; + private final boolean updatable = this instanceof Updater; + private final boolean deletable = this instanceof Deleter; + protected Creator creator; + protected Updater updater; + protected Deleter

deleter; + + @SuppressWarnings("unchecked") + public AbstractDependentResource() { + init(Creator.NOOP, Updater.NOOP, Deleter.NOOP); + } + + @SuppressWarnings({"unchecked"}) + protected void init(Creator defaultCreator, Updater defaultUpdater, + Deleter

defaultDeleter) { + creator = creatable ? (Creator) this : defaultCreator; + updater = updatable ? (Updater) this : defaultUpdater; + deleter = deletable ? (Deleter

) this : defaultDeleter; + } @Override public void reconcile(P primary, Context context) { - var actual = getResource(primary); - var desired = desired(primary, context); - if (actual.isEmpty()) { - create(desired, primary, context); - } else { - if (!match(actual.get(), desired, context)) { - update(actual.get(), desired, primary, context); + final var creatable = isCreatable(primary, context); + final var updatable = isUpdatable(primary, context); + if (creatable || updatable) { + var maybeActual = getResource(primary); + var desired = desired(primary, context); + if (maybeActual.isEmpty()) { + if (creatable) { + log.debug("Creating dependent {} for primary {}", desired, primary); + creator.create(desired, primary, context); + } + } else { + final var actual = maybeActual.get(); + if (updatable && !updater.match(actual, desired, context)) { + log.debug("Updating dependent {} for primary {}", desired, primary); + updater.update(actual, desired, primary, context); + } else { + log.debug("Update skipped for dependent {} as it matched the existing one", desired); + } } + } else { + log.debug( + "Dependent {} is read-only, implement Creator and/or Updater interfaces to modify it", + getClass().getSimpleName()); } } - protected abstract R desired(P primary, Context context); + @Override + public void delete(P primary, Context context) { + if (isDeletable(primary, context)) { + deleter.delete(primary, context); + } + } - protected abstract boolean match(R actual, R target, Context context); + protected R desired(P primary, Context context) { + throw new IllegalStateException( + "desired method must be implemented if this DependentResource can be created and/or updated"); + } - protected abstract R create(R target, P primary, Context context); + @SuppressWarnings("unused") + protected boolean isCreatable(P primary, Context context) { + return creatable; + } - // the actual needed to copy/preserve new labels or annotations - protected abstract R update(R actual, R target, P primary, Context context); + @SuppressWarnings("unused") + protected boolean isUpdatable(P primary, Context context) { + return updatable; + } + @SuppressWarnings("unused") + protected boolean isDeletable(P primary, Context context) { + return deletable; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java new file mode 100644 index 0000000000..f685a5edb4 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@SuppressWarnings("rawtypes") +public interface Creator { + Creator NOOP = (desired, primary, context) -> { + }; + + void create(R desired, P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java new file mode 100644 index 0000000000..7649460934 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@SuppressWarnings("rawtypes") +public interface Deleter

{ + Deleter NOOP = (primary, context) -> { + }; + + void delete(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index fff45fa50f..09394f11b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -8,7 +8,7 @@ public interface DependentResource { void reconcile(P primary, Context context); - void delete(P primary, Context context); + default void delete(P primary, Context context) {} Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java new file mode 100644 index 0000000000..ad82daf661 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface Matcher { + boolean match(R actualResource, R desiredResource, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java new file mode 100644 index 0000000000..df2541aae9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface ResourceUpdatePreProcessor { + + R replaceSpecOnActual(R actual, R desired, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java new file mode 100644 index 0000000000..6984f788f8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@SuppressWarnings("rawtypes") +public interface Updater { + Updater NOOP = (actual, desired, primary, context) -> { + }; + + void update(R actual, R desired, P primary, Context context); + + default boolean match(R actualResource, R desiredResource, Context context) { + return Objects.equals(actualResource, desiredResource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java similarity index 50% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index b8c9b1c7a5..1c3c7d0822 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -6,27 +6,28 @@ import io.fabric8.zjsonpatch.JsonDiff; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; -import com.fasterxml.jackson.databind.ObjectMapper; +public class GenericKubernetesResourceMatcher implements Matcher { -public class DesiredValueMatcher implements ResourceMatcher { + private GenericKubernetesResourceMatcher() {} - private final ObjectMapper objectMapper; - - public DesiredValueMatcher(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Matcher matcherFor(Class resourceType) { + if (Secret.class.isAssignableFrom(resourceType)) { + return (actual, desired, context) -> ResourceComparators.compareSecretData((Secret) desired, + (Secret) actual); + } else if (ConfigMap.class.isAssignableFrom(resourceType)) { + return (actual, desired, context) -> ResourceComparators + .compareConfigMapData((ConfigMap) desired, (ConfigMap) actual); + } else { + return new GenericKubernetesResourceMatcher(); + } } @Override - public boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context) { - if (actualResource instanceof Secret) { - return ResourceComparators.compareSecretData((Secret) desiredResource, - (Secret) actualResource); - } - if (actualResource instanceof ConfigMap) { - return ResourceComparators.compareConfigMapData((ConfigMap) desiredResource, - (ConfigMap) actualResource); - } + public boolean match(R actualResource, R desiredResource, Context context) { + final var objectMapper = context.getConfigurationService().getObjectMapper(); // reflection will be replaced by this: // https://github.com/fabric8io/kubernetes-client/issues/3816 var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource)); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java new file mode 100644 index 0000000000..b33719f64e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; + +public abstract class GenericResourceUpdatePreProcessor implements + ResourceUpdatePreProcessor { + + private GenericResourceUpdatePreProcessor() {} + + @SuppressWarnings("unchecked") + public static ResourceUpdatePreProcessor processorFor( + Class resourceType) { + if (Secret.class.isAssignableFrom(resourceType)) { + return (ResourceUpdatePreProcessor) new GenericResourceUpdatePreProcessor() { + @Override + protected void updateClonedActual(Secret actual, Secret desired) { + actual.setData(desired.getData()); + actual.setStringData(desired.getStringData()); + } + }; + } else if (ConfigMap.class.isAssignableFrom(resourceType)) { + return (ResourceUpdatePreProcessor) new GenericResourceUpdatePreProcessor() { + + @Override + protected void updateClonedActual(ConfigMap actual, ConfigMap desired) { + actual.setData(desired.getData()); + actual.setBinaryData((desired.getBinaryData())); + } + }; + } else { + return new GenericResourceUpdatePreProcessor<>() { + @Override + protected void updateClonedActual(R actual, R desired) { + var desiredSpec = ReconcilerUtils.getSpec(desired); + ReconcilerUtils.setSpec(actual, desiredSpec); + } + }; + } + } + + public R replaceSpecOnActual(R actual, R desired, Context context) { + var clonedActual = context.getConfigurationService().getResourceCloner().clone(actual); + updateClonedActual(clonedActual, desired); + return clonedActual; + } + + protected abstract void updateClonedActual(R actual, R desired); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 1204a3b844..a0513b1234 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -7,7 +7,10 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -17,6 +20,8 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -25,7 +30,7 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public abstract class KubernetesDependentResource - extends AbstractDependentResource + extends AbstractDependentResource implements KubernetesClientAware, EventSourceProvider

, DependentResourceConfigurator { @@ -34,8 +39,18 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; - protected ResourceMatcher resourceMatcher; - protected ResourceUpdatePreProcessor resourceUpdatePreProcessor; + private final Matcher matcher; + private final ResourceUpdatePreProcessor processor; + + @SuppressWarnings("unchecked") + public KubernetesDependentResource() { + init(this::create, this::update, this::delete); + matcher = this instanceof Matcher ? (Matcher) this + : GenericKubernetesResourceMatcher.matcherFor(resourceType()); + processor = this instanceof ResourceUpdatePreProcessor + ? (ResourceUpdatePreProcessor) this + : GenericResourceUpdatePreProcessor.processorFor(resourceType()); + } @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -75,45 +90,44 @@ public void configureWith(ConfigurationService configurationService, boolean addOwnerReference) { this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; - initResourceMatcherAndUpdatePreProcessorIfNotSet(configurationService); } - protected void beforeCreate(R desired, P primary) { - if (addOwnerReference) { - desired.addOwnerReference(primary); - } + public void create(R target, P primary, Context context) { + prepare(target, primary, "Creating").create(target); } - @Override - protected boolean match(R actualResource, R desiredResource, Context context) { - return resourceMatcher.match(actualResource, desiredResource, context); + public void update(R actual, R target, P primary, Context context) { + var updatedActual = processor.replaceSpecOnActual(actual, target, context); + prepare(target, primary, "Updating").replace(updatedActual); } - @SuppressWarnings("unchecked") - @Override - protected R create(R target, P primary, Context context) { - log.debug("Creating target resource with type: " + - "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); - beforeCreate(target, primary); - Class targetClass = (Class) target.getClass(); - return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .create(target); + public boolean match(R actualResource, R desiredResource, Context context) { + return matcher.match(actualResource, desiredResource, context); + } + + public void delete(P primary, Context context) { + if (!addOwnerReference) { + var resource = getResource(primary); + resource.ifPresent(r -> client.resource(r).delete()); + } } @SuppressWarnings("unchecked") - @Override - protected R update(R actual, R target, P primary, Context context) { - log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), - ResourceID.fromResource(target)); - Class targetClass = (Class) target.getClass(); - var updatedActual = resourceUpdatePreProcessor.replaceSpecOnActual(actual, target); - return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .replace(updatedActual); + protected NonNamespaceOperation, Resource> prepare(R desired, + P primary, String actionName) { + log.debug("{} target resource with type: {}, with id: {}", + actionName, + desired.getClass(), + ResourceID.fromResource(desired)); + if (addOwnerReference) { + desired.addOwnerReference(primary); + } + Class targetClass = (Class) desired.getClass(); + return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace()); } @Override public EventSource eventSource(EventSourceContext

context) { - initResourceMatcherAndUpdatePreProcessorIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -129,14 +143,6 @@ public KubernetesDependentResource setInformerEventSource( return this; } - @Override - public void delete(P primary, Context context) { - if (!addOwnerReference) { - var resource = getResource(primary); - resource.ifPresent(r -> client.resource(r).delete()); - } - } - @SuppressWarnings("unchecked") protected Class resourceType() { return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); @@ -151,31 +157,4 @@ public Optional getResource(P primaryResource) { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } - - /** - * Override this method to configure resource matcher - * - * @param configurationService config service to mainly access object mapper - */ - protected void initResourceMatcherAndUpdatePreProcessorIfNotSet( - ConfigurationService configurationService) { - if (resourceMatcher == null) { - resourceMatcher = new DesiredValueMatcher(configurationService.getObjectMapper()); - } - if (resourceUpdatePreProcessor == null) { - resourceUpdatePreProcessor = - new ResourceUpdatePreProcessor<>(configurationService.getResourceCloner()); - } - } - - public KubernetesDependentResource setResourceMatcher(ResourceMatcher resourceMatcher) { - this.resourceMatcher = resourceMatcher; - return this; - } - - public KubernetesDependentResource setResourceUpdatePreProcessor( - ResourceUpdatePreProcessor resourceUpdatePreProcessor) { - this.resourceUpdatePreProcessor = resourceUpdatePreProcessor; - return this; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java deleted file mode 100644 index 0d86709ca0..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface ResourceMatcher { - - boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java deleted file mode 100644 index c7a16381a9..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.Cloner; - -public class ResourceUpdatePreProcessor { - - private final Cloner cloner; - - public ResourceUpdatePreProcessor(Cloner cloner) { - this.cloner = cloner; - } - - public R replaceSpecOnActual(R actual, R desired) { - var clonedActual = cloner.clone(actual); - if (desired instanceof ConfigMap) { - ((ConfigMap) clonedActual).setData(((ConfigMap) desired).getData()); - ((ConfigMap) clonedActual).setBinaryData((((ConfigMap) desired).getBinaryData())); - return clonedActual; - } else if (desired instanceof Secret) { - ((Secret) clonedActual).setData(((Secret) desired).getData()); - ((Secret) clonedActual).setStringData(((Secret) desired).getStringData()); - return clonedActual; - } else { - var desiredSpec = ReconcilerUtils.getSpec(desired); - ReconcilerUtils.setSpec(clonedActual, desiredSpec); - return clonedActual; - } - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index b29209f9e5..a706e244ab 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -77,16 +77,6 @@ private void propagateEvent(T object) { }); } - @Override - public void start() { - manager().start(); - } - - @Override - public void stop() { - manager().stop(); - } - /** * Retrieves the informed resource associated with the specified primary resource as defined by * the function provided when this InformerEventSource was created diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java similarity index 53% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index d3e013d54d..6302c20e6d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -4,47 +4,55 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.Context; import com.fasterxml.jackson.databind.ObjectMapper; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -class DesiredValueMatcherTest { +class GenericKubernetesResourceMatcherTest { - DesiredValueMatcher desiredValueMatcher = new DesiredValueMatcher(new ObjectMapper()); + private static final Context context = mock(Context.class); + static { + final var configurationService = mock(ConfigurationService.class); + when(configurationService.getObjectMapper()).thenReturn(new ObjectMapper()); + when(context.getConfigurationService()).thenReturn(configurationService); + } @Test void checksIfDesiredValuesAreTheSame() { var target1 = createDeployment(); var desired1 = createDeployment(); - assertThat(desiredValueMatcher.match(target1, desired1, null)).isTrue(); + final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class); + assertThat(matcher.match(target1, desired1, context)).isTrue(); var target2 = createDeployment(); var desired2 = createDeployment(); target2.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(desiredValueMatcher.match(target2, desired2, null)) + assertThat(matcher.match(target2, desired2, context)) .withFailMessage("Additive changes should be ok") .isTrue(); var target3 = createDeployment(); var desired3 = createDeployment(); desired3.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(desiredValueMatcher.match(target3, desired3, null)) + assertThat(matcher.match(target3, desired3, context)) .withFailMessage("Removed value should not be ok") .isFalse(); var target4 = createDeployment(); var desired4 = createDeployment(); target4.getSpec().setReplicas(2); - assertThat(desiredValueMatcher.match(target4, desired4, null)) + assertThat(matcher.match(target4, desired4, context)) .withFailMessage("Changed values are not ok") .isFalse(); } Deployment createDeployment() { - Deployment deployment = - ReconcilerUtils.loadYaml( - Deployment.class, DesiredValueMatcherTest.class, "nginx-deployment.yaml"); - return deployment; + return ReconcilerUtils.loadYaml( + Deployment.class, GenericKubernetesResourceMatcherTest.class, "nginx-deployment.yaml"); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java similarity index 51% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index a4907f13b7..941738ad7f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -7,13 +7,26 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -class ResourceUpdatePreProcessorTest { +class GenericResourceUpdatePreProcessorTest { + + private static final Context context = mock(Context.class); + + static { + final var configurationService = mock(ConfigurationService.class); + when(configurationService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); + when(context.getConfigurationService()).thenReturn(configurationService); + } + + ResourceUpdatePreProcessor processor = + GenericResourceUpdatePreProcessor.processorFor(Deployment.class); - ResourceUpdatePreProcessor resourceUpdatePreProcessor = - new ResourceUpdatePreProcessor<>(ConfigurationService.DEFAULT_CLONER); @Test void preservesValues() { @@ -24,7 +37,7 @@ void preservesValues() { actual.getMetadata().setResourceVersion("1234"); actual.getSpec().setRevisionHistoryLimit(5); - var result = resourceUpdatePreProcessor.replaceSpecOnActual(actual, desired); + var result = processor.replaceSpecOnActual(actual, desired, context); assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value"); assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234"); @@ -32,10 +45,8 @@ void preservesValues() { } Deployment createDeployment() { - Deployment deployment = - ReconcilerUtils.loadYaml( - Deployment.class, ResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml"); - return deployment; + return ReconcilerUtils.loadYaml( + Deployment.class, GenericResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml"); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java index dd1e005fc1..67a8937ac4 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java @@ -3,7 +3,6 @@ import java.time.Duration; import java.util.*; import java.util.function.Function; -import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; @@ -27,6 +26,7 @@ public class AnnotationControllerConfiguration private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; + private List specs; public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -142,42 +142,42 @@ public static T valueOrDefault( } } + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public List getDependentResources() { - final var dependents = - valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); - if (dependents.length == 0) { - return Collections.emptyList(); - } + if (specs == null) { + final var dependents = + valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } - List resourceSpecs = new ArrayList<>(dependents.length); - for (Dependent dependent : dependents) { - - final Class dependentType = dependent.type(); - if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - final var namespaces = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::namespaces, - this.getNamespaces().toArray(new String[0])); - final var labelSelector = - Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); - final var addOwnerReference = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::addOwnerReference, - KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - KubernetesDependentResourceConfig config = - new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); - resourceSpecs.add(new DependentResourceSpec(dependentType, config)); - } else { - resourceSpecs.add(new DependentResourceSpec(dependentType)); + specs = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); + final var labelSelector = + Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var addOwnerReference = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::addOwnerReference, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + KubernetesDependentResourceConfig config = + new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); + specs.add(new DependentResourceSpec(dependentType, config)); + } else { + specs.add(new DependentResourceSpec(dependentType)); + } } } - return Arrays.stream(dependents) - .map(d -> new DependentResourceSpec(d.type())) - .collect(Collectors.toList()); + return specs; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java new file mode 100644 index 0000000000..b9556aba1c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator.config.runtime; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.sample.readonly.ReadOnlyDependent; + +import static org.junit.jupiter.api.Assertions.*; + +class AnnotationControllerConfigurationTest { + + @Test + void getDependentResources() { + var configuration = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + var dependents = configuration.getDependentResources(); + assertTrue(dependents.isEmpty()); + + configuration = new AnnotationControllerConfiguration<>(new OneDepReconciler()); + dependents = configuration.getDependentResources(); + assertFalse(dependents.isEmpty()); + assertEquals(1, dependents.size()); + final var dependentSpec = dependents.get(0); + assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); + final var maybeConfig = dependentSpec.getDependentResourceConfiguration(); + assertTrue(maybeConfig.isPresent()); + assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); + final var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow(); + assertEquals(1, config.namespaces().length); + assertEquals(OneDepReconciler.CONFIGURED_NS, config.namespaces()[0]); + } + + + @ControllerConfiguration(namespaces = OneDepReconciler.CONFIGURED_NS, + dependents = @Dependent(type = ReadOnlyDependent.class)) + private static class OneDepReconciler implements Reconciler { + + public static final String CONFIGURED_NS = "foo"; + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) { + return null; + } + } + + private static class NoDepReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) { + return null; + } + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java new file mode 100644 index 0000000000..ffff1bf78a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.sample.readonly; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Version("v1") +@Group("josdk.io") +public class ConfigMapReader extends CustomResource implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java new file mode 100644 index 0000000000..0285d47b83 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.readonly; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +public class ReadOnlyDependent extends KubernetesDependentResource { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index d529f19c3d..21f02c6c3a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -20,29 +21,29 @@ public class StandaloneDependentTestReconciler private KubernetesClient kubernetesClient; - KubernetesDependentResource configMapDependent; + KubernetesDependentResource deploymentDependent; public StandaloneDependentTestReconciler() { - configMapDependent = new DeploymentDependentResource(); + deploymentDependent = new DeploymentDependentResource(); } @Override public List prepareEventSources( EventSourceContext context) { - return List.of(configMapDependent.eventSource(context)); + return List.of(deploymentDependent.eventSource(context)); } @Override public UpdateControl reconcile( StandaloneDependentTestCustomResource resource, Context context) { - configMapDependent.reconcile(resource, context); + deploymentDependent.reconcile(resource, context); return UpdateControl.noUpdate(); } @Override public void setKubernetesClient(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - configMapDependent.setKubernetesClient(kubernetesClient); + deploymentDependent.setKubernetesClient(kubernetesClient); } @Override @@ -50,8 +51,9 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } - private class DeploymentDependentResource extends - KubernetesDependentResource { + private static class DeploymentDependentResource extends + KubernetesDependentResource + implements Creator { @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 67c553268f..b38d3cfdf8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -8,6 +8,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -18,9 +20,11 @@ import static java.lang.String.format; public class SchemaDependentResource - extends AbstractDependentResource + extends AbstractDependentResource implements EventSourceProvider, - DependentResourceConfigurator { + DependentResourceConfigurator, + Creator, + Deleter { private MySQLDbConfig dbConfig; private int pollPeriod = 500; @@ -47,12 +51,7 @@ public Schema desired(MySQLSchema primary, Context context) { } @Override - protected boolean match(Schema actual, Schema target, Context context) { - return actual.equals(target); - } - - @Override - protected Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { + public void create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { final var schema = SchemaService.createSchemaAndRelatedUser( connection, @@ -63,18 +62,12 @@ protected Schema create(Schema target, MySQLSchema mySQLSchema, Context context) // put the newly built schema in the context to let the reconciler know we just built it context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); - return schema; } catch (SQLException e) { MySQLSchemaReconciler.log.error("Error while creating Schema", e); throw new IllegalStateException(e); } } - @Override - protected Schema update(Schema actual, Schema target, MySQLSchema mySQLSchema, Context context) { - throw new IllegalStateException("Target schema should not be changed: " + mySQLSchema); - } - private Connection getConnection() throws SQLException { String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java index 884cc6d2c9..5dfa20a571 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -12,20 +13,12 @@ import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; public class SecretDependentResource extends KubernetesDependentResource - implements AssociatedSecondaryResourceIdentifier { + implements AssociatedSecondaryResourceIdentifier, Updater { private static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); } - // An alternative would be to override reconcile() method and exclude the update part. - @Override - protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { - throw new IllegalStateException( - "Secret should not be updated. Secret: " + target + " for custom resource: " - + primary); - } - @Override protected Secret desired(MySQLSchema schema, Context context) { return new SecretBuilder() @@ -41,7 +34,7 @@ protected Secret desired(MySQLSchema schema, Context context) { } @Override - protected boolean match(Secret actual, Secret target, Context context) { + public boolean match(Secret actual, Secret target, Context context) { return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 3a68973165..56b80b6c82 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -5,11 +5,14 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") -public class DeploymentDependentResource extends KubernetesDependentResource { +public class DeploymentDependentResource extends KubernetesDependentResource + implements Creator, Updater { private static String tomcatImage(Tomcat tomcat) { return "tomcat:" + tomcat.getSpec().getVersion(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index a77d13b345..66ddebf31e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -5,9 +5,12 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -public class ServiceDependentResource extends KubernetesDependentResource { +public class ServiceDependentResource extends KubernetesDependentResource + implements Creator, Updater { @Override protected Service desired(Tomcat tomcat, Context context) { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index b3200903d1..d7fd52f788 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -152,7 +153,7 @@ private static String serviceName(WebPage nginx) { private class ConfigMapDependentResource extends KubernetesDependentResource implements - AssociatedSecondaryResourceIdentifier { + AssociatedSecondaryResourceIdentifier, Updater { @Override protected ConfigMap desired(WebPage webPage, Context context) { @@ -169,15 +170,14 @@ protected ConfigMap desired(WebPage webPage, Context context) { } @Override - protected boolean match(ConfigMap actual, ConfigMap target, Context context) { + public boolean match(ConfigMap actual, ConfigMap target, Context context) { return StringUtils.equals( actual.getData().get("index.html"), target.getData().get("index.html")); } @Override - protected ConfigMap update( - ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - var cm = super.update(actual, target, primary, context); + public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + super.update(actual, target, primary, context); var ns = actual.getMetadata().getNamespace(); log.info("Restarting pods because HTML has changed in {}", ns); kubernetesClient @@ -185,7 +185,6 @@ protected ConfigMap update( .inNamespace(ns) .withLabel("app", deploymentName(primary)) .delete(); - return cm; } @Override From c5966219e0ef8e2ccd14dbddca227c769b163447 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:08:24 +0100 Subject: [PATCH 0034/1319] chore(deps): bump spring-boot.version from 2.6.3 to 2.6.4 (#972) Bumps `spring-boot.version` from 2.6.3 to 2.6.4. Updates `spring-boot-dependencies` from 2.6.3 to 2.6.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.3...v2.6.4) Updates `spring-boot-maven-plugin` from 2.6.3 to 2.6.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 420bf65c12..a8d47595aa 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 1.13.0 3.22.0 4.1.1 - 2.6.3 + 2.6.4 1.8.3 2.11 From 844323f8daac61d966444e9c3083ea2f152b55b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 25 Feb 2022 11:08:53 +0100 Subject: [PATCH 0035/1319] Temporal resource cache in Event Source (#965) --- .../informer/InformerConfiguration.java | 1 + .../operator/processing/Controller.java | 2 +- .../kubernetes/KubernetesDependent.java | 1 - .../KubernetesDependentResource.java | 30 ++- .../processing/event/EventProcessor.java | 50 +--- .../ControllerResourceEventSource.java | 32 +-- .../event/source/informer/EventRecorder.java | 71 ++++++ .../source/informer/InformerEventSource.java | 220 ++++++++++++++---- .../source/informer/InformerManager.java | 6 +- .../source/informer/InformerWrapper.java | 1 + .../informer/ManagedInformerEventSource.java | 82 ++++++- .../informer/TemporaryResourceCache.java | 110 +++++++++ .../KubernetesDependentResourceTest.java | 77 ++++++ .../processing/event/EventProcessorTest.java | 103 +++----- .../ControllerResourceEventSourceTest.java | 22 -- .../source/informer/EventRecorderTest.java | 80 +++++++ .../informer/InformerEventSourceTest.java | 194 +++++++++++++++ .../informer/TemporaryResourceCacheTest.java | 100 ++++++++ .../CreateUpdateDependentEventFilterIT.java | 88 +++++++ .../StandaloneDependentResourceIT.java | 36 ++- ...teUpdateEventFilterTestCustomResource.java | 22 ++ ...dateEventFilterTestCustomResourceSpec.java | 16 ++ ...teEventFilterTestCustomResourceStatus.java | 7 + ...CreateUpdateEventFilterTestReconciler.java | 120 ++++++++++ ...StandaloneDependentTestCustomResource.java | 3 +- ...daloneDependentTestCustomResourceSpec.java | 23 ++ .../StandaloneDependentTestReconciler.java | 35 ++- .../operator/sample/WebPageReconciler.java | 9 - 28 files changed, 1304 insertions(+), 237 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java rename operator-framework/src/test/java/io/javaoperatorsdk/operator/{dependent => }/StandaloneDependentResourceIT.java (56%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index acb5bd23c4..0d08b65f99 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -37,6 +37,7 @@ protected DefaultInformerConfiguration(ConfigurationService service, String labe Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); } + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { return secondaryToPrimaryResourcesIdSet; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 89aabe0d9b..e8996c351c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -214,7 +214,7 @@ public void start() throws OperatorException { } final var context = new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), configurationService(), kubernetesClient); prepareEventSources(context).forEach(eventSourceManager::registerEventSource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java index c0922f569f..45d2095d1a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java @@ -32,5 +32,4 @@ * @return the label selector */ String labelSelector() default EMPTY_STRING; - } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index a0513b1234..8ce3560fdc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -75,17 +75,16 @@ private void configureWith(ConfigurationService configService, String labelSelec .withPrimaryResourcesRetriever(primaryResourcesRetriever) .withAssociatedSecondaryResourceIdentifier(secondaryResourceIdentifier) .build(); - configureWith(configService, new InformerEventSource<>(ic, client), addOwnerReference); + configureWith(new InformerEventSource<>(ic, client), addOwnerReference); } /** * Use to share informers between event more resources. - * - * @param configurationService get configs + * * @param informerEventSource informer to use * @param addOwnerReference to the created resource */ - public void configureWith(ConfigurationService configurationService, + public void configureWith( InformerEventSource informerEventSource, boolean addOwnerReference) { this.informerEventSource = informerEventSource; @@ -93,12 +92,29 @@ public void configureWith(ConfigurationService configurationService, } public void create(R target, P primary, Context context) { - prepare(target, primary, "Creating").create(target); + var resourceID = ResourceID.fromResource(target); + try { + informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); + var created = prepare(target, primary, "Creating").create(target); + informerEventSource.handleRecentResourceCreate(created); + } catch (RuntimeException e) { + informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); + throw e; + } } public void update(R actual, R target, P primary, Context context) { - var updatedActual = processor.replaceSpecOnActual(actual, target, context); - prepare(target, primary, "Updating").replace(updatedActual); + var resourceID = ResourceID.fromResource(target); + try { + var updatedActual = processor.replaceSpecOnActual(actual, target, context); + informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); + var updated = prepare(target, primary, "Updating").replace(updatedActual); + informerEventSource.handleRecentResourceUpdate(updated, + actual.getMetadata().getResourceVersion()); + } catch (RuntimeException e) { + informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); + throw e; + } } public boolean match(R actualResource, R desiredResource, Context context) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 29f9adab55..81e637a2f6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -29,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.retry.RetryExecution; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; class EventProcessor implements EventHandler, LifecycleAware { @@ -50,7 +49,7 @@ class EventProcessor implements EventHandler, LifecycleAw EventProcessor(EventSourceManager eventSourceManager) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), ExecutorServiceManager.instance().executorService(), eventSourceManager.getController().getConfiguration().getName(), new ReconciliationDispatcher<>(eventSourceManager.getController()), @@ -73,7 +72,7 @@ class EventProcessor implements EventHandler, LifecycleAw Retry retry, Metrics metrics) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), null, relatedControllerName, reconciliationDispatcher, @@ -208,12 +207,12 @@ void eventProcessingFinished( if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { + postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { + eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate(r, + executionScope.getResource().getMetadata().getResourceVersion()); + }); if (eventMarker.eventPresent(resourceID)) { - if (isCacheReadyForInstantReconciliation(executionScope, postExecutionControl)) { - submitReconciliationExecution(resourceID); - } else { - postponeReconciliationAndHandleCacheSyncEvent(resourceID); - } + submitReconciliationExecution(resourceID); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); } @@ -223,41 +222,6 @@ void eventProcessingFinished( } } - private void postponeReconciliationAndHandleCacheSyncEvent(ResourceID resourceID) { - eventSourceManager.getControllerResourceEventSource().whitelistNextEvent(resourceID); - } - - private boolean isCacheReadyForInstantReconciliation( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { - if (!postExecutionControl.customResourceUpdatedDuringExecution()) { - return true; - } - String originalResourceVersion = getVersion(executionScope.getResource()); - String customResourceVersionAfterExecution = - getVersion( - postExecutionControl - .getUpdatedCustomResource() - .orElseThrow( - () -> new IllegalStateException( - "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = - getVersion( - cache - .get(executionScope.getCustomResourceID()) - .orElseThrow( - () -> new IllegalStateException( - "Cached custom resource must be present at this point"))); - - if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { - return true; - } - // If the cached resource version equals neither the version before nor after execution - // probably an update happened on the custom resource independent of the framework during - // reconciliation. We cannot tell at this point if it happened before our update or before. - // (Well we could if we would parse resource version, but that should not be done by definition) - return !cachedCustomResourceVersion.equals(originalResourceVersion); - } - private void reScheduleExecutionIfInstructed( PostExecutionControl postExecutionControl, R customResource) { postExecutionControl diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 2a224a87df..801eae09ee 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -13,7 +13,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; @@ -25,30 +24,19 @@ public class ControllerResourceEventSource implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; - private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); private final Controller controller; private final ResourceEventFilter filter; - private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; public ControllerResourceEventSource(Controller controller) { super(controller.getCRClient(), controller.getConfiguration()); this.controller = controller; - var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), ResourceEventFilters.markedForDeletion(), ResourceEventFilters.generationAware(), - null }; - - if (controller.getConfiguration().isGenerationAware()) { - onceWhitelistEventFilterEventFilter = new OnceWhitelistEventFilterEventFilter<>(); - filters[filters.length - 1] = onceWhitelistEventFilterEventFilter; - } else { - onceWhitelistEventFilterEventFilter = null; - } if (controller.getConfiguration().getEventFilter() != null) { filter = controller.getConfiguration().getEventFilter().and(ResourceEventFilters.or(filters)); } else { @@ -87,36 +75,22 @@ public void eventReceived(ResourceAction action, T resource, T oldResource) { @Override public void onAdd(T resource) { + super.onAdd(resource); eventReceived(ResourceAction.ADDED, resource, null); } @Override public void onUpdate(T oldCustomResource, T newCustomResource) { + super.onUpdate(oldCustomResource, newCustomResource); eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); } @Override public void onDelete(T resource, boolean b) { + super.onDelete(resource, b); eventReceived(ResourceAction.DELETED, resource, null); } - public ResourceCache getResourceCache() { - return manager(); - } - - /** - * This will ensure that the next event received after this method is called will not be filtered - * out. - * - * @param resourceID - to which the event is related - */ - public void whitelistNextEvent(ResourceID resourceID) { - if (onceWhitelistEventFilterEventFilter != null) { - onceWhitelistEventFilterEventFilter.whitelistNextEvent(resourceID); - } - } - - private void handleKubernetesClientException(Exception e) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java new file mode 100644 index 0000000000..284b749f07 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java @@ -0,0 +1,71 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class EventRecorder { + + private final Map> resourceEvents = new ConcurrentHashMap<>(); + + void startEventRecording(ResourceID resourceID) { + resourceEvents.putIfAbsent(resourceID, new ArrayList<>(5)); + } + + public boolean isRecordingFor(ResourceID resourceID) { + return resourceEvents.get(resourceID) != null; + } + + public void stopEventRecording(ResourceID resourceID) { + resourceEvents.remove(resourceID); + } + + public void recordEvent(R resource) { + resourceEvents.get(ResourceID.fromResource(resource)).add(resource); + } + + public boolean containsEventWithResourceVersion(ResourceID resourceID, String resourceVersion) { + List events = resourceEvents.get(resourceID); + if (events == null) { + return false; + } + if (events.isEmpty()) { + return false; + } else { + return events.stream() + .anyMatch(e -> e.getMetadata().getResourceVersion().equals(resourceVersion)); + } + } + + public boolean containsEventWithVersionButItsNotLastOne( + ResourceID resourceID, String resourceVersion) { + List resources = resourceEvents.get(resourceID); + if (resources == null) { + throw new IllegalStateException( + "Null events list, this is probably a result of invalid usage of the " + + "InformerEventSource. Resource ID: " + resourceID); + } + if (resources.isEmpty()) { + throw new IllegalStateException("No events for resource id: " + resourceID); + } + return !resources + .get(resources.size() - 1) + .getMetadata() + .getResourceVersion() + .equals(resourceVersion); + } + + public R getLastEvent(ResourceID resourceID) { + List resources = resourceEvents.get(resourceID); + if (resources == null) { + throw new IllegalStateException( + "Null events list, this is probably a result of invalid usage of the " + + "InformerEventSource. Resource ID: " + resourceID); + } + return resources.get(resources.size() - 1); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index a706e244ab..2f8705b63a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; @@ -11,87 +12,224 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; -public class InformerEventSource - extends ManagedInformerEventSource> - implements ResourceCache, ResourceEventHandler { +/** + *

+ * Wraps informer(s) so it is connected to the eventing system of the framework. Note that since + * it's it is built on top of Informers, it also support caching resources using caching from + * fabric8 client Informer caches and additional caches described below. + *

+ *

+ * InformerEventSource also supports two features to better handle events and caching of resources + * on top of Informers from fabric8 Kubernetes client. These two features implementation wise are + * related to each other: + *

+ *
+ *

+ * 1. API that allows to make sure the cache contains the fresh resource after an update. This is + * important for {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} and + * mainly for + * {@link io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource} + * so after reconcile if getResource() called always return the fresh resource. To achieve this + * handleRecentResourceUpdate() and handleRecentResourceCreate() needs to be called explicitly after + * resource created/updated using the kubernetes client. (These calls are done automatically by + * KubernetesDependentResource implementation.). In the background this will store the new resource + * in a temporary cache {@link TemporaryResourceCache} which do additional checks. After a new event + * is received the cachec object is removed from this cache, since in general then it is already in + * the cache of informer. + *

+ *
+ *

+ * 2. Additional API is provided that is ment to be used with the combination of the previous one, + * and the goal is to filter out events that are the results of updates and creates made by the + * controller itself. For example if in reconciler a ConfigMaps is created, there should be an + * Informer in place to handle change events of that ConfigMap, but since it has bean created (or + * updated) by the reconciler this should not trigger an additional reconciliation by default. In + * order to achieve this prepareForCreateOrUpdateEventFiltering(..) method needs to be called before + * the operation of the k8s client. And the operation from point 1. after the k8s client call. See + * it's usage in CreateUpdateEventFilterTestReconciler integration test for the usage. (Again this + * is managed for the developer if using dependent resources.)
+ * Roughly it works in a way that before the K8S API call is made, we set mark the resource ID, and + * from that point informer won't propagate events further just will start record them. After the + * client operation is done, it's checked and analysed what events were received and based on that + * it will propagate event or not and/or put the new resource into the temporal cache - so if the + * event not arrived yet about the update will be able to filter it in the future. + *

+ * + * @param resource type watching + * @param

type of the primary resource + */ +public class InformerEventSource + extends ManagedInformerEventSource> + implements ResourceCache, ResourceEventHandler { + + private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); - private final InformerConfiguration configuration; + private final InformerConfiguration configuration; + private final EventRecorder eventRecorder = new EventRecorder<>(); - public InformerEventSource(InformerConfiguration configuration, - EventSourceContext

context) { + public InformerEventSource( + InformerConfiguration configuration, EventSourceContext

context) { super(context.getClient().resources(configuration.getResourceClass()), configuration); this.configuration = configuration; } - public InformerEventSource(InformerConfiguration configuration, - KubernetesClient client) { + public InformerEventSource(InformerConfiguration configuration, KubernetesClient client) { super(client.resources(configuration.getResourceClass()), configuration); this.configuration = configuration; } @Override - public void onAdd(T t) { - propagateEvent(t); + public void onAdd(R resource) { + onAddOrUpdate("add", resource, () -> InformerEventSource.super.onAdd(resource)); } @Override - public void onUpdate(T oldObject, T newObject) { - if (newObject == null) { - // this is a fix for this potential issue with informer: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 - propagateEvent(oldObject); - return; - } + public void onUpdate(R oldObject, R newObject) { + onAddOrUpdate("update", newObject, + () -> InformerEventSource.super.onUpdate(oldObject, newObject)); + } - if (oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { + private synchronized void onAddOrUpdate(String operation, R newObject, Runnable superOnOp) { + var resourceID = ResourceID.fromResource(newObject); + if (eventRecorder.isRecordingFor(resourceID)) { + log.info("Recording event for: " + resourceID); + eventRecorder.recordEvent(newObject); return; } - propagateEvent(newObject); + if (temporalCacheHasResourceWithVersionAs(newObject)) { + log.debug( + "Skipping event propagation for {}, since was a result of a reconcile action. Resource ID: {}", + operation, + ResourceID.fromResource(newObject)); + superOnOp.run(); + } else { + superOnOp.run(); + log.debug( + "Propagating event for {}, resource with same version not result of a reconciliation. Resource ID: {}", + operation, + resourceID); + propagateEvent(newObject); + } } @Override - public void onDelete(T t, boolean b) { - propagateEvent(t); + public void onDelete(R r, boolean b) { + super.onDelete(r, b); + propagateEvent(r); } - private void propagateEvent(T object) { + private void propagateEvent(R object) { var primaryResourceIdSet = configuration.getPrimaryResourcesRetriever().associatedPrimaryResources(object); if (primaryResourceIdSet.isEmpty()) { return; } - primaryResourceIdSet.forEach(resourceId -> { - Event event = new Event(resourceId); - /* - * In fabric8 client for certain cases informers can be created on in a way that they are - * automatically started, what would cause a NullPointerException here, since an event might - * be received between creation and registration. - */ - final EventHandler eventHandler = getEventHandler(); - if (eventHandler != null) { - eventHandler.handleEvent(event); - } - }); + primaryResourceIdSet.forEach( + resourceId -> { + Event event = new Event(resourceId); + /* + * In fabric8 client for certain cases informers can be created on in a way that they are + * automatically started, what would cause a NullPointerException here, since an event + * might be received between creation and registration. + */ + final EventHandler eventHandler = getEventHandler(); + if (eventHandler != null) { + eventHandler.handleEvent(event); + } + }); } /** * Retrieves the informed resource associated with the specified primary resource as defined by * the function provided when this InformerEventSource was created - * + * * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ @Override - public Optional getAssociated(P resource) { + public Optional getAssociated(P resource) { final var id = configuration.getAssociatedResourceIdentifier().associatedSecondaryID(resource); return get(id); } + public InformerConfiguration getConfiguration() { + return configuration; + } + @Override - public Stream list(String namespace, Predicate predicate) { - return manager().list(namespace, predicate); + public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { + handleRecentCreateOrUpdate(resource, + () -> super.handleRecentResourceUpdate(resource, previousResourceVersion)); + } + + @Override + public void handleRecentResourceCreate(R resource) { + handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceCreate(resource)); + } + + private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnable) { + if (eventRecorder.isRecordingFor(ResourceID.fromResource(resource))) { + handleRecentResourceOperationAndStopEventRecording(resource); + } else { + runnable.run(); + } + } + + /** + * There can be the following cases: + *

    + *
  • 1. Did not receive the event yet for the target resource, then we need to put it to temp + * cache. Because event will arrive. Note that this not necessary mean that the even is not sent + * yet (we are in sync context). Also does not mean that there are no more events received after + * that. But during the event processing (onAdd, onUpdate) we make sure that the propagation just + * skipped for the right event.
  • + *
  • 2. Received the event about the operation already, it was the last. This means already is + * on cache of informer. So we have to do nothing. Since it was just recorded and not propagated. + *
  • + *
  • 3. Received the event but more events received since, so those were not propagated yet. So + * an event needs to be propagated to compensate.
  • + *
+ * + * @param resource just created or updated resource + */ + private synchronized void handleRecentResourceOperationAndStopEventRecording(R resource) { + ResourceID resourceID = ResourceID.fromResource(resource); + try { + if (!eventRecorder.containsEventWithResourceVersion( + resourceID, resource.getMetadata().getResourceVersion())) { + log.debug( + "Did not found event in buffer with target version and resource id: {}", resourceID); + temporaryResourceCache.unconditionallyCacheResource(resource); + } else if (eventRecorder.containsEventWithVersionButItsNotLastOne( + resourceID, resource.getMetadata().getResourceVersion())) { + R lastEvent = eventRecorder.getLastEvent(resourceID); + log.debug( + "Found events in event buffer but the target event is not last for id: {}. Propagating event.", + resourceID); + propagateEvent(lastEvent); + } + } finally { + eventRecorder.stopEventRecording(resourceID); + } } + + public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID) { + log.info("Starting event recording for: {}", resourceID); + eventRecorder.startEventRecording(resourceID); + } + + /** + * Mean to be called to clean up in case of an exception from the client. Usually in a catch + * block. + * + * @param resourceID of the resource + */ + public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) { + log.info("Stopping event recording for: {}", resourceID); + eventRecorder.stopEventRecording(resourceID); + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index e65f34df48..33f962e7ed 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -139,6 +139,10 @@ public T remove(ResourceID key) { @Override public void put(ResourceID key, T resource) { getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) - .ifPresent(c -> c.put(key, resource)); + .ifPresentOrElse(c -> c.put(key, resource), + () -> log.warn( + "Cannot put resource in the cache. No related cache found: {}. Resource: {}", + key, resource)); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index c7de41f331..710f230580 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -81,4 +81,5 @@ public T remove(ResourceID key) { public void put(ResourceID key, T resource) { cache.put(key, resource); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 1dcfa2aa0e..47de996914 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -1,17 +1,30 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; public abstract class ManagedInformerEventSource> extends CachingEventSource - implements ResourceEventHandler { + implements ResourceEventHandler, ResourceCache { + + private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); + + protected TemporaryResourceCache temporaryResourceCache = new TemporaryResourceCache<>(this); protected ManagedInformerEventSource( MixedOperation, Resource> client, C configuration) { @@ -19,6 +32,21 @@ protected ManagedInformerEventSource( manager().initSources(client, configuration, this); } + @Override + public void onAdd(R resource) { + temporaryResourceCache.removeResourceFromCache(resource); + } + + @Override + public void onUpdate(R oldObj, R newObj) { + temporaryResourceCache.removeResourceFromCache(newObj); + } + + @Override + public void onDelete(R obj, boolean deletedFinalStateUnknown) { + temporaryResourceCache.removeResourceFromCache(obj); + } + @Override protected UpdatableCache initCache() { return new InformerManager<>(); @@ -39,4 +67,56 @@ public void stop() { super.stop(); manager().stop(); } + + public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { + temporaryResourceCache.putUpdatedResource(resource, previousResourceVersion); + } + + public void handleRecentResourceCreate(R resource) { + temporaryResourceCache.putAddedResource(resource); + } + + @Override + public Optional get(ResourceID resourceID) { + Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); + if (resource.isPresent()) { + log.debug("Resource found in temporal cache for Resource ID: {}", resourceID); + return resource; + } else { + return super.get(resourceID); + } + } + + @Override + public Optional getAssociated(P primary) { + return get(ResourceID.fromResource(primary)); + } + + @Override + public Optional getCachedValue(ResourceID resourceID) { + return get(resourceID); + } + + protected boolean temporalCacheHasResourceWithVersionAs(R resource) { + var resourceID = ResourceID.fromResource(resource); + var res = temporaryResourceCache.getResourceFromCache(resourceID); + return res.map(r -> { + boolean resVersionsEqual = r.getMetadata().getResourceVersion() + .equals(resource.getMetadata().getResourceVersion()); + log.debug("Resource found in temporal cache for id: {} resource versions equal: {}", + resourceID, resVersionsEqual); + return resVersionsEqual; + }).orElse(false); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return manager().list(namespace, predicate); + } + + ManagedInformerEventSource setTemporalResourceCache( + TemporaryResourceCache temporaryResourceCache) { + this.temporaryResourceCache = temporaryResourceCache; + return this; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java new file mode 100644 index 0000000000..64c93484b8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -0,0 +1,110 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +/** + *

+ * Temporal cache is used to solve the problem for {@link KubernetesDependentResource} that is, when + * a create or update is executed the subsequent getResource opeeration might not return the + * up-to-date resource from informer cache, since it is not received yet by webhook. + *

+ *

+ * The idea of the solution is, that since an update (for create is simpler) was done successfully, + * and optimistic locking is in place, there were no other operations between reading the resource + * from the cache and the actual update. So when the new resource is stored in the temporal cache + * only if the informer still has the previous resource version, from before the update. If not, + * that means there were already updates on the cache (either by the actual update from + * DependentResource or other) so the resource does not needs to be cached. Subsequently if event + * received from the informer, it means that the cache of the informer was updated, so it already + * contains a more fresh version of the resource. + *

+ * + * @param resource to cache. + */ +public class TemporaryResourceCache { + + private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + + private final Map cache = new ConcurrentHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + private final ManagedInformerEventSource managedInformerEventSource; + + public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource) { + this.managedInformerEventSource = managedInformerEventSource; + } + + public void removeResourceFromCache(T resource) { + lock.lock(); + try { + cache.remove(ResourceID.fromResource(resource)); + } finally { + lock.unlock(); + } + } + + public void unconditionallyCacheResource(T newResource) { + lock.lock(); + try { + cache.put(ResourceID.fromResource(newResource), newResource); + } finally { + lock.unlock(); + } + } + + public void putAddedResource(T newResource) { + lock.lock(); + try { + ResourceID resourceID = ResourceID.fromResource(newResource); + if (managedInformerEventSource.get(resourceID).isEmpty()) { + log.debug("Putting resource to cache with ID: {}", resourceID); + cache.put(ResourceID.fromResource(newResource), newResource); + } else { + log.debug("Won't put resource into cache found already informer cache: {}", resourceID); + } + } finally { + lock.unlock(); + } + } + + public void putUpdatedResource(T newResource, String previousResourceVersion) { + lock.lock(); + try { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = managedInformerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + log.debug("Putting resource to temporal cache with id: {}", resourceId); + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); + } + } finally { + lock.unlock(); + } + } + + public Optional getResourceFromCache(ResourceID resourceID) { + try { + lock.lock(); + return Optional.ofNullable(cache.get(resourceID)); + } finally { + lock.unlock(); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java new file mode 100644 index 0000000000..e73244f7c6 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import static org.mockito.Mockito.*; + +class KubernetesDependentResourceTest { + + // private InformerEventSource informerEventSourceMock = mock(InformerEventSource.class); + // private AssociatedSecondaryResourceIdentifier associatedResourceIdentifierMock = + // mock(AssociatedSecondaryResourceIdentifier.class); + // private ResourceMatcher resourceMatcherMock = mock(ResourceMatcher.class); + // private KubernetesDependentResource.ClientFacade clientFacadeMock = + // mock(KubernetesDependentResource.ClientFacade.class); + // + // KubernetesDependentResource kubernetesDependentResource = + // new KubernetesDependentResource() { + // { + // this.informerEventSource = informerEventSourceMock; + // this.resourceMatcher = resourceMatcherMock; + // this.clientFacade = clientFacadeMock; + // this.resourceUpdatePreProcessor = mock(ResourceUpdatePreProcessor.class); + // } + // + // @Override + // protected Object desired(HasMetadata primary, Context context) { + // return testResource(); + // } + // }; + // + // @BeforeEach + // public void setup() { + // InformerConfiguration informerConfigurationMock = mock(InformerConfiguration.class); + // when(informerEventSourceMock.getConfiguration()).thenReturn(informerConfigurationMock); + // when(informerConfigurationMock.getAssociatedResourceIdentifier()) + // .thenReturn(associatedResourceIdentifierMock); + // when(associatedResourceIdentifierMock.associatedSecondaryID(any())) + // .thenReturn(ResourceID.fromResource(testResource())); + // } + // + // @Test + // void updateCallsInformerJustUpdatedHandler() { + // when(resourceMatcherMock.match(any(), any(), any())).thenReturn(false); + // when(clientFacadeMock.replaceResource(any(), any(), any())).thenReturn(testResource()); + // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.of(testResource())); + // + // kubernetesDependentResource.reconcile(primaryResource(), null); + // + // verify(informerEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); + // } + // + // @Test + // void createCallsInformerJustUpdatedHandler() { + // when(clientFacadeMock.createResource(any(), any(), any())).thenReturn(testResource()); + // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.empty()); + // + // kubernetesDependentResource.reconcile(primaryResource(), null); + // + // verify(informerEventSourceMock, times(1)).handleRecentResourceAdd(any()); + // } + // + // TestCustomResource primaryResource() { + // TestCustomResource testCustomResource = new TestCustomResource(); + // testCustomResource.setMetadata(new ObjectMeta()); + // testCustomResource.getMetadata().setName("test"); + // testCustomResource.getMetadata().setNamespace("default"); + // return testCustomResource; + // } + // + // ConfigMap testResource() { + // ConfigMap configMap = new ConfigMap(); + // configMap.setMetadata(new ObjectMeta()); + // configMap.getMetadata().setName("test"); + // configMap.getMetadata().setNamespace("default"); + // configMap.getMetadata().setResourceVersion("0"); + // return configMap; + // } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 5a3f73742e..c02a3b0da1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -13,7 +13,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; @@ -44,8 +43,6 @@ class EventProcessorTest { private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); - private ControllerResourceCache resourceCacheMock = - mock(ControllerResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerResourceEventSource controllerResourceEventSourceMock = mock(ControllerResourceEventSource.class); @@ -54,12 +51,9 @@ class EventProcessorTest { private EventProcessor eventProcessorWithRetry; @BeforeEach - public void setup() { - + void setup() { when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(controllerResourceEventSourceMock); - when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); - eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, null)); @@ -68,22 +62,22 @@ public void setup() { spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", GenericRetry.defaultLimitedExponentialRetry(), null)); eventProcessorWithRetry.start(); - when(eventProcessor.retryEventSource()).thenReturn(retryTimerEventSourceMock); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); } @Test - public void dispatchesEventsIfNoExecutionInProgress() { + void dispatchesEventsIfNoExecutionInProgress() { eventProcessor.handleEvent(prepareCREvent()); verify(reconciliationDispatcherMock, timeout(50).times(1)).handleExecution(any()); } @Test - public void skipProcessingIfLatestCustomResourceNotInCache() { + void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - when(resourceCacheMock.get(event.getRelatedCustomResourceID())).thenReturn(Optional.empty()); + when(controllerResourceEventSourceMock.get(event.getRelatedCustomResourceID())) + .thenReturn(Optional.empty()); eventProcessor.handleEvent(event); @@ -91,7 +85,7 @@ public void skipProcessingIfLatestCustomResourceNotInCache() { } @Test - public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { + void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { ResourceID resourceUid = eventAlreadyUnderProcessing(); eventProcessor.handleEvent(nonCREvent(resourceUid)); @@ -101,7 +95,7 @@ public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedExcep } @Test - public void schedulesAnEventRetryOnException() { + void schedulesAnEventRetryOnException() { TestCustomResource customResource = testCustomResource(); ExecutionScope executionScope = new ExecutionScope(customResource, null); @@ -115,7 +109,7 @@ public void schedulesAnEventRetryOnException() { } @Test - public void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { + void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); overrideData(event.getRelatedCustomResourceID(), customResource); @@ -142,7 +136,7 @@ public void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { } @Test - public void successfulExecutionResetsTheRetry() { + void successfulExecutionResetsTheRetry() { log.info("Starting successfulExecutionResetsTheRetry"); Event event = prepareCREvent(); @@ -182,7 +176,7 @@ public void successfulExecutionResetsTheRetry() { } @Test - public void scheduleTimedEventIfInstructedByPostExecutionControl() { + void scheduleTimedEventIfInstructedByPostExecutionControl() { var testDelay = 10000L; when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); @@ -194,7 +188,7 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { } @Test - public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { + void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { var testDelay = 10000L; when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); @@ -207,7 +201,7 @@ public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { } @Test - public void doNotFireEventsIfClosing() { + void doNotFireEventsIfClosing() { eventProcessor.stop(); eventProcessor.handleEvent(prepareCREvent()); @@ -215,58 +209,7 @@ public void doNotFireEventsIfClosing() { } @Test - public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(1)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventWhenOtherChangeDuringExecution() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var otherChangeCR = testCustomResource(crID); - otherChangeCR.getMetadata().setResourceVersion("3"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventIfUpdatedEventInCache() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(cr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - - @Test - public void cancelScheduleOnceEventsOnSuccessfulExecution() { + void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); @@ -277,12 +220,13 @@ public void cancelScheduleOnceEventsOnSuccessfulExecution() { } @Test - public void startProcessedMarkedEventReceivedBefore() { + void startProcessedMarkedEventReceivedBefore() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, metricsMock)); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); + when(controllerResourceEventSourceMock.get(eq(crID))) + .thenReturn(Optional.of(testCustomResource())); eventProcessor.handleEvent(new Event(crID)); verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); @@ -293,6 +237,19 @@ public void startProcessedMarkedEventReceivedBefore() { verify(metricsMock, times(1)).reconcileCustomResource(any(), isNull()); } + @Test + void updatesEventSourceHandlerIfResourceUpdated() { + TestCustomResource customResource = testCustomResource(); + ExecutionScope executionScope = new ExecutionScope(customResource, null); + PostExecutionControl postExecutionControl = + PostExecutionControl.customResourceUpdated(customResource); + + eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); + + + verify(controllerResourceEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); + } + private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( @@ -311,7 +268,7 @@ private ResourceEvent prepareCREvent() { private ResourceEvent prepareCREvent(ResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - when(resourceCacheMock.get(eq(uid))).thenReturn(Optional.of(customResource)); + when(controllerResourceEventSourceMock.get(eq(uid))).thenReturn(Optional.of(customResource)); return new ResourceEvent(ResourceAction.UPDATED, ResourceID.fromResource(customResource)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 83528256df..9782948dba 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -12,7 +12,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -99,27 +98,6 @@ public void eventWithNoGenerationProcessedIfNoFinalizer() { verify(eventHandler, times(1)).handleEvent(any()); } - @Test - public void handlesNextEventIfWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - source.whitelistNextEvent(ResourceID.fromResource(customResource)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(1)).handleEvent(any()); - } - - @Test - public void notHandlesNextEventIfNotWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(0)).handleEvent(any()); - } - @Test public void callsBroadcastsOnResourceEvents() { TestCustomResource customResource1 = TestUtils.testCustomResource(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java new file mode 100644 index 0000000000..3e25e1da3c --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java @@ -0,0 +1,80 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static org.assertj.core.api.Assertions.assertThat; + +class EventRecorderTest { + + public static final String RESOURCE_VERSION = "0"; + public static final String RESOURCE_VERSION1 = "1"; + EventRecorder eventRecorder = new EventRecorder(); + + ConfigMap testConfigMap = testConfigMap(RESOURCE_VERSION); + ConfigMap testConfigMap2 = testConfigMap(RESOURCE_VERSION1); + + ResourceID id = ResourceID.fromResource(testConfigMap); + + @Test + void recordsEvents() { + + assertThat(eventRecorder.isRecordingFor(id)).isFalse(); + + eventRecorder.startEventRecording(id); + assertThat(eventRecorder.isRecordingFor(id)).isTrue(); + + eventRecorder.recordEvent(testConfigMap); + + eventRecorder.stopEventRecording(id); + assertThat(eventRecorder.isRecordingFor(id)).isFalse(); + } + + @Test + void getsLastRecorded() { + eventRecorder.startEventRecording(id); + + eventRecorder.recordEvent(testConfigMap); + eventRecorder.recordEvent(testConfigMap2); + + assertThat(eventRecorder.getLastEvent(id)).isEqualTo(testConfigMap2); + } + + @Test + void checksContainsWithResourceVersion() { + eventRecorder.startEventRecording(id); + + eventRecorder.recordEvent(testConfigMap); + eventRecorder.recordEvent(testConfigMap2); + + assertThat(eventRecorder.containsEventWithResourceVersion(id, RESOURCE_VERSION)).isTrue(); + assertThat(eventRecorder.containsEventWithResourceVersion(id, RESOURCE_VERSION1)).isTrue(); + assertThat(eventRecorder.containsEventWithResourceVersion(id, "xxx")).isFalse(); + } + + @Test + void checkLastItemVersion() { + eventRecorder.startEventRecording(id); + + eventRecorder.recordEvent(testConfigMap); + eventRecorder.recordEvent(testConfigMap2); + + assertThat(eventRecorder.containsEventWithVersionButItsNotLastOne(id, RESOURCE_VERSION)) + .isTrue(); + assertThat(eventRecorder.containsEventWithVersionButItsNotLastOne(id, RESOURCE_VERSION1)) + .isFalse(); + } + + ConfigMap testConfigMap(String resourceVersion) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName("test"); + configMap.getMetadata().setResourceVersion(resourceVersion); + + return configMap; + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java new file mode 100644 index 0000000000..1c901034e0 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -0,0 +1,194 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class InformerEventSourceTest { + + private static final String PREV_RESOURCE_VERSION = "0"; + private static final String DEFAULT_RESOURCE_VERSION = "1"; + private static final String NEXT_RESOURCE_VERSION = "2"; + + private InformerEventSource informerEventSource; + private KubernetesClient clientMock = mock(KubernetesClient.class); + private TemporaryResourceCache temporaryResourceCacheMock = + mock(TemporaryResourceCache.class); + private EventHandler eventHandlerMock = mock(EventHandler.class); + private MixedOperation crClientMock = mock(MixedOperation.class); + private FilterWatchListMultiDeletable specificResourceClientMock = + mock(FilterWatchListMultiDeletable.class); + private FilterWatchListDeletable labeledResourceClientMock = mock(FilterWatchListDeletable.class); + private SharedIndexInformer informer = mock(SharedIndexInformer.class); + private InformerConfiguration informerConfiguration = + mock(InformerConfiguration.class); + + @BeforeEach + void setup() { + when(clientMock.resources(any())).thenReturn(crClientMock); + when(crClientMock.inAnyNamespace()).thenReturn(specificResourceClientMock); + when(specificResourceClientMock.withLabelSelector((String) null)) + .thenReturn(labeledResourceClientMock); + when(labeledResourceClientMock.runnableInformer(0)).thenReturn(informer); + + when(informerConfiguration.getPrimaryResourcesRetriever()) + .thenReturn(mock(PrimaryResourcesRetriever.class)); + + informerEventSource = new InformerEventSource<>(informerConfiguration, clientMock); + informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); + informerEventSource.setEventHandler(eventHandlerMock); + + PrimaryResourcesRetriever primaryResourcesRetriever = mock(PrimaryResourcesRetriever.class); + when(informerConfiguration.getPrimaryResourcesRetriever()) + .thenReturn(primaryResourcesRetriever); + when(primaryResourcesRetriever.associatedPrimaryResources(any())) + .thenReturn(Set.of(ResourceID.fromResource(testDeployment()))); + } + + @Test + void skipsEventPropagationIfResourceWithSameVersionInResourceCache() { + when(temporaryResourceCacheMock.getResourceFromCache(any())) + .thenReturn(Optional.of(testDeployment())); + + informerEventSource.onAdd(testDeployment()); + informerEventSource.onUpdate(testDeployment(), testDeployment()); + + verify(eventHandlerMock, never()).handleEvent(any()); + } + + @Test + void propagateEventAndRemoveResourceFromTempCacheIfResourceVersionMismatch() { + Deployment cachedDeployment = testDeployment(); + cachedDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + when(temporaryResourceCacheMock.getResourceFromCache(any())) + .thenReturn(Optional.of(cachedDeployment)); + + + informerEventSource.onUpdate(cachedDeployment, testDeployment()); + + verify(eventHandlerMock, times(1)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(1)).removeResourceFromCache(any()); + } + + @Test + void notPropagatesEventIfAfterUpdateReceivedJustTheRelatedEvent() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevTestDeployment, testDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + + @Test + void notPropagatesEventIfAfterCreateReceivedJustTheRelatedEvent() { + var testDeployment = testDeployment(); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onAdd(testDeployment); + informerEventSource.handleRecentResourceCreate(testDeployment); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + @Test + void propagatesEventIfNewEventReceivedAfterTheCurrentTargetEvent() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + var nextTestDeployment = testDeployment(); + nextTestDeployment.getMetadata().setResourceVersion(NEXT_RESOURCE_VERSION); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevTestDeployment, testDeployment); + informerEventSource.onUpdate(testDeployment, nextTestDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(1)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + @Test + void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + var prevPrevTestDeployment = testDeployment(); + prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); + informerEventSource.onUpdate(prevTestDeployment, testDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + @Test + void putsResourceOnTempCacheIfNoEventRecorded() { + var testDeployment = testDeployment(); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); + } + + @Test + void putsResourceOnTempCacheIfNoEventRecordedWithSameResourceVersion() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + var prevPrevTestDeployment = testDeployment(); + prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); + } + + Deployment testDeployment() { + Deployment deployment = new Deployment(); + deployment.setMetadata(new ObjectMeta()); + deployment.getMetadata().setResourceVersion(DEFAULT_RESOURCE_VERSION); + deployment.getMetadata().setName("test"); + return deployment; + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java new file mode 100644 index 0000000000..2af66a4abe --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java @@ -0,0 +1,100 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TemporaryResourceCacheTest { + + public static final String RESOURCE_VERSION = "1"; + private InformerEventSource informerEventSource = mock(InformerEventSource.class); + private TemporaryResourceCache temporaryResourceCache = + new TemporaryResourceCache<>(informerEventSource); + + + @Test + void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() { + var testResource = testResource(); + var prevTestResource = testResource(); + prevTestResource.getMetadata().setResourceVersion("0"); + when(informerEventSource.get(any())).thenReturn(Optional.of(prevTestResource)); + + temporaryResourceCache.putUpdatedResource(testResource, "0"); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isPresent(); + } + + @Test + void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { + var testResource = testResource(); + var informerCachedResource = testResource(); + informerCachedResource.getMetadata().setResourceVersion("x"); + when(informerEventSource.get(any())).thenReturn(Optional.of(informerCachedResource)); + + temporaryResourceCache.putUpdatedResource(testResource, "0"); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isNotPresent(); + } + + @Test + void addOperationAddsTheResourceIfInformerCacheStillEmpty() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.empty()); + + temporaryResourceCache.putAddedResource(testResource); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isPresent(); + } + + @Test + void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.of(testResource())); + + temporaryResourceCache.putAddedResource(testResource); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isNotPresent(); + } + + @Test + void removesResourceFromCache() { + ConfigMap testResource = propagateTestResourceToCache(); + + temporaryResourceCache.removeResourceFromCache(testResource()); + + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isNotPresent(); + } + + private ConfigMap propagateTestResourceToCache() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.empty()); + temporaryResourceCache.putAddedResource(testResource); + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isPresent(); + return testResource; + } + + ConfigMap testResource() { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName("test"); + configMap.getMetadata().setNamespace("default"); + configMap.getMetadata().setResourceVersion(RESOURCE_VERSION); + return configMap; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java new file mode 100644 index 0000000000..57fbc78838 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java @@ -0,0 +1,88 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResource; +import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResourceSpec; +import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestReconciler; + +import static io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestReconciler.CONFIG_MAP_TEST_DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class CreateUpdateDependentEventFilterIT { + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new CreateUpdateEventFilterTestReconciler()) + .build(); + + @Test + void updateEventNotReceivedAfterCreateOrUpdate() { + CreateUpdateEventFilterTestCustomResource resource = prepareTestResource(); + var createdResource = + operator.create(CreateUpdateEventFilterTestCustomResource.class, resource); + + await() + .atMost(Duration.ofSeconds(1)) + .until(() -> { + var cm = operator.get(ConfigMap.class, createdResource.getMetadata().getName()); + if (cm == null) { + return false; + } + return cm.getData() + .get(CONFIG_MAP_TEST_DATA_KEY) + .equals(createdResource.getSpec().getValue()); + }); + + assertThat( + ((CreateUpdateEventFilterTestReconciler) operator.getFirstReconciler()) + .getNumberOfExecutions()) + .isEqualTo(1); // this should be 1 usually but sometimes event is received + // faster than added resource added to the cache + + + CreateUpdateEventFilterTestCustomResource actualCreatedResource = + operator.get(CreateUpdateEventFilterTestCustomResource.class, + resource.getMetadata().getName()); + actualCreatedResource.getSpec().setValue("2"); + operator.replace(CreateUpdateEventFilterTestCustomResource.class, actualCreatedResource); + + + await().atMost(Duration.ofSeconds(1)) + .until(() -> { + var cm = operator.get(ConfigMap.class, createdResource.getMetadata().getName()); + if (cm == null) { + return false; + } + return cm.getData() + .get(CONFIG_MAP_TEST_DATA_KEY) + .equals(actualCreatedResource.getSpec().getValue()); + }); + + assertThat( + ((CreateUpdateEventFilterTestReconciler) operator.getFirstReconciler()) + .getNumberOfExecutions()) + // same as for previous assert (usually this should be 2) + .isEqualTo(2); + } + + private CreateUpdateEventFilterTestCustomResource prepareTestResource() { + CreateUpdateEventFilterTestCustomResource resource = + new CreateUpdateEventFilterTestCustomResource(); + resource.setMetadata(new ObjectMeta()); + resource.getMetadata().setName("test1"); + resource.setSpec(new CreateUpdateEventFilterTestCustomResourceSpec()); + resource.getSpec().setValue("1"); + return resource; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java similarity index 56% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index 20c0f20b80..d86385f1f1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.dependent; +package io.javaoperatorsdk.operator; import java.time.Duration; @@ -10,8 +10,10 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestReconciler; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class StandaloneDependentResourceIT { @@ -29,10 +31,38 @@ class StandaloneDependentResourceIT { void dependentResourceManagesDeployment() { StandaloneDependentTestCustomResource customResource = new StandaloneDependentTestCustomResource(); + customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); customResource.setMetadata(new ObjectMeta()); customResource.getMetadata().setName(DEPENDENT_TEST_NAME); var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + awaitForDeploymentReadyReplicas(1); + assertThat( + ((StandaloneDependentTestReconciler) operator.getFirstReconciler()).isErrorOccurred()) + .isFalse(); + } + + @Test + void executeUpdateForTestingCacheUpdateForGetResource() { + StandaloneDependentTestCustomResource customResource = + new StandaloneDependentTestCustomResource(); + customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); + customResource.setMetadata(new ObjectMeta()); + customResource.getMetadata().setName(DEPENDENT_TEST_NAME); + var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + + awaitForDeploymentReadyReplicas(1); + + createdCR.getSpec().setReplicaCount(2); + operator.replace(StandaloneDependentTestCustomResource.class, createdCR); + + awaitForDeploymentReadyReplicas(2); + assertThat( + ((StandaloneDependentTestReconciler) operator.getFirstReconciler()).isErrorOccurred()) + .isFalse(); + } + + void awaitForDeploymentReadyReplicas(int expectedReplicaCount) { await() .pollInterval(Duration.ofMillis(300)) .atMost(Duration.ofSeconds(50)) @@ -42,13 +72,13 @@ void dependentResourceManagesDeployment() { operator .getKubernetesClient() .resources(Deployment.class) - .inNamespace(createdCR.getMetadata().getNamespace()) + .inNamespace(operator.getNamespace()) .withName(DEPENDENT_TEST_NAME) .get(); return deployment != null && deployment.getStatus() != null && deployment.getStatus().getReadyReplicas() != null - && deployment.getStatus().getReadyReplicas() > 0; + && deployment.getStatus().getReadyReplicas() == expectedReplicaCount; }); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java new file mode 100644 index 0000000000..5797a44d9d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("cue") +public class CreateUpdateEventFilterTestCustomResource + extends + CustomResource + implements Namespaced { + + @Override + protected CreateUpdateEventFilterTestCustomResourceStatus initStatus() { + return new CreateUpdateEventFilterTestCustomResourceStatus(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java new file mode 100644 index 0000000000..fcd3807bc8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +public class CreateUpdateEventFilterTestCustomResourceSpec { + + private String value; + + public String getValue() { + return value; + } + + public CreateUpdateEventFilterTestCustomResourceSpec setValue(String value) { + this.value = value; + return this; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java new file mode 100644 index 0000000000..6733e1a7cd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class CreateUpdateEventFilterTestCustomResourceStatus extends ObservedGenerationAwareStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java new file mode 100644 index 0000000000..ae41eacb68 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -0,0 +1,120 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class CreateUpdateEventFilterTestReconciler + implements Reconciler, + EventSourceInitializer, + KubernetesClientAware { + + public static final String CONFIG_MAP_TEST_DATA_KEY = "key"; + private KubernetesClient client; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private InformerEventSource informerEventSource; + + @Override + public UpdateControl reconcile( + CreateUpdateEventFilterTestCustomResource resource, Context context) { + numberOfExecutions.incrementAndGet(); + + ConfigMap configMap = + client + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .withName(resource.getMetadata().getName()) + .get(); + if (configMap == null) { + var configMapToCreate = createConfigMap(resource); + try { + informerEventSource.prepareForCreateOrUpdateEventFiltering( + ResourceID.fromResource(configMapToCreate)); + configMap = + client + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .create(configMapToCreate); + informerEventSource.handleRecentResourceCreate(configMap); + } catch (RuntimeException e) { + informerEventSource + .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMapToCreate)); + throw e; + } + } else { + if (!Objects.equals( + configMap.getData().get(CONFIG_MAP_TEST_DATA_KEY), resource.getSpec().getValue())) { + configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue()); + try { + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + var newConfigMap = + client + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .replace(configMap); + informerEventSource.handleRecentResourceUpdate( + newConfigMap, configMap.getMetadata().getResourceVersion()); + } catch (RuntimeException e) { + informerEventSource + .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + throw e; + } + } + } + return UpdateControl.noUpdate(); + } + + private ConfigMap createConfigMap(CreateUpdateEventFilterTestCustomResource resource) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(resource.getMetadata().getName()); + configMap.getMetadata().setLabels(new HashMap<>()); + configMap.getMetadata().getLabels().put("integrationtest", this.getClass().getSimpleName()); + configMap.getMetadata().setNamespace(resource.getMetadata().getNamespace()); + configMap.setData(new HashMap<>()); + configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue()); + configMap.addOwnerReference(resource); + + return configMap; + } + + @Override + public List prepareEventSources( + EventSourceContext context) { + InformerConfiguration informerConfiguration = + InformerConfiguration.from(context, ConfigMap.class) + .withLabelSelector("integrationtest = " + this.getClass().getSimpleName()) + .build(); + informerEventSource = new InformerEventSource<>(informerConfiguration, client); + return List.of(informerEventSource); + } + + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java index 3e6737c83c..e88d00c985 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java @@ -10,6 +10,7 @@ @Version("v1") @ShortNames("sdt") public class StandaloneDependentTestCustomResource - extends CustomResource + extends + CustomResource implements Namespaced { } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java new file mode 100644 index 0000000000..4a88cf6044 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +public class StandaloneDependentTestCustomResourceSpec { + + private int replicaCount; + + public StandaloneDependentTestCustomResourceSpec(int replicaCount) { + this.replicaCount = replicaCount; + } + + public StandaloneDependentTestCustomResourceSpec() { + this(1); + } + + public int getReplicaCount() { + return replicaCount; + } + + public StandaloneDependentTestCustomResourceSpec setReplicaCount(int replicaCount) { + this.replicaCount = replicaCount; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 21f02c6c3a..b6efb0f579 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -1,12 +1,14 @@ package io.javaoperatorsdk.operator.sample.standalonedependent; import java.util.List; +import java.util.Optional; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -17,11 +19,12 @@ public class StandaloneDependentTestReconciler implements Reconciler, EventSourceInitializer, - KubernetesClientAware { + KubernetesClientAware, ErrorStatusHandler { private KubernetesClient kubernetesClient; + private boolean errorOccurred = false; - KubernetesDependentResource deploymentDependent; + DeploymentDependentResource deploymentDependent; public StandaloneDependentTestReconciler() { deploymentDependent = new DeploymentDependentResource(); @@ -35,8 +38,17 @@ public List prepareEventSources( @Override public UpdateControl reconcile( - StandaloneDependentTestCustomResource resource, Context context) { - deploymentDependent.reconcile(resource, context); + StandaloneDependentTestCustomResource primary, Context context) { + deploymentDependent.reconcile(primary, context); + Optional deployment = deploymentDependent.getResource(primary); + if (deployment.isEmpty()) { + throw new IllegalStateException("Resource should not be empty after reconcile."); + } + + if (deployment.get().getSpec().getReplicas() != primary.getSpec().getReplicaCount()) { + // see https://github.com/java-operator-sdk/java-operator-sdk/issues/924 + throw new IllegalStateException("Something went wrong withe the cache mechanism."); + } return UpdateControl.noUpdate(); } @@ -51,15 +63,28 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } + @Override + public Optional updateErrorStatus( + StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + errorOccurred = true; + return Optional.empty(); + } + + public boolean isErrorOccurred() { + return errorOccurred; + } + private static class DeploymentDependentResource extends KubernetesDependentResource - implements Creator { + implements Creator, + Updater { @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getSpec().setReplicas(primary.getSpec().getReplicaCount()); deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index d7fd52f788..beef42599c 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.time.Duration; import java.util.*; import org.apache.commons.lang3.StringUtils; @@ -20,7 +19,6 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -import static org.awaitility.Awaitility.await; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class WebPageReconciler @@ -59,7 +57,6 @@ public UpdateControl reconcile(WebPage webPage, Context context) { WebPageStatus status = new WebPageStatus(); - waitUntilConfigMapAvailable(webPage); status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); @@ -68,12 +65,6 @@ public UpdateControl reconcile(WebPage webPage, Context context) { return UpdateControl.updateStatus(webPage); } - // todo after implemented we can remove this method: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/924 - private void waitUntilConfigMapAvailable(WebPage webPage) { - await().atMost(Duration.ofSeconds(5)).until(() -> configMapDR.getResource(webPage).isPresent()); - } - @Override public Optional updateErrorStatus( WebPage resource, RetryInfo retryInfo, RuntimeException e) { From a7eba4303f2962f79a137c8c35fc77d8075b1e5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 07:28:17 +0100 Subject: [PATCH 0036/1319] chore(deps): bump log4j.version from 2.17.1 to 2.17.2 (#974) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a8d47595aa..dfca9ec391 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ 5.8.2 5.12.0 1.7.36 - 2.17.1 + 2.17.2 4.3.1 3.12.0 1.0.1 From 019771fb7e0d58869bcd708111948ddef95bf795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 28 Feb 2022 16:15:48 +0100 Subject: [PATCH 0037/1319] Refactor WebPage Sample (#976) --- .../CrudKubernetesDependentResource.java | 17 ++ sample-operators/webpage/k8s/webpage.yaml | 4 +- .../operator/sample/WebPage.java | 8 + .../operator/sample/WebPageOperator.java | 7 +- .../operator/sample/WebPageReconciler.java | 280 ++++++++++-------- .../WebPageReconcilerDependentResources.java | 199 +++++++++++++ .../operator/sample/WebPageSpec.java | 7 + .../operator/sample/WebPageStatus.java | 9 + 8 files changed, 405 insertions(+), 126 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java new file mode 100644 index 0000000000..88e9bdc7ae --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; + +/** + * Adaptor Class for standalone mode for resources that manages Create, Update and Delete + * + * @param Managed resource + * @param

Primary Resource + */ +public abstract class CrudKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater, Deleter

{ +} diff --git a/sample-operators/webpage/k8s/webpage.yaml b/sample-operators/webpage/k8s/webpage.yaml index 382da972cb..d59c67eee1 100644 --- a/sample-operators/webpage/k8s/webpage.yaml +++ b/sample-operators/webpage/k8s/webpage.yaml @@ -1,6 +1,8 @@ apiVersion: "sample.javaoperatorsdk/v1" kind: WebPage metadata: + labels: + low-level: "true" name: hellows spec: html: | @@ -9,6 +11,6 @@ spec: Hello Operator World - Hello World! + Hello World! diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java index 6c10ffe3af..52ef6a938b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java @@ -14,4 +14,12 @@ public class WebPage extends CustomResource protected WebPageStatus initStatus() { return new WebPageStatus(); } + + @Override + public String toString() { + return "WebPage{" + + "spec=" + spec + + ", status=" + status + + '}'; + } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index 768fd26a72..b61b34334e 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; +import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +27,11 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new WebPageReconciler(client)); + if (Arrays.stream(args).anyMatch(arg -> arg.equals("--classic"))) { + operator.register(new WebPageReconciler(client)); + } else { + operator.register(new WebPageReconcilerDependentResources(client)); + } operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index beef42599c..b2727e065b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -9,125 +9,192 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -@ControllerConfiguration(finalizerName = NO_FINALIZER) +/** Shows how to implement reconciler using the low level api directly. */ +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) public class WebPageReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { - private final Logger log = LoggerFactory.getLogger(getClass()); + public static final String LOW_LEVEL_LABEL_KEY = "low-level"; + public static final String INDEX_HTML = "index.html"; - private final KubernetesClient kubernetesClient; + private static final Logger log = LoggerFactory.getLogger(WebPageReconciler.class); - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; + private final KubernetesClient kubernetesClient; public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - createDependentResources(kubernetesClient); } + InformerEventSource configMapEventSource; + @Override public List prepareEventSources(EventSourceContext context) { - return List.of( - configMapDR.eventSource(context), - deploymentDR.eventSource(context), - serviceDR.eventSource(context)); + configMapEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, ConfigMap.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + var deploymentEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, Deployment.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + var serviceEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, Service.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + return List.of(configMapEventSource, deploymentEventSource, serviceEventSource); } @Override public UpdateControl reconcile(WebPage webPage, Context context) { + log.info("Reconciling web page: {}", webPage); if (webPage.getSpec().getHtml().contains("error")) { throw new ErrorSimulationException("Simulating error"); } + String ns = webPage.getMetadata().getNamespace(); + String configMapName = configMapName(webPage); + String deploymentName = deploymentName(webPage); + + + ConfigMap desiredHtmlConfigMap = makeDesiredHtmlConfigMap(ns, configMapName, webPage); + Deployment desiredDeployment = + makeDesiredDeployment(webPage, deploymentName, ns, configMapName); + Service desiredService = makeDesiredService(webPage, ns, desiredDeployment); + + var previousConfigMap = context.getSecondaryResource(ConfigMap.class).orElse(null); + if (!match(desiredHtmlConfigMap, previousConfigMap)) { + log.info( + "Creating or updating ConfigMap {} in {}", + desiredHtmlConfigMap.getMetadata().getName(), + ns); + kubernetesClient.configMaps().inNamespace(ns).createOrReplace(desiredHtmlConfigMap); + } - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); + var existingDeployment = context.getSecondaryResource(Deployment.class).orElse(null); + if (!match(desiredDeployment, existingDeployment)) { + log.info( + "Creating or updating Deployment {} in {}", + desiredDeployment.getMetadata().getName(), + ns); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(desiredDeployment); + } - WebPageStatus status = new WebPageStatus(); + var existingService = context.getSecondaryResource(Service.class).orElse(null); + if (!match(desiredService, existingService)) { + log.info( + "Creating or updating Deployment {} in {}", + desiredDeployment.getMetadata().getName(), + ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(desiredService); + } + + if (previousConfigMap != null && !StringUtils.equals( + previousConfigMap.getData().get(INDEX_HTML), + desiredHtmlConfigMap.getData().get(INDEX_HTML))) { + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient.pods().inNamespace(ns).withLabel("app", deploymentName(webPage)).delete(); + } + webPage.setStatus(createStatus(desiredHtmlConfigMap.getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } - status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); + private WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); status.setAreWeGood("Yes!"); status.setErrorMessage(null); - webPage.setStatus(status); + return status; + } - return UpdateControl.updateStatus(webPage); + private boolean match(Deployment desiredDeployment, Deployment deployment) { + if (deployment == null) { + return false; + } else { + return desiredDeployment.getSpec().getReplicas().equals(deployment.getSpec().getReplicas()) && + desiredDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } } - @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + private boolean match(Service desiredService, Service service) { + if (service == null) { + return false; + } + return desiredService.getSpec().getSelector().equals(service.getSpec().getSelector()); } - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - - this.deploymentDR = - new KubernetesDependentResource<>() { - - @Override - protected Deployment desired(WebPage webPage, Context context) { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - } - - @Override - protected Class resourceType() { - return Deployment.class; - } - }; - - this.serviceDR = - new KubernetesDependentResource<>() { - - @Override - protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, getClass(), "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - } - - @Override - protected Class resourceType() { - return Service.class; - } - }; + private boolean match(ConfigMap desiredHtmlConfigMap, ConfigMap existingConfigMap) { + if (existingConfigMap == null) { + return false; + } else { + return desiredHtmlConfigMap.getData().equals(existingConfigMap.getData()); + } + } + + private Service makeDesiredService(WebPage webPage, String ns, Deployment desiredDeployment) { + Service desiredService = ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml"); + desiredService.getMetadata().setName(serviceName(webPage)); + desiredService.getMetadata().setNamespace(ns); + desiredService.getMetadata().setLabels(lowLevelLabel()); + desiredService + .getSpec() + .setSelector(desiredDeployment.getSpec().getTemplate().getMetadata().getLabels()); + desiredService.addOwnerReference(webPage); + return desiredService; + } + + private Deployment makeDesiredDeployment(WebPage webPage, String deploymentName, String ns, + String configMapName) { + Deployment desiredDeployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + desiredDeployment.getMetadata().setName(deploymentName); + desiredDeployment.getMetadata().setNamespace(ns); + desiredDeployment.getMetadata().setLabels(lowLevelLabel()); + desiredDeployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + desiredDeployment.getSpec().getTemplate().getMetadata().getLabels().put("app", deploymentName); + desiredDeployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap(new ConfigMapVolumeSourceBuilder().withName(configMapName).build()); + desiredDeployment.addOwnerReference(webPage); + return desiredDeployment; + } + + private ConfigMap makeDesiredHtmlConfigMap(String ns, String configMapName, WebPage webPage) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + ConfigMap configMap = + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName) + .withNamespace(ns) + .withLabels(lowLevelLabel()) + .build()) + .withData(data) + .build(); + configMap.addOwnerReference(webPage); + return configMap; + } + + private Map lowLevelLabel() { + Map labels = new HashMap<>(); + labels.put(LOW_LEVEL_LABEL_KEY, "true"); + return labels; } private static String configMapName(WebPage nginx) { @@ -142,45 +209,10 @@ private static String serviceName(WebPage nginx) { return nginx.getMetadata().getName(); } - private class ConfigMapDependentResource extends KubernetesDependentResource - implements - AssociatedSecondaryResourceIdentifier, Updater { - - @Override - protected ConfigMap desired(WebPage webPage, Context context) { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(WebPageReconciler.configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - } - - @Override - public boolean match(ConfigMap actual, ConfigMap target, Context context) { - return StringUtils.equals( - actual.getData().get("index.html"), target.getData().get("index.html")); - } - - @Override - public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - } - - @Override - public ResourceID associatedSecondaryID(WebPage primary) { - return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); - } + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java new file mode 100644 index 0000000000..a313a0bf3d --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -0,0 +1,199 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CrudKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +/** + * Shows how to implement reconciler using standalone dependent resources. + */ +@ControllerConfiguration(finalizerName = NO_FINALIZER, + labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) +public class WebPageReconcilerDependentResources + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { + + public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; + private static final Logger log = + LoggerFactory.getLogger(WebPageReconcilerDependentResources.class); + private final KubernetesClient kubernetesClient; + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + + public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + return List.of( + configMapDR.eventSource(context), + deploymentDR.eventSource(context), + serviceDR.eventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + if (webPage.getSpec().getHtml().contains("error")) { + // special case just to showcase error if doing a demo + throw new ErrorSimulationException("Simulating error"); + } + + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } + + private WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); + status.setAreWeGood("Yes!"); + status.setErrorMessage(null); + return status; + } + + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.configMapDR.setKubernetesClient(client); + configMapDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.deploymentDR = + new CrudKubernetesDependentResource<>() { + + @Override + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + } + + @Override + protected Class resourceType() { + return Deployment.class; + } + }; + deploymentDR.setKubernetesClient(client); + deploymentDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.serviceDR = + new CrudKubernetesDependentResource<>() { + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, getClass(), "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } + + @Override + protected Class resourceType() { + return Service.class; + } + }; + serviceDR.setKubernetesClient(client); + serviceDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + } + + private static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + private static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private static String serviceName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private class ConfigMapDependentResource + extends CrudKubernetesDependentResource + implements + AssociatedSecondaryResourceIdentifier { + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(WebPageReconcilerDependentResources.configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java index db50be6c2d..562cda291d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java @@ -11,4 +11,11 @@ public String getHtml() { public void setHtml(String html) { this.html = html; } + + @Override + public String toString() { + return "WebPageSpec{" + + "html='" + html + '\'' + + '}'; + } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java index 2b2e2a23c8..3750e2f7c7 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -34,4 +34,13 @@ public WebPageStatus setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; return this; } + + @Override + public String toString() { + return "WebPageStatus{" + + "htmlConfigMap='" + htmlConfigMap + '\'' + + ", areWeGood='" + areWeGood + '\'' + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } } From a4582a3f5b147964b500aa09a695a18b8d7dc8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 08:33:59 +0100 Subject: [PATCH 0038/1319] feat: dependent resource context + my sql e2e test improvements (#979) --- .../operator/api/reconciler/Context.java | 3 ++ .../api/reconciler/DefaultContext.java | 8 ++++ .../ManagedDependentResourceContext.java | 36 +++++++++++++++++ .../operator/processing/Controller.java | 5 +++ .../dependent/DependentResourceManager.java | 4 ++ .../operator/sample/MySQLSchemaOperator.java | 2 + .../sample/MySQLSchemaReconciler.java | 40 +++++-------------- .../{ => dependent}/ResourcePollerConfig.java | 4 +- .../SchemaDependentResource.java | 31 +++++++++----- .../SchemaPollingResourceSupplier.java | 4 +- .../SecretDependentResource.java | 26 +++++++++--- .../sample/MySQLSchemaOperatorE2E.java | 2 + 12 files changed, 118 insertions(+), 47 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/ResourcePollerConfig.java (78%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/SchemaDependentResource.java (77%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/SchemaPollingResourceSupplier.java (81%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/SecretDependentResource.java (59%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 6948d0f6a7..c2d853f38a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; public interface Context extends AttributeHolder { @@ -22,4 +23,6 @@ default T getMandatory(Object key, Class expectedType) { } ConfigurationService getConfigurationService(); + + ManagedDependentResourceContext managedDependentResourceContext(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 46ee5a3fcf..e4aa164d37 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; public class DefaultContext

extends MapAttributeHolder implements Context { @@ -12,12 +13,15 @@ public class DefaultContext

extends MapAttributeHolder im private final Controller

controller; private final P primaryResource; private final ConfigurationService configurationService; + private ManagedDependentResourceContext managedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; this.configurationService = controller.getConfiguration().getConfigurationService(); + this.managedDependentResourceContext = new ManagedDependentResourceContext( + controller.getDependents()); } @Override @@ -36,4 +40,8 @@ public Optional getSecondaryResource(Class expectedType, String eventS public ConfigurationService getConfigurationService() { return configurationService; } + + public ManagedDependentResourceContext managedDependentResourceContext() { + return managedDependentResourceContext; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java new file mode 100644 index 0000000000..b2aa377aad --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.OperatorException; + +public class ManagedDependentResourceContext { + + private List dependentResources; + + public ManagedDependentResourceContext(List dependentResources) { + this.dependentResources = dependentResources; + } + + public List getDependentResources() { + return Collections.unmodifiableList(dependentResources); + } + + public T getDependentResource(Class resourceClass) { + var resourceList = + dependentResources.stream() + .filter(dr -> dr.getClass().equals(resourceClass)) + .collect(Collectors.toList()); + if (resourceList.isEmpty()) { + throw new OperatorException( + "No dependent resource found for class: " + resourceClass.getName()); + } + if (resourceList.size() > 1) { + throw new OperatorException( + "More than one dependent resource found for class: " + resourceClass.getName()); + } + return (T) resourceList.get(0); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index e8996c351c..a6c1979421 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -29,6 +29,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.dependent.DependentResourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -281,4 +282,8 @@ public void stop() { eventSourceManager.stop(); } } + + public List getDependents() { + return dependents.getDependents(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index e860cbe577..3f46e12ca5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -105,4 +105,8 @@ private DependentResource createAndConfigureFrom(DependentResourceSpec dependent throw new IllegalStateException(e); } } + + public List getDependents() { + return dependents; + } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index 150c72b391..8abf73f3f6 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -17,6 +17,8 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; +import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; public class MySQLSchemaOperator { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 5952a1508f..a6923823aa 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -2,21 +2,22 @@ import java.util.Optional; -import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import io.javaoperatorsdk.operator.sample.schema.Schema; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; +import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; +import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; // todo handle this, should work with finalizer @@ -26,42 +27,23 @@ @Dependent(type = SchemaDependentResource.class) }) public class MySQLSchemaReconciler - implements Reconciler, ErrorStatusHandler, - ContextInitializer { + implements Reconciler, ErrorStatusHandler { - static final String SECRET_FORMAT = "%s-secret"; - static final String USERNAME_FORMAT = "%s-user"; - - static final String MYSQL_SECRET_NAME = "mysql.secret.name"; - static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; - static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; - static final String BUILT_SCHEMA = "built schema"; static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); public MySQLSchemaReconciler() {} - @Override - public void initContext(MySQLSchema primary, Context context) { - final var name = primary.getMetadata().getName(); - final var password = RandomStringUtils - .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here - - final var secretName = String.format(SECRET_FORMAT, name); - final var userName = String.format(USERNAME_FORMAT, name); - - // put information in context for other dependents and reconciler to use - context.put(MYSQL_SECRET_PASSWORD, password); - context.put(MYSQL_SECRET_NAME, secretName); - context.put(MYSQL_SECRET_USERNAME, userName); - } @Override public UpdateControl reconcile(MySQLSchema schema, Context context) { // we only need to update the status if we just built the schema, i.e. when it's present in the // context - return context.get(BUILT_SCHEMA, Schema.class).map(s -> { - updateStatusPojo(schema, context.getMandatory(MYSQL_SECRET_NAME, String.class), - context.getMandatory(MYSQL_SECRET_USERNAME, String.class)); + Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); + SchemaDependentResource schemaDependentResource = context.managedDependentResourceContext() + .getDependentResource(SchemaDependentResource.class); + return schemaDependentResource.getResource(schema).map(s -> { + updateStatusPojo(schema, secret.getMetadata().getName(), + secret.getData().get(MYSQL_SECRET_USERNAME)); log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); return UpdateControl.updateStatus(schema); }).orElse(UpdateControl.noUpdate()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/ResourcePollerConfig.java similarity index 78% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/ResourcePollerConfig.java index 924c3978d1..5e9cc3f964 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/ResourcePollerConfig.java @@ -1,4 +1,6 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; + +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; public class ResourcePollerConfig { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java similarity index 77% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index b38d3cfdf8..0a97684e0d 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -1,10 +1,15 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Base64; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; @@ -14,9 +19,12 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.*; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; +import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_PASSWORD; +import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; public class SchemaDependentResource @@ -26,6 +34,8 @@ public class SchemaDependentResource Creator, Deleter { + private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); + private MySQLDbConfig dbConfig; private int pollPeriod = 500; @@ -53,25 +63,22 @@ public Schema desired(MySQLSchema primary, Context context) { @Override public void create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { + Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); + var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME)); + var password = decode(secret.getData().get(MYSQL_SECRET_PASSWORD)); final var schema = SchemaService.createSchemaAndRelatedUser( connection, target.getName(), - target.getCharacterSet(), - context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class), - context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class)); - - // put the newly built schema in the context to let the reconciler know we just built it - context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); + target.getCharacterSet(), username, password); } catch (SQLException e) { - MySQLSchemaReconciler.log.error("Error while creating Schema", e); + log.error("Error while creating Schema", e); throw new IllegalStateException(e); } } private Connection getConnection() throws SQLException { String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); - - MySQLSchemaReconciler.log.debug("Connecting to '{}' with user '{}'", connectURL, + log.debug("Connecting to '{}' with user '{}'", connectURL, dbConfig.getUser()); return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword()); } @@ -99,4 +106,8 @@ public Optional getResource(MySQLSchema primaryResource) { } } + private static String decode(String value) { + return new String(Base64.getDecoder().decode(value.getBytes())); + } + } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java similarity index 81% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java index e7be1b3c42..acf620e819 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java @@ -1,8 +1,10 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; import java.util.Optional; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; +import io.javaoperatorsdk.operator.sample.MySQLSchema; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java similarity index 59% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 5dfa20a571..b118b07f0c 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -1,19 +1,27 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; import java.util.Base64; +import org.apache.commons.lang3.RandomStringUtils; + import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.sample.MySQLSchema; import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; public class SecretDependentResource extends KubernetesDependentResource - implements AssociatedSecondaryResourceIdentifier, Updater { + implements AssociatedSecondaryResourceIdentifier, Creator { + + public static final String SECRET_FORMAT = "%s-secret"; + public static final String USERNAME_FORMAT = "%s-user"; + public static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; + public static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; private static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); @@ -21,15 +29,21 @@ private static String encode(String value) { @Override protected Secret desired(MySQLSchema schema, Context context) { + final var password = RandomStringUtils + .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here + final var name = schema.getMetadata().getName(); + final var secretName = String.format(SECRET_FORMAT, name); + final var userName = String.format(USERNAME_FORMAT, name); + return new SecretBuilder() .withNewMetadata() - .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) + .withName(secretName) .withNamespace(schema.getMetadata().getNamespace()) .endMetadata() .addToData( - "MYSQL_USERNAME", encode(context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) + MYSQL_SECRET_USERNAME, encode(userName)) .addToData( - "MYSQL_PASSWORD", encode(context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) + MYSQL_SECRET_PASSWORD, encode(password)) .build(); } diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index 2ebece8aa2..8f9302a81a 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -21,6 +21,8 @@ import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import static java.util.concurrent.TimeUnit.MINUTES; import static org.awaitility.Awaitility.await; From 67f972d26da9fa4cd84ce1a86ed554741c6b07ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 09:00:08 +0100 Subject: [PATCH 0039/1319] fix: remove map attribute holder (#981) --- .../api/reconciler/AttributeHolder.java | 12 -------- .../operator/api/reconciler/Context.java | 9 +----- .../api/reconciler/DefaultContext.java | 2 +- .../api/reconciler/EventSourceContext.java | 2 +- .../api/reconciler/MapAttributeHolder.java | 28 ------------------- 5 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java deleted file mode 100644 index 8fee3c2557..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.util.Optional; - -public interface AttributeHolder { - - Optional get(Object key, Class expectedType); - - T getMandatory(Object key, Class expectedType); - - Optional put(Object key, Object value); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index c2d853f38a..2f203fa07c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -5,7 +5,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; -public interface Context extends AttributeHolder { +public interface Context { Optional getRetryInfo(); @@ -15,13 +15,6 @@ default Optional getSecondaryResource(Class expectedType) { Optional getSecondaryResource(Class expectedType, String eventSourceName); - @Override - default T getMandatory(Object key, Class expectedType) { - return get(key, expectedType).orElseThrow(() -> new IllegalStateException( - "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() - + ") is missing or not of the expected type")); - } - ConfigurationService getConfigurationService(); ManagedDependentResourceContext managedDependentResourceContext(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index e4aa164d37..33f640789b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -7,7 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; -public class DefaultContext

extends MapAttributeHolder implements Context { +public class DefaultContext

implements Context { private final RetryInfo retryInfo; private final Controller

controller; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index 026af88923..1a6c82bc6f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -11,7 +11,7 @@ * * @param

the type associated with the primary resource that is handled by your reconciler */ -public class EventSourceContext

extends MapAttributeHolder { +public class EventSourceContext

{ private final ResourceCache

primaryCache; private final ConfigurationService configurationService; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java deleted file mode 100644 index aee5ee7e54..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -public class MapAttributeHolder { - - private final ConcurrentHashMap attributes = new ConcurrentHashMap(); - - public Optional get(Object key, Class expectedType) { - return Optional.ofNullable(attributes.get(key)) - .filter(expectedType::isInstance) - .map(expectedType::cast); - } - - public Optional put(Object key, Object value) { - if (value == null) { - return Optional.ofNullable(attributes.remove(key)); - } - return Optional.ofNullable(attributes.put(key, value)); - } - - public T getMandatory(Object key, Class expectedType) { - return get(key, expectedType).orElseThrow(() -> new IllegalStateException( - "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() - + ") is missing or not of the expected type")); - } -} From 1e17e753b8adefd9a84b29c2150005465487fc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 12:25:09 +0100 Subject: [PATCH 0040/1319] feat: executor service override (#983) --- .../config/ConfigurationServiceOverrider.java | 16 ++++++++++++++++ .../sample/PureJavaApplicationRunner.java | 10 +++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index c9b60d2617..e2251eea64 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.util.Set; +import java.util.concurrent.ExecutorService; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; @@ -16,6 +17,7 @@ public class ConfigurationServiceOverrider { private Cloner cloner; private int timeoutSeconds; private boolean closeClientOnStop; + private ExecutorService executorService = null; public ConfigurationServiceOverrider( ConfigurationService original) { @@ -65,6 +67,11 @@ public ConfigurationServiceOverrider withCloseClientOnStop(boolean close) { return this; } + public ConfigurationServiceOverrider withExecutorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + public ConfigurationService build() { return new ConfigurationService() { @Override @@ -120,6 +127,15 @@ public Metrics getMetrics() { public boolean closeClientOnStop() { return closeClientOnStop; } + + @Override + public ExecutorService getExecutorService() { + if (executorService != null) { + return executorService; + } else { + return ConfigurationService.super.getExecutorService(); + } + } }; } diff --git a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java index 2cc212340b..19d0a471b7 100644 --- a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample; +import java.util.concurrent.Executors; + import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @@ -8,9 +10,11 @@ public class PureJavaApplicationRunner { public static void main(String[] args) { Operator operator = - new Operator(ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) - .withConcurrentReconciliationThreads(2) - .build()); + new Operator( + ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) + .withExecutorService(Executors.newCachedThreadPool()) + .withConcurrentReconciliationThreads(2) + .build()); operator.register(new CustomServiceReconciler()); operator.start(); } From 0bd71d51d9637b6fa87460325928837de81a4429 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 1 Mar 2022 17:05:52 +0100 Subject: [PATCH 0041/1319] feat: re-add contextual map to context (#985) --- .../ManagedDependentResourceContext.java | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java index b2aa377aad..e0c0060646 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java @@ -2,22 +2,97 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import io.javaoperatorsdk.operator.OperatorException; +/** + * Contextual information related to {@link DependentResource} either to retrieve the actual + * implementations to interact with them or to pass information between them and/or the reconciler + */ +@SuppressWarnings("rawtypes") public class ManagedDependentResourceContext { - private List dependentResources; + private final List dependentResources; + private final ConcurrentHashMap attributes = new ConcurrentHashMap(); + + /** + * Retrieve a contextual object, if it exists and is of the specified expected type, associated + * with the specified key. Contextual objects can be used to pass data between the reconciler and + * dependent resources and are scoped to the current reconciliation. + * + * @param key the key identifying which contextual object to retrieve + * @param expectedType the class representing the expected type of the contextual object + * @param the type of the expected contextual object + * @return an Optional containing the contextual object or {@link Optional#empty()} if no such + * object exists or doesn't match the expected type + */ + public Optional get(Object key, Class expectedType) { + return Optional.ofNullable(attributes.get(key)) + .filter(expectedType::isInstance) + .map(expectedType::cast); + } + + /** + * Associates the specified contextual value to the specified key. If the value is {@code null}, + * the semantics of this operation is defined as removing the mapping associated with the + * specified key. + * + * @param key the key identifying which contextual object to add or remove from the context + * @param value the value to add to the context or {@code null} to remove an existing entry + * associated with the specified key + * @return an Optional containing the previous value associated with the key or + * {@link Optional#empty()} if none existed + */ + @SuppressWarnings("unchecked") + public Optional put(Object key, Object value) { + if (value == null) { + return Optional.ofNullable(attributes.remove(key)); + } + return Optional.ofNullable(attributes.put(key, value)); + } + + /** + * Retrieves the value associated with the key or fail with an exception if none exists. + * + * @param key the key identifying which contextual object to retrieve + * @param expectedType the expected type of the value to retrieve + * @param the type of the expected contextual object + * @return the contextual object value associated with the specified key + * @see #get(Object, Class) + */ + public T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } public ManagedDependentResourceContext(List dependentResources) { - this.dependentResources = dependentResources; + this.dependentResources = Collections.unmodifiableList(dependentResources); } + /** + * Retrieve all the known {@link DependentResource} implementations + * + * @return a list of known {@link DependentResource} implementations + */ public List getDependentResources() { - return Collections.unmodifiableList(dependentResources); + return dependentResources; } + /** + * Retrieve the dependent resource implementation associated with the specified resource type. + * + * @param resourceClass the dependent resource class for which we want to retrieve the associated + * dependent resource implementation + * @param the type of the resources for which we want to retrieve the associated dependent + * resource implementation + * @return the associated dependent resource implementation if it exists or an exception if it + * doesn't or several implementations are associated with the specified resource type + */ + @SuppressWarnings("unchecked") public T getDependentResource(Class resourceClass) { var resourceList = dependentResources.stream() From cd50a731432512eb1ca4d767054ebdef798d14cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 17:16:35 +0100 Subject: [PATCH 0042/1319] docs: filter docs javadoc (#984) --- .../api/config/ControllerConfiguration.java | 17 +++++++++-------- .../api/reconciler/ControllerConfiguration.java | 5 ++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index eb247752aa..57a5648c5d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -68,8 +68,7 @@ default boolean watchCurrentNamespace() { static boolean currentNamespaceWatched(Set namespaces) { return namespaces != null && namespaces.size() == 1 - && namespaces.contains( - Constants.WATCH_CURRENT_NAMESPACE); + && namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE); } /** @@ -101,15 +100,17 @@ default RetryConfiguration getRetryConfiguration() { default void setConfigurationService(ConfigurationService service) {} default boolean useFinalizer() { - return !Constants.NO_FINALIZER - .equals(getFinalizer()); + return !Constants.NO_FINALIZER.equals(getFinalizer()); } /** - * Allow controllers to filter events before they are provided to the - * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. Note that the provided - * filter is combined with {@link #isGenerationAware()} to compute the final set of filters that - * should be applied; + * Allow controllers to filter events before they are passed to the + * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. + * + *

+ * Resource event filters only applies on events of the main custom resource. Not on events from + * other event sources nor the periodic events. + *

* * @return filter */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index bd8863cad1..cd63ba2153 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -51,7 +51,10 @@ /** - * Optional list of classes providing custom {@link ResourceEventFilter}. + *

+ * Resource event filters only applies on events of the main custom resource. Not on events from + * other event sources nor the periodic events. + *

* * @return the list of event filters. */ From f5806d7b8ac9c4a9fc6f481eac96161d556da97a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 07:52:04 +0100 Subject: [PATCH 0043/1319] chore(deps): bump actions/checkout from 2 to 3 (#988) --- .github/workflows/e2e-test.yml | 2 +- .github/workflows/pr.yml | 4 ++-- .github/workflows/release.yml | 8 ++++---- .github/workflows/snapshot-releases.yml | 4 ++-- .github/workflows/sonar.yml | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 31b89e1b20..fe24ca4343 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Minikube-Kubernetes uses: manusa/actions-setup-minikube@v2.4.3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f6c5935cd0..2ca0df5fb2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ jobs: check_format_and_unit_tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: @@ -46,7 +46,7 @@ jobs: - java: 11 kubernetes: 'v1.22.1' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d250e8610a..99e7992f50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,11 +8,11 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} with: ref: "v1" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v2.') }} - name: Set up Java and Maven uses: actions/setup-java@v2 @@ -53,11 +53,11 @@ jobs: update-working-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} with: ref: "v1" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v2.') }} - name: Set up Java and Maven uses: actions/setup-java@v2 diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml index df593393a6..67c13b3bfc 100644 --- a/.github/workflows/snapshot-releases.yml +++ b/.github/workflows/snapshot-releases.yml @@ -14,7 +14,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 05c7d6f753..1611625618 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: From def54f51e3f7184de20ec15e256488eed9341f4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 07:52:54 +0100 Subject: [PATCH 0044/1319] chore(deps): bump actions/stale from 4.1.0 to 5 (#987) --- .github/workflows/stale-issues-and-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-issues-and-prs.yml b/.github/workflows/stale-issues-and-prs.yml index 604bc4fcd6..e5235ed13e 100644 --- a/.github/workflows/stale-issues-and-prs.yml +++ b/.github/workflows/stale-issues-and-prs.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v4.1.0 + - uses: actions/stale@v5 with: days-before-issue-stale: 60 days-before-pr-stale: 60 From 19c473fddd715b47647f504258e220cbfbdd883d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 3 Mar 2022 09:23:40 +0100 Subject: [PATCH 0045/1319] fix: webpage e2e test (#982) --- .github/workflows/e2e-test.yml | 1 + .../operator/junit/E2EOperatorExtension.java | 11 +- sample-operators/webpage/k8s/operator.yaml | 27 ++--- sample-operators/webpage/pom.xml | 8 +- .../operator/sample/WebPageOperator.java | 7 +- .../operator/sample/WebPageReconciler.java | 4 +- .../WebPageReconcilerDependentResources.java | 10 +- .../operator/sample/WebPageStatus.java | 6 +- .../operator/sample/ingress.yaml | 19 --- .../sample/WebPageOperatorAbstractTest.java | 113 ++++++++++++++++++ .../WebPageOperatorDependentResourcesE2E.java | 35 ++++++ .../operator/sample/WebPageOperatorE2E.java | 62 ++++++++++ 12 files changed, 251 insertions(+), 52 deletions(-) delete mode 100644 sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml create mode 100644 sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java create mode 100644 sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java create mode 100644 sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 774df82b48..9c555b9e4b 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -16,6 +16,7 @@ jobs: sample_dir: - "sample-operators/mysql-schema" - "sample-operators/tomcat-operator" + - "sample-operators/webpage" runs-on: ubuntu-latest steps: - name: Checkout diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java index 91fecafc2e..55e6cc5c30 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java @@ -10,6 +10,7 @@ import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; @@ -70,7 +71,7 @@ protected void before(ExtensionContext context) { } } - LOGGER.debug("Deploying the operator into Kubernetes"); + LOGGER.debug("Deploying the operator into Kubernetes. Target namespace: {}", namespace); operatorDeployment.forEach(hm -> { hm.getMetadata().setNamespace(namespace); if (hm.getKind().toLowerCase(Locale.ROOT).equals("clusterrolebinding")) { @@ -88,6 +89,7 @@ protected void before(ExtensionContext context) { kubernetesClient .resourceList(operatorDeployment) .waitUntilReady(operatorDeploymentTimeout.toMillis(), TimeUnit.MILLISECONDS); + LOGGER.debug("Operator resources deployed."); } @Override @@ -110,6 +112,13 @@ public Builder withDeploymentTimeout(Duration value) { return this; } + public Builder withOperatorDeployment(List hm, + Consumer> modifications) { + modifications.accept(hm); + operatorDeployment.addAll(hm); + return this; + } + public Builder withOperatorDeployment(List hm) { operatorDeployment.addAll(hm); return this; diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml index cfdb1fe47c..0b673bf204 100644 --- a/sample-operators/webpage/k8s/operator.yaml +++ b/sample-operators/webpage/k8s/operator.yaml @@ -1,35 +1,27 @@ apiVersion: v1 -kind: Namespace -metadata: - name: webserver-operator - ---- -apiVersion: v1 kind: ServiceAccount metadata: - name: webserver-operator - namespace: webserver-operator + name: webpage-operator --- apiVersion: apps/v1 kind: Deployment metadata: - name: webserver-operator - namespace: webserver-operator + name: webpage-operator spec: selector: matchLabels: - app: webserver-operator + app: webpage-operator replicas: 1 template: metadata: labels: - app: webserver-operator + app: webpage-operator spec: - serviceAccountName: webserver-operator + serviceAccountName: webpage-operator containers: - name: operator - image: webserver-operator + image: webpage-operator imagePullPolicy: Never ports: - containerPort: 80 @@ -53,18 +45,17 @@ metadata: name: operator-admin subjects: - kind: ServiceAccount - name: webserver-operator - namespace: webserver-operator + name: webpage-operator roleRef: kind: ClusterRole - name: webserver-operator + name: webpage-operator apiGroup: "" --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: webserver-operator + name: webpage-operator rules: - apiGroups: - "" diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index f23c7407c5..93e32953c3 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -45,6 +45,12 @@ awaitility compile + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + @@ -57,7 +63,7 @@ gcr.io/distroless/java:11 - webserver-operator + webpage-operator diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index b61b34334e..0b643462b1 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; -import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,16 +17,18 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class WebPageOperator { - + public static final String WEBPAGE_RECONCILER_ENV = "WEBPAGE_RECONCILER"; + public static final String WEBPAGE_RECONCILER_ENV_VALUE = "classic"; private static final Logger log = LoggerFactory.getLogger(WebPageOperator.class); + public static void main(String[] args) throws IOException { log.info("WebServer Operator starting!"); Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); - if (Arrays.stream(args).anyMatch(arg -> arg.equals("--classic"))) { + if (WEBPAGE_RECONCILER_ENV_VALUE.equals(System.getenv(WEBPAGE_RECONCILER_ENV))) { operator.register(new WebPageReconciler(client)); } else { operator.register(new WebPageReconcilerDependentResources(client)); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index b2727e065b..90fb61251f 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -111,7 +111,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { private WebPageStatus createStatus(String configMapName) { WebPageStatus status = new WebPageStatus(); status.setHtmlConfigMap(configMapName); - status.setAreWeGood("Yes!"); + status.setAreWeGood(true); status.setErrorMessage(null); return status; } @@ -191,7 +191,7 @@ private ConfigMap makeDesiredHtmlConfigMap(String ns, String configMapName, WebP return configMap; } - private Map lowLevelLabel() { + public static Map lowLevelLabel() { Map labels = new HashMap<>(); labels.put(LOW_LEVEL_LABEL_KEY, "true"); return labels; diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index a313a0bf3d..cb5144a8d3 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -69,7 +69,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { private WebPageStatus createStatus(String configMapName) { WebPageStatus status = new WebPageStatus(); status.setHtmlConfigMap(configMapName); - status.setAreWeGood("Yes!"); + status.setAreWeGood(true); status.setErrorMessage(null); return status; } @@ -148,16 +148,16 @@ protected Class resourceType() { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); } - private static String configMapName(WebPage nginx) { + public static String configMapName(WebPage nginx) { return nginx.getMetadata().getName() + "-html"; } - private static String deploymentName(WebPage nginx) { + public static String deploymentName(WebPage nginx) { return nginx.getMetadata().getName(); } - private static String serviceName(WebPage nginx) { - return nginx.getMetadata().getName(); + public static String serviceName(WebPage webPage) { + return webPage.getMetadata().getName(); } private class ConfigMapDependentResource diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java index 3750e2f7c7..5f66646b92 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -6,7 +6,7 @@ public class WebPageStatus extends ObservedGenerationAwareStatus { private String htmlConfigMap; - private String areWeGood; + private Boolean areWeGood; private String errorMessage; @@ -18,11 +18,11 @@ public void setHtmlConfigMap(String htmlConfigMap) { this.htmlConfigMap = htmlConfigMap; } - public String getAreWeGood() { + public Boolean getAreWeGood() { return areWeGood; } - public void setAreWeGood(String areWeGood) { + public void setAreWeGood(Boolean areWeGood) { this.areWeGood = areWeGood; } diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml deleted file mode 100644 index 6fd1ed93e9..0000000000 --- a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - replicas: 1 - template: - metadata: - labels: - app: - spec: - containers: - - name: nginx - image: nginx:1.7.9 - ports: - - containerPort: 80 \ No newline at end of file diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java new file mode 100644 index 0000000000..092ad6f25a --- /dev/null +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java @@ -0,0 +1,113 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Objects; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.LocalPortForward; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; + +import static io.javaoperatorsdk.operator.sample.WebPageReconcilerDependentResources.serviceName; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public abstract class WebPageOperatorAbstractTest { + + static final Logger log = LoggerFactory.getLogger(WebPageOperatorDependentResourcesE2E.class); + + static final KubernetesClient client = new DefaultKubernetesClient(); + public static final String TEST_PAGE = "test-page"; + + boolean isLocal() { + String deployment = System.getProperty("test.deployment"); + boolean remote = (deployment != null && deployment.equals("remote")); + log.info("Running the operator " + (remote ? "remote" : "locally")); + return !remote; + } + + @Test + void testAddingWebPage() { + + var webPage = createWebPage(); + operator().create(WebPage.class, webPage); + + await() + .atMost(Duration.ofSeconds(20)) + .pollInterval(Duration.ofSeconds(1)) + .until( + () -> { + var actual = operator().get(WebPage.class, TEST_PAGE); + var deployment = operator().get(Deployment.class, + WebPageReconcilerDependentResources.deploymentName(webPage)); + + return Boolean.TRUE.equals(actual.getStatus().getAreWeGood()) + && Objects.equals(deployment.getSpec().getReplicas(), + deployment.getStatus().getReadyReplicas()); + }); + + String response = httpGetForWebPage(webPage); + assertThat(response).contains("Hello Operator World"); + } + + String httpGetForWebPage(WebPage webPage) { + LocalPortForward portForward = null; + try { + portForward = + client.services().inNamespace(webPage.getMetadata().getNamespace()) + .withName(serviceName(webPage)).portForward(80); + HttpClient httpClient = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + HttpRequest request = + HttpRequest.newBuilder().GET() + .uri(new URI("http://localhost/" + portForward.getLocalPort())).build(); + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body(); + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new IllegalStateException(e); + } finally { + if (portForward != null) { + try { + portForward.close(); + } catch (IOException e) { + log.error("Port forward close error.", e); + } + } + } + } + + WebPage createWebPage() { + WebPage webPage = new WebPage(); + webPage.setMetadata(new ObjectMeta()); + webPage.getMetadata().setName(TEST_PAGE); + webPage.getMetadata().setNamespace(operator().getNamespace()); + webPage.setSpec(new WebPageSpec()); + webPage + .getSpec() + .setHtml( + "\n" + + " \n" + + " Hello Operator World\n" + + " \n" + + " \n" + + " Hello World! \n" + + " \n" + + " "); + + return webPage; + } + + abstract AbstractOperatorExtension operator(); + +} diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java new file mode 100644 index 0000000000..580a2022c7 --- /dev/null +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; +import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.OperatorExtension; + +class WebPageOperatorDependentResourcesE2E extends WebPageOperatorAbstractTest { + + public WebPageOperatorDependentResourcesE2E() throws FileNotFoundException {} + + @RegisterExtension + AbstractOperatorExtension operator = + isLocal() + ? OperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new WebPageReconcilerDependentResources(client)) + .build() + : E2EOperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) + .build(); + + @Override + AbstractOperatorExtension operator() { + return operator; + } +} diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java new file mode 100644 index 0000000000..a096b03965 --- /dev/null +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; +import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.OperatorExtension; + +import static io.javaoperatorsdk.operator.sample.WebPageOperator.WEBPAGE_RECONCILER_ENV; +import static io.javaoperatorsdk.operator.sample.WebPageOperator.WEBPAGE_RECONCILER_ENV_VALUE; +import static io.javaoperatorsdk.operator.sample.WebPageReconciler.lowLevelLabel; + +class WebPageOperatorE2E extends WebPageOperatorAbstractTest { + + public WebPageOperatorE2E() throws FileNotFoundException {} + + @RegisterExtension + AbstractOperatorExtension operator = + isLocal() + ? OperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new WebPageReconciler(client)) + .build() + : E2EOperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get(), + resources -> { + Deployment deployment = (Deployment) resources.stream() + .filter(r -> r instanceof Deployment).findFirst().orElseThrow(); + Container container = + deployment.getSpec().getTemplate().getSpec().getContainers().get(0); + if (container.getEnv() == null) { + container.setEnv(new ArrayList<>()); + } + container.getEnv().add( + new EnvVar(WEBPAGE_RECONCILER_ENV, WEBPAGE_RECONCILER_ENV_VALUE, null)); + }) + .build(); + + + @Override + AbstractOperatorExtension operator() { + return operator; + } + + @Override + WebPage createWebPage() { + WebPage page = super.createWebPage(); + page.getMetadata().setLabels(lowLevelLabel()); + return page; + } +} From 7433c8bb89faf10a5e1a93cf4a53022a6b991be2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 10:03:18 +0100 Subject: [PATCH 0046/1319] chore(deps): bump formatter-maven-plugin from 2.17.1 to 2.18.0 (#989) --- .../operator/api/ObservedGenerationAware.java | 2 +- .../api/config/ConfigurationService.java | 2 +- .../api/reconciler/EventSourceContext.java | 4 +- .../reconciler/EventSourceInitializer.java | 4 +- .../processing/event/source/EventSource.java | 4 +- .../source/informer/InformerEventSource.java | 131 ++++++++++-------- .../source/polling/PollingEventSource.java | 45 +++--- pom.xml | 2 +- 8 files changed, 105 insertions(+), 89 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index 43927b8136..069953a32d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -15,7 +15,7 @@ * {@link UpdateControl#updateResource(HasMetadata)} is called. Although those results call normally * does not result in a status update, there will be a subsequent status update Kubernetes API call * in this case. - * + * * @see ObservedGenerationAwareStatus */ public interface ObservedGenerationAware { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 2e4679efd3..49ccc1da07 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -92,7 +92,7 @@ default int concurrentReconciliationThreads() { /** * Used to clone custom resources. - * + * * @return the ObjectMapper to use */ default Cloner getResourceCloner() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index 0a93d33d40..ba32f493ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -39,7 +39,7 @@ public ResourceCache

getPrimaryCache() { * {@link io.javaoperatorsdk.operator.api.monitoring.Metrics} or * {@link io.javaoperatorsdk.operator.api.config.Cloner} implementations, which could be useful to * event sources. - * + * * @return the {@link ConfigurationService} associated with the operator */ public ConfigurationService getConfigurationService() { @@ -49,7 +49,7 @@ public ConfigurationService getConfigurationService() { /** * Provides access to the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. - * + * * @return the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 79b15380a5..562ddc3b8f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -8,14 +8,14 @@ /** * An interface that a {@link Reconciler} can implement to have the SDK register the provided * {@link EventSource} - * + * * @param

the primary resource type handled by the associated {@link Reconciler} */ public interface EventSourceInitializer

{ /** * Prepares a list of {@link EventSource} implementations to be registered by the SDK. - * + * * @param context a {@link EventSourceContext} providing access to information useful to event * sources * @return list of event sources to register diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java index 04e19f7ceb..10a690c0e8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java @@ -15,7 +15,7 @@ public interface EventSource extends LifecycleAware { /** * An optional name for your EventSource. This is only required if you need to register multiple * EventSources for the same resource type (e.g. {@code Deployment}). - * + * * @return the name associated with this EventSource */ default String name() { @@ -25,7 +25,7 @@ default String name() { /** * Sets the {@link EventHandler} that is linked to your reconciler when this EventSource is * registered. - * + * * @param handler the {@link EventHandler} associated with your reconciler */ void setEventHandler(EventHandler handler); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index eada096713..95ed3b6ef0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -22,8 +22,7 @@ import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; public class InformerEventSource - extends AbstractResourceEventSource - implements ResourceCache { + extends AbstractResourceEventSource implements ResourceCache { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); @@ -32,33 +31,46 @@ public class InformerEventSource private final AssociatedSecondaryResourceIdentifier

associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; - public InformerEventSource(SharedInformer sharedInformer, + public InformerEventSource( + SharedInformer sharedInformer, PrimaryResourcesRetriever resourceToTargetResourceIDSet) { this(sharedInformer, resourceToTargetResourceIDSet, null, true); } - public InformerEventSource(KubernetesClient client, Class type, + public InformerEventSource( + KubernetesClient client, + Class type, PrimaryResourcesRetriever resourceToTargetResourceIDSet) { this(client, type, resourceToTargetResourceIDSet, false); } - public InformerEventSource(KubernetesClient client, Class type, + public InformerEventSource( + KubernetesClient client, + Class type, PrimaryResourcesRetriever resourceToTargetResourceIDSet, AssociatedSecondaryResourceIdentifier

associatedWith, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, + this( + client.informers().sharedIndexInformerFor(type, 0), + resourceToTargetResourceIDSet, associatedWith, skipUpdateEventPropagationIfNoChange); } - InformerEventSource(KubernetesClient client, Class type, + InformerEventSource( + KubernetesClient client, + Class type, PrimaryResourcesRetriever resourceToTargetResourceIDSet, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, + this( + client.informers().sharedIndexInformerFor(type, 0), + resourceToTargetResourceIDSet, + null, skipUpdateEventPropagationIfNoChange); } - public InformerEventSource(SharedInformer sharedInformer, + public InformerEventSource( + SharedInformer sharedInformer, PrimaryResourcesRetriever resourceToTargetResourceIDSet, AssociatedSecondaryResourceIdentifier

associatedWith, boolean skipUpdateEventPropagationIfNoChange) { @@ -68,41 +80,44 @@ public InformerEventSource(SharedInformer sharedInformer, this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; if (sharedInformer.isRunning()) { log.warn( - "Informer is already running on event source creation, this is not desirable and may " + - "lead to non deterministic behavior."); + "Informer is already running on event source creation, this is not desirable and may " + + "lead to non deterministic behavior."); } this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - sharedInformer.addEventHandler(new ResourceEventHandler<>() { - @Override - public void onAdd(T t) { - propagateEvent(t); - } - - @Override - public void onUpdate(T oldObject, T newObject) { - if (newObject == null) { - // this is a fix for this potential issue with informer: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 - propagateEvent(oldObject); - return; - } - - if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange && - oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { - return; - } - propagateEvent(newObject); - } - - @Override - public void onDelete(T t, boolean b) { - propagateEvent(t); - } - }); + sharedInformer.addEventHandler( + new ResourceEventHandler<>() { + @Override + public void onAdd(T t) { + propagateEvent(t); + } + + @Override + public void onUpdate(T oldObject, T newObject) { + if (newObject == null) { + // this is a fix for this potential issue with informer: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 + propagateEvent(oldObject); + return; + } + + if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange + && oldObject + .getMetadata() + .getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { + return; + } + propagateEvent(newObject); + } + + @Override + public void onDelete(T t, boolean b) { + propagateEvent(t); + } + }); } private void propagateEvent(T object) { @@ -110,18 +125,19 @@ private void propagateEvent(T object) { if (primaryResourceIdSet.isEmpty()) { return; } - primaryResourceIdSet.forEach(resourceId -> { - Event event = new Event(resourceId); - /* - * In fabric8 client for certain cases informers can be created on in a way that they are - * automatically started, what would cause a NullPointerException here, since an event might - * be received between creation and registration. - */ - final EventHandler eventHandler = getEventHandler(); - if (eventHandler != null) { - eventHandler.handleEvent(event); - } - }); + primaryResourceIdSet.forEach( + resourceId -> { + Event event = new Event(resourceId); + /* + * In fabric8 client for certain cases informers can be created on in a way that they are + * automatically started, what would cause a NullPointerException here, since an event + * might be received between creation and registration. + */ + final EventHandler eventHandler = getEventHandler(); + if (eventHandler != null) { + eventHandler.handleEvent(event); + } + }); } @Override @@ -141,7 +157,7 @@ private Store getStore() { /** * Retrieves the informed resource associated with the specified primary resource as defined by * the function provided when this InformerEventSource was created - * + * * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ @@ -150,17 +166,18 @@ public Optional getAssociated(P resource) { return get(id); } - public SharedInformer getSharedInformer() { return sharedInformer; } @Override public Optional get(ResourceID resourceID) { - return Optional.ofNullable(sharedInformer.getStore() - .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( - resourceID.getNamespace().orElse(null), - resourceID.getName()))); + return Optional.ofNullable( + sharedInformer + .getStore() + .getByKey( + io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( + resourceID.getNamespace().orElse(null), resourceID.getName()))); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index 4702bd8c38..f9a4265c04 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -12,29 +12,26 @@ import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; /** - *

* Polls resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but * instead to calls supplier periodically and independently of the number of state of custom * resources managed by the operator. It is called on start (synced). This means that when the * reconciler first time executed on startup a poll already happened before. So if the cache does * not contain the target resource it means it is not created yet or was deleted while an operator * was not running. - *

+ * *

* Another caveat with this is if the cached object is checked in the reconciler and created since * not in the cache it should be manually added to the cache, since it can happen that the * reconciler is triggered before the cache is propagated with the new resource from a scheduled - * execution. See {@link #put(ResourceID, Object)} method. - *

- * So the generic workflow in reconciler should be: + * execution. See {@link #put(ResourceID, Object)} method. So the generic workflow in reconciler + * should be: * *
    - *
  • Check if the cache contains the resource.
  • + *
  • Check if the cache contains the resource. *
  • If cache contains the resource reconcile it - compare with target state, update if necessary - *
  • - *
  • if cache not contains the resource create it.
  • + *
  • if cache not contains the resource create it. *
  • If the resource was created or updated, put the new version of the resource manually to the - * cache.
  • + * cache. *
* * @param type of the polled resource @@ -48,8 +45,8 @@ public class PollingEventSource extends CachingEventSo private final Supplier> supplierToPoll; private final long period; - public PollingEventSource(Supplier> supplier, - long period, Class resourceClass) { + public PollingEventSource( + Supplier> supplier, long period, Class resourceClass) { super(resourceClass); this.supplierToPoll = supplier; this.period = period; @@ -59,16 +56,19 @@ public PollingEventSource(Supplier> supplier, public void start() throws OperatorException { super.start(); getStateAndFillCache(); - timer.schedule(new TimerTask() { - @Override - public void run() { - if (!isRunning()) { - log.debug("Event source not yet started. Will not run."); - return; - } - getStateAndFillCache(); - } - }, period, period); + timer.schedule( + new TimerTask() { + @Override + public void run() { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run."); + return; + } + getStateAndFillCache(); + } + }, + period, + period); } protected void getStateAndFillCache() { @@ -89,7 +89,7 @@ public void stop() throws OperatorException { /** * See {@link PerResourcePollingEventSource} for more info. - * + * * @param primary custom resource * @return related resource */ @@ -97,5 +97,4 @@ public void stop() throws OperatorException { public Optional getAssociated(P primary) { return getCachedValue(ResourceID.fromResource(primary)); } - } diff --git a/pom.xml b/pom.xml index dfca9ec391..5412cfac2a 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 2.8.2 2.5.2 5.0.0 - 2.17.1 + 2.18.0 1.0 1.6.2 From 57a29abcb2bb290b568f94ae7e7ca9239abaa233 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 3 Mar 2022 10:08:24 +0100 Subject: [PATCH 0047/1319] fix: merge from master --- .../dependent/kubernetes/KubernetesDependentResource.java | 2 +- .../processing/event/source/informer/InformerEventSource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 8ce3560fdc..bbdf74da5c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -80,7 +80,7 @@ private void configureWith(ConfigurationService configService, String labelSelec /** * Use to share informers between event more resources. - * + * * @param informerEventSource informer to use * @param addOwnerReference to the created resource */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 2f8705b63a..006564426e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -224,7 +224,7 @@ public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resou /** * Mean to be called to clean up in case of an exception from the client. Usually in a catch * block. - * + * * @param resourceID of the resource */ public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) { From 4aa7b80a4ce1784b57b91672df5661bb7b5a5271 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 3 Mar 2022 10:09:08 +0100 Subject: [PATCH 0048/1319] fix: formatting issue for new version for formatter plugin --- .../operator/api/reconciler/ControllerConfiguration.java | 4 ++-- .../dependent/ManagedDependentResourceContext.java | 8 ++++---- .../event/source/informer/TemporaryResourceCache.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index f71ef5c4f0..0feaa34b2d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -62,7 +62,7 @@ /** * Optional configuration of the maximal interval the SDK will wait for a reconciliation request * to happen before one will be automatically triggered. - * + * * @return the maximal interval configuration */ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval( @@ -71,7 +71,7 @@ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMax /** * Optional list of {@link Dependent} configurations which associate a resource type to a * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation - * + * * @return the list of {@link Dependent} configurations */ Dependent[] dependents() default {}; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java index e0c0060646..9543b7d582 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java @@ -22,7 +22,7 @@ public class ManagedDependentResourceContext { * Retrieve a contextual object, if it exists and is of the specified expected type, associated * with the specified key. Contextual objects can be used to pass data between the reconciler and * dependent resources and are scoped to the current reconciliation. - * + * * @param key the key identifying which contextual object to retrieve * @param expectedType the class representing the expected type of the contextual object * @param the type of the expected contextual object @@ -39,7 +39,7 @@ public Optional get(Object key, Class expectedType) { * Associates the specified contextual value to the specified key. If the value is {@code null}, * the semantics of this operation is defined as removing the mapping associated with the * specified key. - * + * * @param key the key identifying which contextual object to add or remove from the context * @param value the value to add to the context or {@code null} to remove an existing entry * associated with the specified key @@ -75,7 +75,7 @@ public ManagedDependentResourceContext(List dependentResource /** * Retrieve all the known {@link DependentResource} implementations - * + * * @return a list of known {@link DependentResource} implementations */ public List getDependentResources() { @@ -84,7 +84,7 @@ public List getDependentResources() { /** * Retrieve the dependent resource implementation associated with the specified resource type. - * + * * @param resourceClass the dependent resource class for which we want to retrieve the associated * dependent resource implementation * @param the type of the resources for which we want to retrieve the associated dependent diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 64c93484b8..fb14ea6847 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -28,7 +28,7 @@ * received from the informer, it means that the cache of the informer was updated, so it already * contains a more fresh version of the resource. *

- * + * * @param resource to cache. */ public class TemporaryResourceCache { From 9d269a62cb7ede1cfe96292fd613a405dc0ddb65 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Mar 2022 09:40:52 +0100 Subject: [PATCH 0049/1319] feat: matcher avoiding creating the desired state when possible (#992) --- .../dependent/AbstractDependentResource.java | 14 +++-- .../api/reconciler/dependent/Matcher.java | 33 ++++++++++- .../api/reconciler/dependent/Updater.java | 16 ++++-- .../GenericKubernetesResourceMatcher.java | 41 ++++++++----- .../KubernetesDependentResource.java | 29 ++++++++-- .../GenericKubernetesResourceMatcherTest.java | 57 +++++++++++++------ .../dependent/SecretDependentResource.java | 18 +++--- 7 files changed, 152 insertions(+), 56 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 456e9339de..8700af0805 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -36,19 +36,23 @@ public void reconcile(P primary, Context context) { final var updatable = isUpdatable(primary, context); if (creatable || updatable) { var maybeActual = getResource(primary); - var desired = desired(primary, context); if (maybeActual.isEmpty()) { if (creatable) { + var desired = desired(primary, context); log.debug("Creating dependent {} for primary {}", desired, primary); creator.create(desired, primary, context); } } else { final var actual = maybeActual.get(); - if (updatable && !updater.match(actual, desired, context)) { - log.debug("Updating dependent {} for primary {}", desired, primary); - updater.update(actual, desired, primary, context); + if (updatable) { + final var match = updater.match(actual, primary, context); + if (!match.matched()) { + final var desired = match.computedDesired().orElse(desired(primary, context)); + log.debug("Updating dependent {} for primary {}", desired, primary); + updater.update(actual, desired, primary, context); + } } else { - log.debug("Update skipped for dependent {} as it matched the existing one", desired); + log.debug("Update skipped for dependent {} as it matched the existing one", actual); } } } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java index ad82daf661..f0c5bd7fd9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java @@ -1,7 +1,36 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -public interface Matcher { - boolean match(R actualResource, R desiredResource, Context context); +public interface Matcher { + interface Result { + boolean matched(); + + default Optional computedDesired() { + return Optional.empty(); + } + + static Result nonComputed(boolean matched) { + return () -> matched; + } + + static Result computed(boolean matched, T computedDesired) { + return new Result<>() { + @Override + public boolean matched() { + return matched; + } + + @Override + public Optional computedDesired() { + return Optional.of(computedDesired); + } + }; + } + } + + Result match(R actualResource, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java index 6984f788f8..62199e1ede 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -1,18 +1,22 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; -import java.util.Objects; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; @SuppressWarnings("rawtypes") public interface Updater { - Updater NOOP = (actual, desired, primary, context) -> { + Updater NOOP = new Updater() { + @Override + public void update(Object actual, Object desired, HasMetadata primary, Context context) {} + + @Override + public Result match(Object actualResource, HasMetadata primary, Context context) { + return Result.nonComputed(true); + } }; void update(R actual, R desired, P primary, Context context); - default boolean match(R actualResource, R desiredResource, Context context) { - return Objects.equals(actualResource, desiredResource); - } + Result match(R actualResource, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 1c3c7d0822..25d7c098d5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -8,37 +8,52 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; -public class GenericKubernetesResourceMatcher implements Matcher { +public class GenericKubernetesResourceMatcher + implements Matcher { - private GenericKubernetesResourceMatcher() {} + private final KubernetesDependentResource dependentResource; - @SuppressWarnings({"rawtypes", "unchecked"}) - public static Matcher matcherFor(Class resourceType) { + private GenericKubernetesResourceMatcher(KubernetesDependentResource dependentResource) { + this.dependentResource = dependentResource; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static Matcher matcherFor( + Class resourceType, KubernetesDependentResource dependentResource) { if (Secret.class.isAssignableFrom(resourceType)) { - return (actual, desired, context) -> ResourceComparators.compareSecretData((Secret) desired, - (Secret) actual); + return (actual, primary, context) -> { + final var desired = dependentResource.desired(primary, context); + return Result.computed( + ResourceComparators.compareSecretData((Secret) desired, (Secret) actual), desired); + }; } else if (ConfigMap.class.isAssignableFrom(resourceType)) { - return (actual, desired, context) -> ResourceComparators - .compareConfigMapData((ConfigMap) desired, (ConfigMap) actual); + return (actual, primary, context) -> { + final var desired = dependentResource.desired(primary, context); + return Result.computed( + ResourceComparators.compareConfigMapData((ConfigMap) desired, (ConfigMap) actual), + desired); + }; } else { - return new GenericKubernetesResourceMatcher(); + return new GenericKubernetesResourceMatcher(dependentResource); } } @Override - public boolean match(R actualResource, R desiredResource, Context context) { + public Result match(R actualResource, P primary, Context context) { final var objectMapper = context.getConfigurationService().getObjectMapper(); + final var desired = dependentResource.desired(primary, context); + // reflection will be replaced by this: // https://github.com/fabric8io/kubernetes-client/issues/3816 - var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource)); + var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desired)); var actualSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(actualResource)); var diffJsonPatch = JsonDiff.asJson(desiredSpecNode, actualSpecNode); for (int i = 0; i < diffJsonPatch.size(); i++) { String operation = diffJsonPatch.get(i).get("op").asText(); if (!operation.equals("add")) { - return false; + return Result.computed(false, desired); } } - return true; + return Result.computed(true, desired); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index bbdf74da5c..04e3c87a54 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -21,7 +21,9 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -39,14 +41,24 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; - private final Matcher matcher; + private final Matcher matcher; private final ResourceUpdatePreProcessor processor; @SuppressWarnings("unchecked") public KubernetesDependentResource() { - init(this::create, this::update, this::delete); - matcher = this instanceof Matcher ? (Matcher) this - : GenericKubernetesResourceMatcher.matcherFor(resourceType()); + init(this::create, new Updater<>() { + @Override + public void update(R actual, R desired, P primary, Context context) { + KubernetesDependentResource.this.update(actual, desired, primary, context); + } + + @Override + public Result match(R actualResource, P primary, Context context) { + return KubernetesDependentResource.this.match(actualResource, primary, context); + } + }, this::delete); + matcher = this instanceof Matcher ? (Matcher) this + : GenericKubernetesResourceMatcher.matcherFor(resourceType(), this); processor = this instanceof ResourceUpdatePreProcessor ? (ResourceUpdatePreProcessor) this : GenericResourceUpdatePreProcessor.processorFor(resourceType()); @@ -117,8 +129,8 @@ public void update(R actual, R target, P primary, Context context) { } } - public boolean match(R actualResource, R desiredResource, Context context) { - return matcher.match(actualResource, desiredResource, context); + public Result match(R actualResource, P primary, Context context) { + return matcher.match(actualResource, primary, context); } public void delete(P primary, Context context) { @@ -173,4 +185,9 @@ public Optional getResource(P primaryResource) { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } + + @Override + protected R desired(P primary, Context context) { + return super.desired(primary, context); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 6302c20e6d..3ae424cdfb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -1,8 +1,12 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; +import java.util.Optional; + import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -24,29 +28,40 @@ class GenericKubernetesResourceMatcherTest { @Test void checksIfDesiredValuesAreTheSame() { - var target1 = createDeployment(); - var desired1 = createDeployment(); - final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class); - assertThat(matcher.match(target1, desired1, context)).isTrue(); - - var target2 = createDeployment(); - var desired2 = createDeployment(); - target2.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(matcher.match(target2, desired2, context)) + var actual = createDeployment(); + final var desired = createDeployment(); + final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class, + new KubernetesDependentResource<>() { + @Override + protected Deployment desired(HasMetadata primary, Context context) { + final var currentCase = Optional.ofNullable(primary) + .map(p -> p.getMetadata().getLabels().get("case")) + .orElse(null); + var d = desired; + if ("removed".equals(currentCase)) { + d = createDeployment(); + d.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + } + return d; + } + }); + assertThat(matcher.match(actual, null, context).matched()).isTrue(); + assertThat(matcher.match(actual, null, context).computedDesired().isPresent()).isTrue(); + assertThat(matcher.match(actual, null, context).computedDesired().get()).isEqualTo(desired); + + actual.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + assertThat(matcher.match(actual, null, context).matched()) .withFailMessage("Additive changes should be ok") .isTrue(); - var target3 = createDeployment(); - var desired3 = createDeployment(); - desired3.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(matcher.match(target3, desired3, context)) + actual = createDeployment(); + assertThat(matcher.match(actual, createPrimary("removed"), context).matched()) .withFailMessage("Removed value should not be ok") .isFalse(); - var target4 = createDeployment(); - var desired4 = createDeployment(); - target4.getSpec().setReplicas(2); - assertThat(matcher.match(target4, desired4, context)) + actual = createDeployment(); + actual.getSpec().setReplicas(2); + assertThat(matcher.match(actual, null, context).matched()) .withFailMessage("Changed values are not ok") .isFalse(); } @@ -55,4 +70,12 @@ Deployment createDeployment() { return ReconcilerUtils.loadYaml( Deployment.class, GenericKubernetesResourceMatcherTest.class, "nginx-deployment.yaml"); } + + HasMetadata createPrimary(String caseName) { + return new DeploymentBuilder() + .editOrNewMetadata() + .addToLabels("case", caseName) + .endMetadata() + .build(); + } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index b118b07f0c..6c71252bb5 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -32,7 +33,7 @@ protected Secret desired(MySQLSchema schema, Context context) { final var password = RandomStringUtils .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here final var name = schema.getMetadata().getName(); - final var secretName = String.format(SECRET_FORMAT, name); + final var secretName = getSecretName(name); final var userName = String.format(USERNAME_FORMAT, name); return new SecretBuilder() @@ -40,16 +41,19 @@ protected Secret desired(MySQLSchema schema, Context context) { .withName(secretName) .withNamespace(schema.getMetadata().getNamespace()) .endMetadata() - .addToData( - MYSQL_SECRET_USERNAME, encode(userName)) - .addToData( - MYSQL_SECRET_PASSWORD, encode(password)) + .addToData(MYSQL_SECRET_USERNAME, encode(userName)) + .addToData(MYSQL_SECRET_PASSWORD, encode(password)) .build(); } + private String getSecretName(String name) { + return String.format(SECRET_FORMAT, name); + } + @Override - public boolean match(Secret actual, Secret target, Context context) { - return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); + public Result match(Secret actual, MySQLSchema primary, Context context) { + final var desiredSecretName = getSecretName(primary.getMetadata().getName()); + return Result.nonComputed(actual.getMetadata().getName().equals(desiredSecretName)); } @Override From 45fb7e045c3ef5bf4f3bf391d23672e5baa181b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 4 Mar 2022 18:41:55 +0100 Subject: [PATCH 0050/1319] feat: Dependent Resources for External Resources (#991) --- .../dependent/AbstractDependentResource.java | 92 ++++++++++-- .../api/reconciler/dependent/Creator.java | 7 +- .../api/reconciler/dependent/Deleter.java | 5 +- .../dependent/DependentResource.java | 2 +- .../dependent/DesiredEqualsMatcher.java | 19 +++ .../dependent/EventSourceProvider.java | 7 +- .../dependent/RecentOperationCacheFiller.java | 10 ++ .../dependent/RecentOperationEventFilter.java | 11 ++ .../api/reconciler/dependent/Updater.java | 13 +- .../dependent/DependentResourceManager.java | 4 +- .../AbstractCachingDependentResource.java | 34 +++++ .../AbstractSimpleDependentResource.java | 73 ++++++++++ .../PerResourcePollingDependentResource.java | 40 +++++ .../external/PollingDependentResource.java | 36 +++++ .../KubernetesDependentResource.java | 47 ++---- .../processing/event/EventProcessor.java | 5 +- .../ExternalResourceCachingEventSource.java | 54 +++++++ .../event/source/CachingEventSource.java | 74 ++-------- .../event/source/ConcurrentHashMapCache.java | 38 +++++ .../inbound/CachingInboundEventSource.java | 5 +- .../source/informer/InformerEventSource.java | 24 ++- .../informer/ManagedInformerEventSource.java | 13 +- .../PerResourcePollingEventSource.java | 55 +++---- .../source/polling/PollingEventSource.java | 17 ++- .../javaoperatorsdk/operator/TestUtils.java | 4 + .../AbstractSimpleDependentResourceTest.java | 137 ++++++++++++++++++ .../KubernetesDependentResourceTest.java | 77 ---------- .../processing/event/EventProcessorTest.java | 3 +- ...ternalResourceCachingEventSourceTest.java} | 13 +- .../informer/InformerEventSourceTest.java | 38 +++-- .../PerResourcePollingEventSourceTest.java | 18 +-- .../sample/simple/TestCustomResource.java | 1 + ...CreateUpdateEventFilterTestReconciler.java | 17 ++- .../StandaloneDependentTestReconciler.java | 7 +- .../sample/MySQLSchemaReconciler.java | 2 +- .../dependent/SchemaDependentResource.java | 27 +--- ...java => SchemaPollingResourceFetcher.java} | 8 +- .../sample/MySQLSchemaOperatorE2E.java | 2 +- .../operator/sample/TomcatOperatorE2E.java | 4 +- .../WebPageReconcilerDependentResources.java | 11 +- 40 files changed, 720 insertions(+), 334 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/{CachingEventSourceTest.java => ExternalResourceCachingEventSourceTest.java} (81%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/{SchemaPollingResourceSupplier.java => SchemaPollingResourceFetcher.java} (70%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 8700af0805..af727b66eb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public abstract class AbstractDependentResource implements DependentResource { @@ -13,21 +14,25 @@ public abstract class AbstractDependentResource private final boolean creatable = this instanceof Creator; private final boolean updatable = this instanceof Updater; private final boolean deletable = this instanceof Deleter; + private final boolean filteringEventSource; + private final boolean cachingEventSource; protected Creator creator; protected Updater updater; protected Deleter

deleter; @SuppressWarnings("unchecked") public AbstractDependentResource() { - init(Creator.NOOP, Updater.NOOP, Deleter.NOOP); - } - - @SuppressWarnings({"unchecked"}) - protected void init(Creator defaultCreator, Updater defaultUpdater, - Deleter

defaultDeleter) { - creator = creatable ? (Creator) this : defaultCreator; - updater = updatable ? (Updater) this : defaultUpdater; - deleter = deletable ? (Deleter

) this : defaultDeleter; + if (this instanceof EventSourceProvider) { + final var eventSource = ((EventSourceProvider

) this).getEventSource(); + filteringEventSource = eventSource instanceof RecentOperationEventFilter; + cachingEventSource = eventSource instanceof RecentOperationCacheFiller; + } else { + filteringEventSource = false; + cachingEventSource = false; + } + creator = creatable ? (Creator) this : null; + updater = updatable ? (Updater) this : null; + deleter = deletable ? (Deleter

) this : null; } @Override @@ -40,7 +45,7 @@ public void reconcile(P primary, Context context) { if (creatable) { var desired = desired(primary, context); log.debug("Creating dependent {} for primary {}", desired, primary); - creator.create(desired, primary, context); + handleCreate(desired, primary, context); } } else { final var actual = maybeActual.get(); @@ -49,7 +54,7 @@ public void reconcile(P primary, Context context) { if (!match.matched()) { final var desired = match.computedDesired().orElse(desired(primary, context)); log.debug("Updating dependent {} for primary {}", desired, primary); - updater.update(actual, desired, primary, context); + handleUpdate(actual, desired, primary, context); } } else { log.debug("Update skipped for dependent {} as it matched the existing one", actual); @@ -62,8 +67,71 @@ public void reconcile(P primary, Context context) { } } + protected void handleCreate(R desired, P primary, Context context) { + ResourceID resourceID = ResourceID.fromResource(primary); + R created = null; + try { + prepareEventFiltering(desired, resourceID); + created = creator.create(desired, primary, context); + cacheAfterCreate(resourceID, created); + } catch (RuntimeException e) { + cleanupAfterEventFiltering(desired, resourceID, created); + throw e; + } + } + + private void cleanupAfterEventFiltering(R desired, ResourceID resourceID, R created) { + if (filteringEventSource) { + eventSourceAsRecentOperationEventFilter() + .cleanupOnCreateOrUpdateEventFiltering(resourceID, created); + } + } + + private void cacheAfterCreate(ResourceID resourceID, R created) { + if (cachingEventSource) { + eventSourceAsRecentOperationCacheFiller().handleRecentResourceCreate(resourceID, created); + } + } + + private void cacheAfterUpdate(R actual, ResourceID resourceID, R updated) { + if (cachingEventSource) { + eventSourceAsRecentOperationCacheFiller().handleRecentResourceUpdate(resourceID, updated, + actual); + } + } + + private void prepareEventFiltering(R desired, ResourceID resourceID) { + if (filteringEventSource) { + eventSourceAsRecentOperationEventFilter().prepareForCreateOrUpdateEventFiltering(resourceID, + desired); + } + } + + protected void handleUpdate(R actual, R desired, P primary, Context context) { + ResourceID resourceID = ResourceID.fromResource(primary); + R updated = null; + try { + prepareEventFiltering(desired, resourceID); + updated = updater.update(actual, desired, primary, context); + cacheAfterUpdate(actual, resourceID, updated); + } catch (RuntimeException e) { + cleanupAfterEventFiltering(desired, resourceID, updated); + throw e; + } + } + + @SuppressWarnings("unchecked") + private RecentOperationEventFilter eventSourceAsRecentOperationEventFilter() { + return (RecentOperationEventFilter) ((EventSourceProvider

) this).getEventSource(); + } + + @SuppressWarnings("unchecked") + private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() { + return (RecentOperationCacheFiller) ((EventSourceProvider

) this).getEventSource(); + } + @Override - public void delete(P primary, Context context) { + public void cleanup(P primary, Context context) { if (isDeletable(primary, context)) { deleter.delete(primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java index f685a5edb4..223203b2b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java @@ -3,10 +3,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -@SuppressWarnings("rawtypes") +@FunctionalInterface public interface Creator { - Creator NOOP = (desired, primary, context) -> { - }; - - void create(R desired, P primary, Context context); + R create(R desired, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java index 7649460934..b81ffc0380 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -3,10 +3,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -@SuppressWarnings("rawtypes") +@FunctionalInterface public interface Deleter

{ - Deleter NOOP = (primary, context) -> { - }; - void delete(P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 09394f11b0..239ac58d0b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -8,7 +8,7 @@ public interface DependentResource { void reconcile(P primary, Context context); - default void delete(P primary, Context context) {} + default void cleanup(P primary, Context context) {} Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java new file mode 100644 index 0000000000..99470bf1d5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public class DesiredEqualsMatcher implements Matcher { + + private final AbstractDependentResource abstractDependentResource; + + public DesiredEqualsMatcher(AbstractDependentResource abstractDependentResource) { + this.abstractDependentResource = abstractDependentResource; + } + + @Override + public Result match(R actualResource, P primary, Context context) { + var desired = abstractDependentResource.desired(primary, context); + return Result.computed(actualResource.equals(desired), desired); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java index c9a2ca35a2..cc1514fac0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java @@ -5,6 +5,11 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; public interface EventSourceProvider

{ + /** + * @param context - event source context where the event source is initialized + * @return the initiated event source. + */ + EventSource initEventSource(EventSourceContext

context); - EventSource eventSource(EventSourceContext

context); + EventSource getEventSource(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java new file mode 100644 index 0000000000..7e27537d49 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public interface RecentOperationCacheFiller { + + void handleRecentResourceCreate(ResourceID resourceID, R resource); + + void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousResourceVersion); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java new file mode 100644 index 0000000000..5bb9a13f5f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public interface RecentOperationEventFilter extends RecentOperationCacheFiller { + + void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID, R resource); + + void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID, R resource); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java index 62199e1ede..25e0567781 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -4,19 +4,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; -@SuppressWarnings("rawtypes") public interface Updater { - Updater NOOP = new Updater() { - @Override - public void update(Object actual, Object desired, HasMetadata primary, Context context) {} - - @Override - public Result match(Object actualResource, HasMetadata primary, Context context) { - return Result.nonComputed(true); - } - }; - - void update(R actual, R desired, P primary, Context context); + R update(R actual, R desired, P primary, Context context); Result match(R actualResource, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 3f46e12ca5..554f019d81 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -54,7 +54,7 @@ public List prepareEventSources(EventSourceContext

context) { final var dependentResource = createAndConfigureFrom(drc, context.getClient()); if (dependentResource instanceof EventSourceProvider) { EventSourceProvider provider = (EventSourceProvider) dependentResource; - sources.add(provider.eventSource(context)); + sources.add(provider.initEventSource(context)); } return dependentResource; }) @@ -72,7 +72,7 @@ public UpdateControl

reconcile(P resource, Context context) { @Override public DeleteControl cleanup(P resource, Context context) { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.delete(resource, context)); + dependents.forEach(dependent -> dependent.cleanup(resource, context)); return Reconciler.super.cleanup(resource, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java new file mode 100644 index 0000000000..86f2c23d54 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public abstract class AbstractCachingDependentResource + extends AbstractDependentResource implements EventSourceProvider

{ + + protected ExternalResourceCachingEventSource eventSource; + + public Optional fetchResource(P primaryResource) { + return eventSource.getAssociated(primaryResource); + } + + @Override + public EventSource getEventSource() { + return eventSource; + } + + protected Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + } + + @Override + public Optional getResource(P primaryResource) { + return eventSource.getAssociated(primaryResource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java new file mode 100644 index 0000000000..18de3390e0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -0,0 +1,73 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +/** A base class for external dependent resources that don't have an event source. */ +public abstract class AbstractSimpleDependentResource + extends AbstractDependentResource { + + // cache serves only to keep the resource readable again until next reconciliation when the + // new resource is read again. + protected final UpdatableCache cache; + protected Matcher matcher; + + public AbstractSimpleDependentResource() { + this(new ConcurrentHashMapCache<>()); + } + + public AbstractSimpleDependentResource(UpdatableCache cache) { + this.cache = cache; + initMatcher(); + } + + @Override + public Optional getResource(HasMetadata primaryResource) { + return cache.get(ResourceID.fromResource(primaryResource)); + } + + /** Actually read the resource from the target API */ + public abstract Optional fetchResource(HasMetadata primaryResource); + + @Override + public void reconcile(P primary, Context context) { + var resourceId = ResourceID.fromResource(primary); + Optional resource = fetchResource(primary); + resource.ifPresentOrElse(r -> cache.put(resourceId, r), () -> cache.remove(resourceId)); + super.reconcile(primary, context); + } + + public void cleanup(P primary, Context context) { + super.cleanup(primary, context); + cache.remove(ResourceID.fromResource(primary)); + } + + @Override + protected void handleCreate(R desired, P primary, Context context) { + var res = this.creator.create(desired, primary, context); + cache.put(ResourceID.fromResource(primary), res); + } + + @Override + protected void handleUpdate(R actual, R desired, P primary, Context context) { + var res = updater.update(actual, desired, primary, context); + cache.put(ResourceID.fromResource(primary), res); + } + + public Matcher.Result match(R actualResource, P primary, Context context) { + return matcher.match(actualResource, primary, context); + } + + protected void initMatcher() { + matcher = new DesiredEqualsMatcher<>(this); + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java new file mode 100644 index 0000000000..c5f9e2c888 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; + +import static io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource.DEFAULT_POLLING_PERIOD; + +public abstract class PerResourcePollingDependentResource + extends AbstractCachingDependentResource + implements PerResourcePollingEventSource.ResourceFetcher { + + protected long pollingPeriod; + + public PerResourcePollingDependentResource() { + this(DEFAULT_POLLING_PERIOD); + } + + public PerResourcePollingDependentResource(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + eventSource = new PerResourcePollingEventSource<>(this, context.getPrimaryCache(), + pollingPeriod, resourceType()); + return eventSource; + } + + public PerResourcePollingDependentResource setPollingPeriod(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + return this; + } + + public long getPollingPeriod() { + return pollingPeriod; + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java new file mode 100644 index 0000000000..18e0c66b60 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Map; +import java.util.function.Supplier; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PollingEventSource; + +public abstract class PollingDependentResource + extends AbstractCachingDependentResource implements Supplier> { + + public static final int DEFAULT_POLLING_PERIOD = 5000; + protected long pollingPeriod; + + public PollingDependentResource() { + this(DEFAULT_POLLING_PERIOD); + } + + public PollingDependentResource(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + eventSource = new PollingEventSource<>(this, pollingPeriod, resourceType()); + return eventSource; + } + + public PollingDependentResource setPollingPeriod(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + return this; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 04e3c87a54..c94cec73d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -23,7 +23,6 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -46,19 +45,9 @@ public abstract class KubernetesDependentResource() { - @Override - public void update(R actual, R desired, P primary, Context context) { - KubernetesDependentResource.this.update(actual, desired, primary, context); - } - - @Override - public Result match(R actualResource, P primary, Context context) { - return KubernetesDependentResource.this.match(actualResource, primary, context); - } - }, this::delete); matcher = this instanceof Matcher ? (Matcher) this : GenericKubernetesResourceMatcher.matcherFor(resourceType(), this); + processor = this instanceof ResourceUpdatePreProcessor ? (ResourceUpdatePreProcessor) this : GenericResourceUpdatePreProcessor.processorFor(resourceType()); @@ -103,30 +92,13 @@ public void configureWith( this.addOwnerReference = addOwnerReference; } - public void create(R target, P primary, Context context) { - var resourceID = ResourceID.fromResource(target); - try { - informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); - var created = prepare(target, primary, "Creating").create(target); - informerEventSource.handleRecentResourceCreate(created); - } catch (RuntimeException e) { - informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); - throw e; - } + public R create(R target, P primary, Context context) { + return prepare(target, primary, "Creating").create(target); } - public void update(R actual, R target, P primary, Context context) { - var resourceID = ResourceID.fromResource(target); - try { - var updatedActual = processor.replaceSpecOnActual(actual, target, context); - informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); - var updated = prepare(target, primary, "Updating").replace(updatedActual); - informerEventSource.handleRecentResourceUpdate(updated, - actual.getMetadata().getResourceVersion()); - } catch (RuntimeException e) { - informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); - throw e; - } + public R update(R actual, R target, P primary, Context context) { + var updatedActual = processor.replaceSpecOnActual(actual, target, context); + return prepare(target, primary, "Updating").replace(updatedActual); } public Result match(R actualResource, P primary, Context context) { @@ -155,7 +127,7 @@ protected NonNamespaceOperation, Resource> prepa } @Override - public EventSource eventSource(EventSourceContext

context) { + public EventSource initEventSource(EventSourceContext

context) { if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -190,4 +162,9 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { protected R desired(P primary, Context context) { return super.desired(primary, context); } + + @Override + public EventSource getEventSource() { + return informerEventSource; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 81e637a2f6..9ea6df7332 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -208,8 +208,9 @@ void eventProcessingFinished( cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { - eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate(r, - executionScope.getResource().getMetadata().getResourceVersion()); + eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( + ResourceID.fromResource(r), r, + executionScope.getResource()); }); if (eventMarker.eventPresent(resourceID)) { submitReconciliationExecution(resourceID); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java new file mode 100644 index 0000000000..5ffa9b2166 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.processing.event; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; + +public class ExternalResourceCachingEventSource + extends CachingEventSource + implements RecentOperationCacheFiller { + + public ExternalResourceCachingEventSource(Class resourceClass) { + super(resourceClass); + } + + public synchronized void handleDelete(ResourceID relatedResourceID) { + if (!isRunning()) { + return; + } + var cachedValue = cache.get(relatedResourceID); + cache.remove(relatedResourceID); + // we only propagate event if the resource was previously in cache + if (cachedValue.isPresent()) { + getEventHandler().handleEvent(new Event(relatedResourceID)); + } + } + + public synchronized void handleEvent(R value, ResourceID relatedResourceID) { + if (!isRunning()) { + return; + } + var cachedValue = cache.get(relatedResourceID); + if (cachedValue.map(v -> !v.equals(value)).orElse(true)) { + cache.put(relatedResourceID, value); + getEventHandler().handleEvent(new Event(relatedResourceID)); + } + } + + @Override + public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { + if (cache.get(resourceID).isEmpty()) { + cache.put(resourceID, resource); + } + } + + @Override + public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource, + R previousResourceVersion) { + cache.get(resourceID).ifPresent(r -> { + if (r.equals(previousResourceVersion)) { + cache.put(resourceID, resource); + } + }); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index aac04115a3..cfc065331d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -1,13 +1,10 @@ package io.javaoperatorsdk.operator.processing.event.source; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; /** @@ -20,20 +17,20 @@ * an event if the resource is new or not equals to the one in the cache, and if accepted by the * filter if one is present. * - * @param represents the type of resources (usually external non-kubernetes ones) being handled. + * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ -public abstract class CachingEventSource - extends AbstractResourceEventSource implements Cache { +public abstract class CachingEventSource + extends AbstractResourceEventSource implements Cache { - protected UpdatableCache cache; + protected UpdatableCache cache; - protected CachingEventSource(Class resourceClass) { + protected CachingEventSource(Class resourceClass) { super(resourceClass); cache = initCache(); } @Override - public Optional get(ResourceID resourceID) { + public Optional get(ResourceID resourceID) { return cache.get(resourceID); } @@ -48,72 +45,23 @@ public Stream keys() { } @Override - public Stream list(Predicate predicate) { + public Stream list(Predicate predicate) { return cache.list(predicate); } - protected void handleDelete(ResourceID relatedResourceID) { - if (!isRunning()) { - return; - } - var cachedValue = cache.get(relatedResourceID); - cache.remove(relatedResourceID); - // we only propagate event if the resource was previously in cache - if (cachedValue.isPresent()) { - getEventHandler().handleEvent(new Event(relatedResourceID)); - } - } - protected void handleEvent(T value, ResourceID relatedResourceID) { - if (!isRunning()) { - return; - } - var cachedValue = cache.get(relatedResourceID); - if (cachedValue.map(v -> !v.equals(value)).orElse(true)) { - cache.put(relatedResourceID, value); - getEventHandler().handleEvent(new Event(relatedResourceID)); - } - } - protected UpdatableCache initCache() { - return new MapCache<>(); + protected UpdatableCache initCache() { + return new ConcurrentHashMapCache<>(); } - public Optional getCachedValue(ResourceID resourceID) { + public Optional getCachedValue(ResourceID resourceID) { return cache.get(resourceID); } @Override - public Optional getAssociated(P primary) { + public Optional getAssociated(P primary) { return cache.get(ResourceID.fromResource(primary)); } - protected static class MapCache implements UpdatableCache { - private final Map cache = new ConcurrentHashMap<>(); - - @Override - public Optional get(ResourceID resourceID) { - return Optional.ofNullable(cache.get(resourceID)); - } - - @Override - public Stream keys() { - return cache.keySet().stream(); - } - - @Override - public Stream list(Predicate predicate) { - return cache.values().stream().filter(predicate); - } - - @Override - public T remove(ResourceID key) { - return cache.remove(key); - } - - @Override - public void put(ResourceID key, T resource) { - cache.put(key, resource); - } - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java new file mode 100644 index 0000000000..76dc615fcb --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class ConcurrentHashMapCache implements UpdatableCache { + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); + } + + @Override + public Stream keys() { + return cache.keySet().stream(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.values().stream().filter(predicate); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(key); + } + + @Override + public void put(ResourceID key, T resource) { + cache.put(key, resource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java index 1d60da2a3a..34f37a048b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -1,10 +1,11 @@ package io.javaoperatorsdk.operator.processing.event.source.inbound; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; -public class CachingInboundEventSource extends CachingEventSource { +public class CachingInboundEventSource + extends ExternalResourceCachingEventSource { public CachingInboundEventSource(Class resourceClass) { super(resourceClass); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 006564426e..4a23bd9363 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationEventFilter; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -63,7 +64,7 @@ */ public class InformerEventSource extends ManagedInformerEventSource> - implements ResourceCache, ResourceEventHandler { + implements ResourceCache, ResourceEventHandler, RecentOperationEventFilter { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); @@ -160,14 +161,17 @@ public InformerConfiguration getConfiguration() { } @Override - public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { + public void handleRecentResourceUpdate(ResourceID resourceID, R resource, + R previousResourceVersion) { handleRecentCreateOrUpdate(resource, - () -> super.handleRecentResourceUpdate(resource, previousResourceVersion)); + () -> super.handleRecentResourceUpdate(resourceID, resource, + previousResourceVersion)); } @Override - public void handleRecentResourceCreate(R resource) { - handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceCreate(resource)); + public void handleRecentResourceCreate(ResourceID resourceID, R resource) { + handleRecentCreateOrUpdate(resource, + () -> super.handleRecentResourceCreate(resourceID, resource)); } private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnable) { @@ -216,7 +220,9 @@ private synchronized void handleRecentResourceOperationAndStopEventRecording(R r } } - public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID) { + @Override + public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID, + R resource) { log.info("Starting event recording for: {}", resourceID); eventRecorder.startEventRecording(resourceID); } @@ -225,9 +231,11 @@ public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resou * Mean to be called to clean up in case of an exception from the client. Usually in a catch * block. * - * @param resourceID of the resource + * @param resource handled by the informer */ - public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) { + @Override + public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID, + R resource) { log.info("Stopping event recording for: {}", resourceID); eventRecorder.stopEventRecording(resourceID); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 47de996914..063207b0e7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; @@ -20,7 +21,7 @@ public abstract class ManagedInformerEventSource> extends CachingEventSource - implements ResourceEventHandler, ResourceCache { + implements ResourceEventHandler, ResourceCache, RecentOperationCacheFiller { private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); @@ -68,11 +69,15 @@ public void stop() { manager().stop(); } - public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { - temporaryResourceCache.putUpdatedResource(resource, previousResourceVersion); + @Override + public void handleRecentResourceUpdate(ResourceID resourceID, R resource, + R previousResourceVersion) { + temporaryResourceCache.putUpdatedResource(resource, + previousResourceVersion.getMetadata().getResourceVersion()); } - public void handleRecentResourceCreate(R resource) { + @Override + public void handleRecentResourceCreate(ResourceID resourceID, R resource) { temporaryResourceCache.putAddedResource(resource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java index 9130d27a45..74cf420cd5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; @@ -25,39 +26,39 @@ *

* For other behavior see {@link CachingEventSource} * - * @param the resource polled by the event source - * @param related custom resource + * @param the resource polled by the event source + * @param

related custom resource */ -public class PerResourcePollingEventSource - extends CachingEventSource - implements ResourceEventAware { +public class PerResourcePollingEventSource + extends ExternalResourceCachingEventSource + implements ResourceEventAware

{ private static final Logger log = LoggerFactory.getLogger(PerResourcePollingEventSource.class); private final Timer timer = new Timer(); private final Map timerTasks = new ConcurrentHashMap<>(); - private final ResourceSupplier resourceSupplier; - private final Cache resourceCache; - private final Predicate registerPredicate; + private final ResourceFetcher resourceFetcher; + private final Cache

resourceCache; + private final Predicate

registerPredicate; private final long period; - public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - Cache resourceCache, long period, Class resourceClass) { - this(resourceSupplier, resourceCache, period, null, resourceClass); + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, + Cache

resourceCache, long period, Class resourceClass) { + this(resourceFetcher, resourceCache, period, null, resourceClass); } - public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - Cache resourceCache, long period, - Predicate registerPredicate, Class resourceClass) { + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, + Cache

resourceCache, long period, + Predicate

registerPredicate, Class resourceClass) { super(resourceClass); - this.resourceSupplier = resourceSupplier; + this.resourceFetcher = resourceFetcher; this.resourceCache = resourceCache; this.period = period; this.registerPredicate = registerPredicate; } - private void pollForResource(R resource) { - var value = resourceSupplier.getResource(resource); + private void pollForResource(P resource) { + var value = resourceFetcher.fetchResource(resource); var resourceID = ResourceID.fromResource(resource); if (value.isEmpty()) { super.handleDelete(resourceID); @@ -66,10 +67,10 @@ private void pollForResource(R resource) { } } - private Optional getAndCacheResource(ResourceID resourceID) { + private Optional getAndCacheResource(ResourceID resourceID) { var resource = resourceCache.get(resourceID); if (resource.isPresent()) { - var value = resourceSupplier.getResource(resource.get()); + var value = resourceFetcher.fetchResource(resource.get()); value.ifPresent(v -> cache.put(resourceID, v)); return value; } @@ -77,17 +78,17 @@ private Optional getAndCacheResource(ResourceID resourceID) { } @Override - public void onResourceCreated(R resource) { + public void onResourceCreated(P resource) { checkAndRegisterTask(resource); } @Override - public void onResourceUpdated(R newResource, R oldResource) { + public void onResourceUpdated(P newResource, P oldResource) { checkAndRegisterTask(newResource); } @Override - public void onResourceDeleted(R resource) { + public void onResourceDeleted(P resource) { var resourceID = ResourceID.fromResource(resource); TimerTask task = timerTasks.remove(resourceID); if (task != null) { @@ -101,7 +102,7 @@ public void onResourceDeleted(R resource) { // since events from ResourceEventAware are propagated from the thread of the informer. This is // important // because otherwise there will be a race condition related to the timerTasks. - private void checkAndRegisterTask(R resource) { + private void checkAndRegisterTask(P resource) { var resourceID = ResourceID.fromResource(resource); if (timerTasks.get(resourceID) == null && (registerPredicate == null || registerPredicate.test(resource))) { @@ -131,7 +132,7 @@ public void run() { * @return the related resource for this event source */ @Override - public Optional getAssociated(R primary) { + public Optional getAssociated(P primary) { return getValueFromCacheOrSupplier(ResourceID.fromResource(primary)); } @@ -142,7 +143,7 @@ public Optional getAssociated(R primary) { * supplier. The value provided from the supplier is cached, but no new event is * propagated. */ - public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { + public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { var cachedValue = getCachedValue(resourceID); if (cachedValue.isPresent()) { return cachedValue; @@ -151,8 +152,8 @@ public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { } } - public interface ResourceSupplier { - Optional getResource(R resource); + public interface ResourceFetcher { + Optional fetchResource(P primaryResource); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index f9a4265c04..8a09b71658 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -8,8 +8,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; /** * Polls resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but @@ -34,19 +34,20 @@ * cache. * * - * @param type of the polled resource + * @param type of the polled resource * @param

primary resource type */ -public class PollingEventSource extends CachingEventSource { +public class PollingEventSource + extends ExternalResourceCachingEventSource { private static final Logger log = LoggerFactory.getLogger(PollingEventSource.class); private final Timer timer = new Timer(); - private final Supplier> supplierToPoll; + private final Supplier> supplierToPoll; private final long period; - public PollingEventSource( - Supplier> supplier, long period, Class resourceClass) { + public PollingEventSource(Supplier> supplier, + long period, Class resourceClass) { super(resourceClass); this.supplierToPoll = supplier; this.period = period; @@ -77,7 +78,7 @@ protected void getStateAndFillCache() { cache.keys().filter(e -> !values.containsKey(e)).forEach(super::handleDelete); } - public void put(ResourceID key, T resource) { + public void put(ResourceID key, R resource) { cache.put(key, resource); } @@ -94,7 +95,7 @@ public void stop() throws OperatorException { * @return related resource */ @Override - public Optional getAssociated(P primary) { + public Optional getAssociated(P primary) { return getCachedValue(ResourceID.fromResource(primary)); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index 137ed2d11d..e8361c49cf 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -27,6 +27,10 @@ public static CustomResourceDefinition testCRD(String scope) { .build(); } + public static TestCustomResource testCustomResource1() { + return testCustomResource(new ResourceID("test1", "default")); + } + public static TestCustomResource testCustomResource(ResourceID id) { TestCustomResource resource = new TestCustomResource(); resource.setMetadata( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java new file mode 100644 index 0000000000..00557e7e61 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java @@ -0,0 +1,137 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Optional; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unchecked") +class AbstractSimpleDependentResourceTest { + + UpdatableCache updatableCacheMock = mock(UpdatableCache.class); + Supplier supplierMock = mock(Supplier.class); + + SimpleDependentResource simpleDependentResource = + new SimpleDependentResource(updatableCacheMock, supplierMock); + + @BeforeEach + void setup() { + when(supplierMock.get()).thenReturn(SampleExternalResource.testResource1()); + } + + @Test + void getsTheResourceFromSupplyIfReconciling() { + simpleDependentResource = new SimpleDependentResource(supplierMock); + + simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); + + verify(supplierMock, times(1)).get(); + assertThat(simpleDependentResource.getResource(TestUtils.testCustomResource1())) + .isPresent() + .isEqualTo(Optional.of(SampleExternalResource.testResource1())); + } + + @Test + void getResourceReadsTheResourceFromCache() { + simpleDependentResource.getResource(TestUtils.testCustomResource1()); + + verify(supplierMock, times(0)).get(); + verify(updatableCacheMock, times(1)).get(any()); + } + + @Test + void createPutsNewResourceToTheCache() { + when(supplierMock.get()).thenReturn(null); + when(updatableCacheMock.get(any())).thenReturn(Optional.empty()); + + simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); + + verify(updatableCacheMock, times(1)).put(any(), any()); + } + + @Test + void updatePutsNewResourceToCache() { + var actual = SampleExternalResource.testResource1(); + actual.setValue("changedValue"); + when(supplierMock.get()).thenReturn(actual); + when(updatableCacheMock.get(any())).thenReturn(Optional.of(actual)); + + simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); + + verify(updatableCacheMock, times(1)) + .put(ResourceID.fromResource(TestUtils.testCustomResource1()), actual); + + verify(updatableCacheMock, times(1)) + .put( + ResourceID.fromResource(TestUtils.testCustomResource1()), + SampleExternalResource.testResource1()); + } + + @Test + void deleteRemovesResourceFromCache() { + simpleDependentResource.cleanup(TestUtils.testCustomResource1(), null); + verify(updatableCacheMock, times(1)).remove(any()); + } + + private static class SimpleDependentResource + extends AbstractSimpleDependentResource + implements Creator, + Updater, + Deleter { + + private final Supplier supplier; + + public SimpleDependentResource(Supplier supplier) { + this.supplier = supplier; + } + + public SimpleDependentResource( + UpdatableCache cache, Supplier supplier) { + super(cache); + this.supplier = supplier; + } + + @Override + public Optional fetchResource(HasMetadata primaryResource) { + return Optional.ofNullable(supplier.get()); + } + + @Override + public SampleExternalResource create( + SampleExternalResource desired, TestCustomResource primary, Context context) { + return SampleExternalResource.testResource1(); + } + + @Override + public SampleExternalResource update( + SampleExternalResource actual, + SampleExternalResource desired, + TestCustomResource primary, + Context context) { + return SampleExternalResource.testResource1(); + } + + @Override + public void delete(TestCustomResource primary, Context context) {} + + @Override + protected SampleExternalResource desired(TestCustomResource primary, Context context) { + return SampleExternalResource.testResource1(); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java deleted file mode 100644 index e73244f7c6..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import static org.mockito.Mockito.*; - -class KubernetesDependentResourceTest { - - // private InformerEventSource informerEventSourceMock = mock(InformerEventSource.class); - // private AssociatedSecondaryResourceIdentifier associatedResourceIdentifierMock = - // mock(AssociatedSecondaryResourceIdentifier.class); - // private ResourceMatcher resourceMatcherMock = mock(ResourceMatcher.class); - // private KubernetesDependentResource.ClientFacade clientFacadeMock = - // mock(KubernetesDependentResource.ClientFacade.class); - // - // KubernetesDependentResource kubernetesDependentResource = - // new KubernetesDependentResource() { - // { - // this.informerEventSource = informerEventSourceMock; - // this.resourceMatcher = resourceMatcherMock; - // this.clientFacade = clientFacadeMock; - // this.resourceUpdatePreProcessor = mock(ResourceUpdatePreProcessor.class); - // } - // - // @Override - // protected Object desired(HasMetadata primary, Context context) { - // return testResource(); - // } - // }; - // - // @BeforeEach - // public void setup() { - // InformerConfiguration informerConfigurationMock = mock(InformerConfiguration.class); - // when(informerEventSourceMock.getConfiguration()).thenReturn(informerConfigurationMock); - // when(informerConfigurationMock.getAssociatedResourceIdentifier()) - // .thenReturn(associatedResourceIdentifierMock); - // when(associatedResourceIdentifierMock.associatedSecondaryID(any())) - // .thenReturn(ResourceID.fromResource(testResource())); - // } - // - // @Test - // void updateCallsInformerJustUpdatedHandler() { - // when(resourceMatcherMock.match(any(), any(), any())).thenReturn(false); - // when(clientFacadeMock.replaceResource(any(), any(), any())).thenReturn(testResource()); - // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.of(testResource())); - // - // kubernetesDependentResource.reconcile(primaryResource(), null); - // - // verify(informerEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); - // } - // - // @Test - // void createCallsInformerJustUpdatedHandler() { - // when(clientFacadeMock.createResource(any(), any(), any())).thenReturn(testResource()); - // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.empty()); - // - // kubernetesDependentResource.reconcile(primaryResource(), null); - // - // verify(informerEventSourceMock, times(1)).handleRecentResourceAdd(any()); - // } - // - // TestCustomResource primaryResource() { - // TestCustomResource testCustomResource = new TestCustomResource(); - // testCustomResource.setMetadata(new ObjectMeta()); - // testCustomResource.getMetadata().setName("test"); - // testCustomResource.getMetadata().setNamespace("default"); - // return testCustomResource; - // } - // - // ConfigMap testResource() { - // ConfigMap configMap = new ConfigMap(); - // configMap.setMetadata(new ObjectMeta()); - // configMap.getMetadata().setName("test"); - // configMap.getMetadata().setNamespace("default"); - // configMap.getMetadata().setResourceVersion("0"); - // return configMap; - // } - -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index c02a3b0da1..a716a49898 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -247,7 +247,8 @@ void updatesEventSourceHandlerIfResourceUpdated() { eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); - verify(controllerResourceEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); + verify(controllerResourceEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any(), + any()); } private ResourceID eventAlreadyUnderProcessing() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java similarity index 81% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java index 29cf0a981d..9ed4e25342 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java @@ -6,17 +6,18 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -class CachingEventSourceTest extends - AbstractEventSourceTestBase, EventHandler> { +class ExternalResourceCachingEventSourceTest extends + AbstractEventSourceTestBase, EventHandler> { @BeforeEach public void setup() { - setUpSource(new SimpleCachingEventSource()); + setUpSource(new SimpleExternalCachingEventSource()); } @Test @@ -65,9 +66,9 @@ public void noEventOnDeleteIfResourceWasNotInCacheBefore() { } - public static class SimpleCachingEventSource - extends CachingEventSource { - public SimpleCachingEventSource() { + public static class SimpleExternalCachingEventSource + extends ExternalResourceCachingEventSource { + public SimpleExternalCachingEventSource() { super(SampleExternalResource.class); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 1c901034e0..4caf9b70d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -96,9 +96,11 @@ void notPropagatesEventIfAfterUpdateReceivedJustTheRelatedEvent() { informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevTestDeployment, testDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -110,9 +112,11 @@ void notPropagatesEventIfAfterCreateReceivedJustTheRelatedEvent() { var testDeployment = testDeployment(); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onAdd(testDeployment); - informerEventSource.handleRecentResourceCreate(testDeployment); + informerEventSource.handleRecentResourceCreate(ResourceID.fromResource(testDeployment), + testDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -127,10 +131,12 @@ void propagatesEventIfNewEventReceivedAfterTheCurrentTargetEvent() { nextTestDeployment.getMetadata().setResourceVersion(NEXT_RESOURCE_VERSION); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevTestDeployment, testDeployment); informerEventSource.onUpdate(testDeployment, nextTestDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(1)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -145,10 +151,12 @@ void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() { prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); informerEventSource.onUpdate(prevTestDeployment, testDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -157,10 +165,14 @@ void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() { @Test void putsResourceOnTempCacheIfNoEventRecorded() { var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); @@ -175,9 +187,11 @@ void putsResourceOnTempCacheIfNoEventRecordedWithSameResourceVersion() { prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index 8486f705ca..eb8d80c0c1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -27,15 +27,15 @@ class PerResourcePollingEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { public static final int PERIOD = 80; - private PerResourcePollingEventSource.ResourceSupplier supplier = - mock(PerResourcePollingEventSource.ResourceSupplier.class); + private PerResourcePollingEventSource.ResourceFetcher supplier = + mock(PerResourcePollingEventSource.ResourceFetcher.class); private Cache resourceCache = mock(Cache.class); private TestCustomResource testCustomResource = TestUtils.testCustomResource(); @BeforeEach public void setup() { when(resourceCache.get(any())).thenReturn(Optional.of(testCustomResource)); - when(supplier.getResource(any())) + when(supplier.fetchResource(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())); setUpSource(new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, @@ -47,7 +47,7 @@ public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { source.onResourceCreated(testCustomResource); Thread.sleep(3 * PERIOD); - verify(supplier, atLeast(2)).getResource(eq(testCustomResource)); + verify(supplier, atLeast(2)).fetchResource(eq(testCustomResource)); verify(eventHandler, times(1)).handleEvent(any()); } @@ -59,31 +59,31 @@ public void registeringTaskOnAPredicate() throws InterruptedException { source.onResourceCreated(testCustomResource); Thread.sleep(2 * PERIOD); - verify(supplier, times(0)).getResource(eq(testCustomResource)); + verify(supplier, times(0)).fetchResource(eq(testCustomResource)); testCustomResource.getMetadata().setGeneration(2L); source.onResourceUpdated(testCustomResource, testCustomResource); Thread.sleep(2 * PERIOD); - verify(supplier, atLeast(1)).getResource(eq(testCustomResource)); + verify(supplier, atLeast(1)).fetchResource(eq(testCustomResource)); } @Test public void propagateEventOnDeletedResource() throws InterruptedException { source.onResourceCreated(testCustomResource); - when(supplier.getResource(any())) + when(supplier.fetchResource(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())) .thenReturn(Optional.empty()); Thread.sleep(3 * PERIOD); - verify(supplier, atLeast(2)).getResource(eq(testCustomResource)); + verify(supplier, atLeast(2)).fetchResource(eq(testCustomResource)); verify(eventHandler, times(2)).handleEvent(any()); } @Test public void getsValueFromCacheOrSupplier() throws InterruptedException { source.onResourceCreated(testCustomResource); - when(supplier.getResource(any())) + when(supplier.fetchResource(any())) .thenReturn(Optional.empty()) .thenReturn(Optional.of(SampleExternalResource.testResource1())); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java index d01bd3c747..59d9d4fb91 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java @@ -18,4 +18,5 @@ protected TestCustomResourceSpec initSpec() { protected TestCustomResourceStatus initStatus() { return new TestCustomResourceStatus(); } + } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index ae41eacb68..015f7a18bd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -41,37 +41,38 @@ public UpdateControl reconcile( .get(); if (configMap == null) { var configMapToCreate = createConfigMap(resource); + final var resourceID = ResourceID.fromResource(configMapToCreate); try { - informerEventSource.prepareForCreateOrUpdateEventFiltering( - ResourceID.fromResource(configMapToCreate)); + informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID, configMapToCreate); configMap = client .configMaps() .inNamespace(resource.getMetadata().getNamespace()) .create(configMapToCreate); - informerEventSource.handleRecentResourceCreate(configMap); + informerEventSource.handleRecentResourceCreate(resourceID, configMap); } catch (RuntimeException e) { informerEventSource - .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMapToCreate)); + .cleanupOnCreateOrUpdateEventFiltering(resourceID, configMapToCreate); throw e; } } else { + ResourceID resourceID = ResourceID.fromResource(configMap); if (!Objects.equals( configMap.getData().get(CONFIG_MAP_TEST_DATA_KEY), resource.getSpec().getValue())) { configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue()); try { informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + .prepareForCreateOrUpdateEventFiltering(resourceID, configMap); var newConfigMap = client .configMaps() .inNamespace(resource.getMetadata().getNamespace()) .replace(configMap); - informerEventSource.handleRecentResourceUpdate( - newConfigMap, configMap.getMetadata().getResourceVersion()); + informerEventSource.handleRecentResourceUpdate(resourceID, + newConfigMap, configMap); } catch (RuntimeException e) { informerEventSource - .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + .cleanupOnCreateOrUpdateEventFiltering(resourceID, configMap); throw e; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index b6efb0f579..12b7f9506e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; @@ -33,7 +34,7 @@ public StandaloneDependentTestReconciler() { @Override public List prepareEventSources( EventSourceContext context) { - return List.of(deploymentDependent.eventSource(context)); + return List.of(deploymentDependent.initEventSource(context)); } @Override @@ -66,6 +67,10 @@ public KubernetesClient getKubernetesClient() { @Override public Optional updateErrorStatus( StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + // this can happen when a namespace is terminated in test + if (e instanceof KubernetesClientException) { + return Optional.empty(); + } errorOccurred = true; return Optional.empty(); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index a6923823aa..140047a3b4 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -41,7 +41,7 @@ public UpdateControl reconcile(MySQLSchema schema, Context context) Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); SchemaDependentResource schemaDependentResource = context.managedDependentResourceContext() .getDependentResource(SchemaDependentResource.class); - return schemaDependentResource.getResource(schema).map(s -> { + return schemaDependentResource.fetchResource(schema).map(s -> { updateStatusPojo(schema, secret.getMetadata().getName(), secret.getData().get(MYSQL_SECRET_USERNAME)); log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index 0a97684e0d..f1fbc4b642 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -11,14 +11,11 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.*; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -28,7 +25,7 @@ import static java.lang.String.format; public class SchemaDependentResource - extends AbstractDependentResource + extends PerResourcePollingDependentResource implements EventSourceProvider, DependentResourceConfigurator, Creator, @@ -37,22 +34,11 @@ public class SchemaDependentResource private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); private MySQLDbConfig dbConfig; - private int pollPeriod = 500; @Override public void configureWith(ResourcePollerConfig config) { this.dbConfig = config.getMySQLDbConfig(); - this.pollPeriod = config.getPollPeriod(); - } - - @Override - public EventSource eventSource(EventSourceContext context) { - if (dbConfig == null) { - dbConfig = MySQLDbConfig.loadFromEnvironmentVars(); - } - return new PerResourcePollingEventSource<>( - new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, - Schema.class); + setPollingPeriod(config.getPollPeriod()); } @Override @@ -61,12 +47,12 @@ public Schema desired(MySQLSchema primary, Context context) { } @Override - public void create(Schema target, MySQLSchema mySQLSchema, Context context) { + public Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME)); var password = decode(secret.getData().get(MYSQL_SECRET_PASSWORD)); - final var schema = SchemaService.createSchemaAndRelatedUser( + return SchemaService.createSchemaAndRelatedUser( connection, target.getName(), target.getCharacterSet(), username, password); @@ -94,9 +80,8 @@ public void delete(MySQLSchema primary, Context context) { } } - // todo this should read the resource from event source? @Override - public Optional getResource(MySQLSchema primaryResource) { + public Optional fetchResource(MySQLSchema primaryResource) { try (Connection connection = getConnection()) { var schema = SchemaService.getSchema(connection, primaryResource.getMetadata().getName()).orElse(null); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java similarity index 70% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java index acf620e819..2830a4b08b 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java @@ -8,17 +8,17 @@ import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; -public class SchemaPollingResourceSupplier - implements PerResourcePollingEventSource.ResourceSupplier { +public class SchemaPollingResourceFetcher + implements PerResourcePollingEventSource.ResourceFetcher { private final SchemaService schemaService; - public SchemaPollingResourceSupplier(MySQLDbConfig mySQLDbConfig) { + public SchemaPollingResourceFetcher(MySQLDbConfig mySQLDbConfig) { this.schemaService = new SchemaService(mySQLDbConfig); } @Override - public Optional getResource(MySQLSchema resource) { + public Optional fetchResource(MySQLSchema resource) { return schemaService.getSchema(resource.getMetadata().getName()); } } diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index 8f9302a81a..ed0ff40489 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -83,7 +83,7 @@ boolean isLocal() { public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @Test - public void test() throws IOException { + void test() throws IOException { // Opening a port-forward if running locally LocalPortForward portForward = null; if (isLocal()) { diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index 60ef0bc187..dabcbdfa4b 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -23,7 +23,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; -public class TomcatOperatorE2E { +class TomcatOperatorE2E { final static Logger log = LoggerFactory.getLogger(TomcatOperatorE2E.class); @@ -80,7 +80,7 @@ Webapp getWebapp() { } @Test - public void test() { + void test() { var tomcat = getTomcat(); var webapp1 = getWebapp(); var tomcatClient = client.resources(Tomcat.class); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index cb5144a8d3..b4f6db1395 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -45,9 +45,9 @@ public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { return List.of( - configMapDR.eventSource(context), - deploymentDR.eventSource(context), - serviceDR.eventSource(context)); + configMapDR.initEventSource(context), + deploymentDR.initEventSource(context), + serviceDR.initEventSource(context)); } @Override @@ -180,8 +180,8 @@ protected ConfigMap desired(WebPage webPage, Context context) { } @Override - public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - super.update(actual, target, primary, context); + public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + var res = super.update(actual, target, primary, context); var ns = actual.getMetadata().getNamespace(); log.info("Restarting pods because HTML has changed in {}", ns); kubernetesClient @@ -189,6 +189,7 @@ public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context .inNamespace(ns) .withLabel("app", deploymentName(primary)) .delete(); + return res; } @Override From 2db40f0b71fbcb1b2c9a5555534cf1d7a7342386 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 09:10:47 +0100 Subject: [PATCH 0051/1319] chore(deps): bump awaitility from 4.1.1 to 4.2.0 (#999) --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 5412cfac2a..aebabe2416 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 0.19 1.13.0 3.22.0 - 4.1.1 + 4.2.0 2.6.4 1.8.3 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 7d25dc2f5f..5f96f8af9a 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -64,7 +64,7 @@ org.awaitility awaitility - 4.1.0 + 4.2.0 test diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 4381948214..7b41dfb4b2 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -54,7 +54,7 @@ org.awaitility awaitility - 4.1.1 + 4.2.0 test From 496adeb4a626ef151e90ef3f1891e1883298608c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 09:11:03 +0100 Subject: [PATCH 0052/1319] chore(deps): bump jackson-dataformat-yaml from 2.13.1 to 2.13.2 (#998) Bumps [jackson-dataformat-yaml](https://github.com/FasterXML/jackson-dataformats-text) from 2.13.1 to 2.13.2. - [Release notes](https://github.com/FasterXML/jackson-dataformats-text/releases) - [Commits](https://github.com/FasterXML/jackson-dataformats-text/compare/jackson-dataformats-text-2.13.1...jackson-dataformats-text-2.13.2) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/mysql-schema/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 5f96f8af9a..6cf30996a6 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -70,7 +70,7 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.13.1 + 2.13.2 io.javaoperatorsdk From 297b0848391e24be5eca9fc6a576d46f4293eede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 7 Mar 2022 11:14:49 +0100 Subject: [PATCH 0053/1319] fix: javadoc for fails snapshot release (#1000) --- .../external/AbstractSimpleDependentResource.java | 7 ++++++- .../event/ExternalResourceCachingEventSource.java | 3 +-- .../processing/event/source/CachingEventSource.java | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 18de3390e0..564646b217 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -34,7 +34,12 @@ public Optional getResource(HasMetadata primaryResource) { return cache.get(ResourceID.fromResource(primaryResource)); } - /** Actually read the resource from the target API */ + /** + * Actually read the resource from the target API + * + * @param primaryResource the primary associated resource + * @return fetched resource if present + **/ public abstract Optional fetchResource(HasMetadata primaryResource); @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java index 5ffa9b2166..f69b33dd43 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java @@ -5,8 +5,7 @@ import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; public class ExternalResourceCachingEventSource - extends CachingEventSource - implements RecentOperationCacheFiller { + extends CachingEventSource implements RecentOperationCacheFiller { public ExternalResourceCachingEventSource(Class resourceClass) { super(resourceClass); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index cfc065331d..57d94d8922 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -10,12 +10,6 @@ /** * Base class for event sources with caching capabilities. *

- * {@link #handleDelete(ResourceID)} - if the related resource is present in the cache it is removed - * and event propagated. There is no event propagated if the resource is not in the cache. - *

- * {@link #handleEvent(Object, ResourceID)} - caches the resource if changed or missing. Propagates - * an event if the resource is new or not equals to the one in the cache, and if accepted by the - * filter if one is present. * * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ From 95ac79f75e3d99850b6cfd56d7b64de9a0eeaa74 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 7 Mar 2022 17:27:21 +0100 Subject: [PATCH 0054/1319] fix: state controller name in exception (#1002) Fixes #1001 --- .../operator/processing/event/EventSourceManager.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 6e0b218d55..fda64660b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -129,12 +129,14 @@ public final void registerEventSource(EventSource eventSource) } catch (IllegalStateException | MissingCRDException e) { throw e; // leave untouched } catch (Exception e) { - throw new OperatorException("Couldn't register event source: " + eventSource.name(), e); + throw new OperatorException("Couldn't register event source: " + eventSource.name() + " for " + + controller.getConfiguration().getName() + " controller`", e); } finally { lock.unlock(); } } + @SuppressWarnings("unchecked") public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldResource) { for (var eventSource : eventSources) { if (eventSource instanceof ResourceEventAware) { @@ -211,8 +213,8 @@ public Iterator iterator() { } public Set all() { - return new LinkedHashSet<>(sources.values().stream().flatMap(Collection::stream) - .collect(Collectors.toList())); + return sources.values().stream().flatMap(Collection::stream) + .collect(Collectors.toCollection(LinkedHashSet::new)); } public void clear() { @@ -236,6 +238,7 @@ public void add(EventSource eventSource) { sources.computeIfAbsent(keyFor(eventSource), k -> new ArrayList<>()).add(eventSource); } + @SuppressWarnings("rawtypes") private Class getDependentType(EventSource source) { return source instanceof ResourceEventSource ? ((ResourceEventSource) source).getResourceClass() @@ -265,6 +268,7 @@ private String keyFor(Class dependentType) { return key; } + @SuppressWarnings("unchecked") public ResourceEventSource get(Class dependentType, String name) { final var sourcesForType = sources.get(keyFor(dependentType)); if (sourcesForType == null || sourcesForType.isEmpty()) { From 551ea02dfd9ce4d6116c5cc16cbdd107a8e4bea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 8 Mar 2022 21:19:13 +0100 Subject: [PATCH 0055/1319] fix: added logging (#1005) --- .../operator/processing/event/EventProcessor.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 9ea6df7332..7f4cae69a4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -168,8 +168,10 @@ private void submitReconciliationExecution(ResourceID resourceID) { private void handleEventMarking(Event event) { if (event instanceof ResourceEvent && ((ResourceEvent) event).getAction() == ResourceAction.DELETED) { + log.debug("Marking delete event received for: {}", event.getRelatedCustomResourceID()); eventMarker.markDeleteEventReceived(event); } else if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { + log.debug("Marking event received for: {}", event.getRelatedCustomResourceID()); eventMarker.markEventReceived(event); } } @@ -227,7 +229,13 @@ private void reScheduleExecutionIfInstructed( PostExecutionControl postExecutionControl, R customResource) { postExecutionControl .getReScheduleDelay() - .ifPresent(delay -> retryEventSource().scheduleOnce(customResource, delay)); + .ifPresent(delay -> { + if (log.isDebugEnabled()) { + log.debug("ReScheduling event for resource: {} with delay: {}", + ResourceID.fromResource(customResource), delay); + } + retryEventSource().scheduleOnce(customResource, delay); + }); } TimerEventSource retryEventSource() { @@ -284,6 +292,7 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) } private void cleanupForDeletedEvent(ResourceID customResourceUid) { + log.debug("Cleaning up for delete event for: {}", customResourceUid); eventMarker.cleanup(customResourceUid); metrics.cleanupDoneFor(customResourceUid); } From bf075d75616457e43c1d764ce92b46b1f8255fb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 11:28:53 +0100 Subject: [PATCH 0056/1319] chore(deps-dev): bump mockito-core from 4.3.1 to 4.4.0 (#1007) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.3.1 to 4.4.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.3.1...v4.4.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aebabe2416..dee39368d9 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 5.12.0 1.7.36 2.17.2 - 4.3.1 + 4.4.0 3.12.0 1.0.1 0.19 From 1c6b283183306219ce17dd4ddf8010ddbb9b017d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 9 Mar 2022 21:09:41 +0100 Subject: [PATCH 0057/1319] fix: making sure there is not race condition with temporal cache (#1009) --- .../ControllerResourceEventSource.java | 6 +- .../event/source/informer/EventRecorder.java | 9 +- .../source/informer/InformerEventSource.java | 9 +- .../informer/TemporaryResourceCache.java | 83 +++++++------------ 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 801eae09ee..464cc3e500 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -48,10 +48,8 @@ public ControllerResourceEventSource(Controller controller) { public void start() { try { super.start(); - } catch (Exception e) { - if (e instanceof KubernetesClientException) { - handleKubernetesClientException(e); - } + } catch (KubernetesClientException e) { + handleKubernetesClientException(e); throw e; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java index 284b749f07..5d23d870aa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java @@ -1,18 +1,18 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; public class EventRecorder { - private final Map> resourceEvents = new ConcurrentHashMap<>(); + private final Map> resourceEvents = new HashMap<>(); - void startEventRecording(ResourceID resourceID) { + public void startEventRecording(ResourceID resourceID) { resourceEvents.putIfAbsent(resourceID, new ArrayList<>(5)); } @@ -28,7 +28,8 @@ public void recordEvent(R resource) { resourceEvents.get(ResourceID.fromResource(resource)).add(resource); } - public boolean containsEventWithResourceVersion(ResourceID resourceID, String resourceVersion) { + public boolean containsEventWithResourceVersion(ResourceID resourceID, + String resourceVersion) { List events = resourceEvents.get(resourceID); if (events == null) { return false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 4a23bd9363..e1ef859549 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -69,6 +69,7 @@ public class InformerEventSource private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); private final InformerConfiguration configuration; + // always called from a synchronized method private final EventRecorder eventRecorder = new EventRecorder<>(); public InformerEventSource( @@ -161,7 +162,7 @@ public InformerConfiguration getConfiguration() { } @Override - public void handleRecentResourceUpdate(ResourceID resourceID, R resource, + public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousResourceVersion) { handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceUpdate(resourceID, resource, @@ -169,12 +170,12 @@ public void handleRecentResourceUpdate(ResourceID resourceID, R resource, } @Override - public void handleRecentResourceCreate(ResourceID resourceID, R resource) { + public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceCreate(resourceID, resource)); } - private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnable) { + private void handleRecentCreateOrUpdate(R resource, Runnable runnable) { if (eventRecorder.isRecordingFor(ResourceID.fromResource(resource))) { handleRecentResourceOperationAndStopEventRecording(resource); } else { @@ -199,7 +200,7 @@ private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnab * * @param resource just created or updated resource */ - private synchronized void handleRecentResourceOperationAndStopEventRecording(R resource) { + private void handleRecentResourceOperationAndStopEventRecording(R resource) { ResourceID resourceID = ResourceID.fromResource(resource); try { if (!eventRecorder.containsEventWithResourceVersion( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index fb14ea6847..51a5a73bd4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,75 +35,49 @@ public class TemporaryResourceCache { private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); private final Map cache = new ConcurrentHashMap<>(); - private final ReentrantLock lock = new ReentrantLock(); private final ManagedInformerEventSource managedInformerEventSource; public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource) { this.managedInformerEventSource = managedInformerEventSource; } - public void removeResourceFromCache(T resource) { - lock.lock(); - try { - cache.remove(ResourceID.fromResource(resource)); - } finally { - lock.unlock(); - } + public synchronized void removeResourceFromCache(T resource) { + cache.remove(ResourceID.fromResource(resource)); } - public void unconditionallyCacheResource(T newResource) { - lock.lock(); - try { - cache.put(ResourceID.fromResource(newResource), newResource); - } finally { - lock.unlock(); - } + public synchronized void unconditionallyCacheResource(T newResource) { + cache.put(ResourceID.fromResource(newResource), newResource); } - public void putAddedResource(T newResource) { - lock.lock(); - try { - ResourceID resourceID = ResourceID.fromResource(newResource); - if (managedInformerEventSource.get(resourceID).isEmpty()) { - log.debug("Putting resource to cache with ID: {}", resourceID); - cache.put(ResourceID.fromResource(newResource), newResource); - } else { - log.debug("Won't put resource into cache found already informer cache: {}", resourceID); - } - } finally { - lock.unlock(); + public synchronized void putAddedResource(T newResource) { + ResourceID resourceID = ResourceID.fromResource(newResource); + if (managedInformerEventSource.get(resourceID).isEmpty()) { + log.debug("Putting resource to cache with ID: {}", resourceID); + cache.put(ResourceID.fromResource(newResource), newResource); + } else { + log.debug("Won't put resource into cache found already informer cache: {}", resourceID); } } - public void putUpdatedResource(T newResource, String previousResourceVersion) { - lock.lock(); - try { - var resourceId = ResourceID.fromResource(newResource); - var informerCacheResource = managedInformerEventSource.get(resourceId); - if (informerCacheResource.isEmpty()) { - log.debug("No cached value present for resource: {}", newResource); - return; - } - // if this is not true that means the cache was already updated - if (informerCacheResource.get().getMetadata().getResourceVersion() - .equals(previousResourceVersion)) { - log.debug("Putting resource to temporal cache with id: {}", resourceId); - cache.put(resourceId, newResource); - } else { - // if something is in cache it's surely obsolete now - cache.remove(resourceId); - } - } finally { - lock.unlock(); + public synchronized void putUpdatedResource(T newResource, String previousResourceVersion) { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = managedInformerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + log.debug("Putting resource to temporal cache with id: {}", resourceId); + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); } } - public Optional getResourceFromCache(ResourceID resourceID) { - try { - lock.lock(); - return Optional.ofNullable(cache.get(resourceID)); - } finally { - lock.unlock(); - } + public synchronized Optional getResourceFromCache(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); } } From acd9ec4b2675e387ff21f0efc5db485f4161dc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 9 Mar 2022 21:10:25 +0100 Subject: [PATCH 0058/1319] Race condition fix (#1011) --- .../operator/processing/Controller.java | 2 +- .../processing/event/EventProcessor.java | 50 ++------------ .../ControllerResourceEventSource.java | 67 +++++++++++-------- .../OnceWhitelistEventFilterEventFilter.java | 35 ---------- .../controller/TemporaryResourceCache.java | 50 ++++++++++++++ .../processing/event/EventProcessorTest.java | 63 ++--------------- ...ceWhitelistEventFilterEventFilterTest.java | 41 ------------ .../ControllerResourceEventSourceTest.java | 22 ------ 8 files changed, 101 insertions(+), 229 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index f9a600f1a7..704945925f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -188,7 +188,7 @@ public void start() throws OperatorException { if (reconciler instanceof EventSourceInitializer) { ((EventSourceInitializer) reconciler) .prepareEventSources(new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), configurationService(), kubernetesClient)) .forEach(eventSourceManager::registerEventSource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 29f9adab55..a88d80783d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -29,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.retry.RetryExecution; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; class EventProcessor implements EventHandler, LifecycleAware { @@ -50,7 +49,7 @@ class EventProcessor implements EventHandler, LifecycleAw EventProcessor(EventSourceManager eventSourceManager) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), ExecutorServiceManager.instance().executorService(), eventSourceManager.getController().getConfiguration().getName(), new ReconciliationDispatcher<>(eventSourceManager.getController()), @@ -73,7 +72,7 @@ class EventProcessor implements EventHandler, LifecycleAw Retry retry, Metrics metrics) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), null, relatedControllerName, reconciliationDispatcher, @@ -208,12 +207,12 @@ void eventProcessingFinished( if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { + postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { + eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( + r, executionScope.getResource()); + }); if (eventMarker.eventPresent(resourceID)) { - if (isCacheReadyForInstantReconciliation(executionScope, postExecutionControl)) { - submitReconciliationExecution(resourceID); - } else { - postponeReconciliationAndHandleCacheSyncEvent(resourceID); - } + submitReconciliationExecution(resourceID); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); } @@ -223,41 +222,6 @@ void eventProcessingFinished( } } - private void postponeReconciliationAndHandleCacheSyncEvent(ResourceID resourceID) { - eventSourceManager.getControllerResourceEventSource().whitelistNextEvent(resourceID); - } - - private boolean isCacheReadyForInstantReconciliation( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { - if (!postExecutionControl.customResourceUpdatedDuringExecution()) { - return true; - } - String originalResourceVersion = getVersion(executionScope.getResource()); - String customResourceVersionAfterExecution = - getVersion( - postExecutionControl - .getUpdatedCustomResource() - .orElseThrow( - () -> new IllegalStateException( - "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = - getVersion( - cache - .get(executionScope.getCustomResourceID()) - .orElseThrow( - () -> new IllegalStateException( - "Cached custom resource must be present at this point"))); - - if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { - return true; - } - // If the cached resource version equals neither the version before nor after execution - // probably an update happened on the custom resource independent of the framework during - // reconciliation. We cannot tell at this point if it happened before our update or before. - // (Well we could if we would parse resource version, but that should not be done by definition) - return !cachedCustomResourceVersion.equals(originalResourceVersion); - } - private void reScheduleExecutionIfInstructed( PostExecutionControl postExecutionControl, R customResource) { postExecutionControl diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 9feabf40bf..e2b4e89ebd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -5,6 +5,8 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +24,7 @@ import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @@ -29,7 +32,7 @@ public class ControllerResourceEventSource extends AbstractResourceEventSource - implements ResourceEventHandler { + implements ResourceEventHandler, ResourceCache { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; @@ -40,8 +43,8 @@ public class ControllerResourceEventSource new ConcurrentHashMap<>(); private final ResourceEventFilter filter; - private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; private final ControllerResourceCache cache; + private final TemporaryResourceCache temporaryResourceCache; public ControllerResourceEventSource(Controller controller) { super(controller.getConfiguration().getResourceClass()); @@ -50,20 +53,12 @@ public ControllerResourceEventSource(Controller controller) { var cloner = configurationService != null ? configurationService.getResourceCloner() : ConfigurationService.DEFAULT_CLONER; this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); - + temporaryResourceCache = new TemporaryResourceCache<>(cache); var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), ResourceEventFilters.markedForDeletion(), - ResourceEventFilters.generationAware(), - null + ResourceEventFilters.generationAware() }; - - if (controller.getConfiguration().isGenerationAware()) { - onceWhitelistEventFilterEventFilter = new OnceWhitelistEventFilterEventFilter<>(); - filters[filters.length - 1] = onceWhitelistEventFilterEventFilter; - } else { - onceWhitelistEventFilterEventFilter = null; - } if (controller.getConfiguration().getEventFilter() != null) { filter = controller.getConfiguration().getEventFilter().and(ResourceEventFilters.or(filters)); } else { @@ -126,6 +121,7 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource try { log.debug( "Event received for resource: {}", getName(customResource)); + temporaryResourceCache.removeResourceFromCache(customResource); MDCUtils.addResourceInfo(customResource); controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, oldResource); @@ -158,12 +154,31 @@ public void onDelete(T resource, boolean b) { eventReceived(ResourceAction.DELETED, resource, null); } + + @Override public Optional get(ResourceID resourceID) { - return cache.get(resourceID); + Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); + if (resource.isPresent()) { + log.debug("Resource found in temporal cache for Resource ID: {}", resourceID); + return resource; + } else { + return cache.get(resourceID); + } + } + + @Override + public Stream keys() { + return cache.keys(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list(predicate); } - public ControllerResourceCache getResourceCache() { - return cache; + @Override + public Stream list(String namespace, Predicate predicate) { + return cache.list(namespace, predicate); } /** @@ -178,19 +193,6 @@ public SharedIndexInformer getInformer(String namespace) { return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); } - /** - * This will ensure that the next event received after this method is called will not be filtered - * out. - * - * @param resourceID - to which the event is related - */ - public void whitelistNextEvent(ResourceID resourceID) { - if (onceWhitelistEventFilterEventFilter != null) { - onceWhitelistEventFilterEventFilter.whitelistNextEvent(resourceID); - } - } - - private void handleKubernetesClientException(Exception e) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { @@ -204,6 +206,13 @@ private void handleKubernetesClientException(Exception e) { @Override public Optional getAssociated(T primary) { - return cache.get(ResourceID.fromResource(primary)); + return get(ResourceID.fromResource(primary)); } + + public void handleRecentResourceUpdate(T resource, + T previousResourceVersion) { + temporaryResourceCache.putUpdatedResource(resource, + previousResourceVersion.getMetadata().getResourceVersion()); + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java deleted file mode 100644 index 8262ff1c21..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source.controller; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - -public class OnceWhitelistEventFilterEventFilter - implements ResourceEventFilter { - - private static final Logger log = - LoggerFactory.getLogger(OnceWhitelistEventFilterEventFilter.class); - - private final ConcurrentMap whiteList = new ConcurrentHashMap<>(); - - @Override - public boolean acceptChange(ControllerConfiguration configuration, T oldResource, - T newResource) { - ResourceID resourceID = ResourceID.fromResource(newResource); - boolean res = whiteList.remove(resourceID, resourceID); - if (res) { - log.debug("Accepting whitelisted event for CR id: {}", resourceID); - } - return res; - } - - public void whitelistNextEvent(ResourceID resourceID) { - whiteList.putIfAbsent(resourceID, resourceID); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java new file mode 100644 index 0000000000..fb402b0b86 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.processing.event.source.controller; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class TemporaryResourceCache { + + private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + + private final Map cache = new HashMap<>(); + private final ControllerResourceCache managedInformerEventSource; + + public TemporaryResourceCache(ControllerResourceCache managedInformerEventSource) { + this.managedInformerEventSource = managedInformerEventSource; + } + + public synchronized void removeResourceFromCache(T resource) { + cache.remove(ResourceID.fromResource(resource)); + } + + public synchronized void putUpdatedResource(T newResource, String previousResourceVersion) { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = managedInformerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + log.debug("Putting resource to temporal cache with id: {}", resourceId); + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); + } + } + + public synchronized Optional getResourceFromCache(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); + } +} + diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 5a3f73742e..7422e6f572 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -13,7 +13,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; @@ -44,8 +43,6 @@ class EventProcessorTest { private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); - private ControllerResourceCache resourceCacheMock = - mock(ControllerResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerResourceEventSource controllerResourceEventSourceMock = mock(ControllerResourceEventSource.class); @@ -58,7 +55,6 @@ public void setup() { when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(controllerResourceEventSourceMock); - when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, @@ -83,7 +79,8 @@ public void dispatchesEventsIfNoExecutionInProgress() { @Test public void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - when(resourceCacheMock.get(event.getRelatedCustomResourceID())).thenReturn(Optional.empty()); + when(controllerResourceEventSourceMock.get(event.getRelatedCustomResourceID())) + .thenReturn(Optional.empty()); eventProcessor.handleEvent(event); @@ -214,57 +211,6 @@ public void doNotFireEventsIfClosing() { verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); } - @Test - public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(1)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventWhenOtherChangeDuringExecution() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var otherChangeCR = testCustomResource(crID); - otherChangeCR.getMetadata().setResourceVersion("3"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventIfUpdatedEventInCache() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(cr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - @Test public void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); @@ -282,7 +228,8 @@ public void startProcessedMarkedEventReceivedBefore() { eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, metricsMock)); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); + when(controllerResourceEventSourceMock.get(eq(crID))) + .thenReturn(Optional.of(testCustomResource())); eventProcessor.handleEvent(new Event(crID)); verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); @@ -311,7 +258,7 @@ private ResourceEvent prepareCREvent() { private ResourceEvent prepareCREvent(ResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - when(resourceCacheMock.get(eq(uid))).thenReturn(Optional.of(customResource)); + when(controllerResourceEventSourceMock.get(eq(uid))).thenReturn(Optional.of(customResource)); return new ResourceEvent(ResourceAction.UPDATED, ResourceID.fromResource(customResource)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java deleted file mode 100644 index f82bea55c7..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import org.junit.jupiter.api.Test; - -import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.controller.OnceWhitelistEventFilterEventFilter; - -import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static org.assertj.core.api.Assertions.assertThat; - -class OnceWhitelistEventFilterEventFilterTest { - - private OnceWhitelistEventFilterEventFilter filter = new OnceWhitelistEventFilterEventFilter<>(); - - @Test - public void notAcceptCustomResourceNotWhitelisted() { - assertThat(filter.acceptChange(null, - testCustomResource(), testCustomResource())).isFalse(); - } - - @Test - public void allowCustomResourceWhitelisted() { - var cr = TestUtils.testCustomResource(); - - filter.whitelistNextEvent(ResourceID.fromResource(cr)); - - assertThat(filter.acceptChange(null, cr, cr)).isTrue(); - } - - @Test - public void allowCustomResourceWhitelistedOnlyOnce() { - var cr = TestUtils.testCustomResource(); - - filter.whitelistNextEvent(ResourceID.fromResource(cr)); - - assertThat(filter.acceptChange(null, cr, cr)).isTrue(); - assertThat(filter.acceptChange(null, cr, cr)).isFalse(); - } - -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 5859115ee2..7f20918f13 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -14,7 +14,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -103,27 +102,6 @@ public void eventWithNoGenerationProcessedIfNoFinalizer() { verify(eventHandler, times(1)).handleEvent(any()); } - @Test - public void handlesNextEventIfWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - source.whitelistNextEvent(ResourceID.fromResource(customResource)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(1)).handleEvent(any()); - } - - @Test - public void notHandlesNextEventIfNotWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(0)).handleEvent(any()); - } - @Test public void callsBroadcastsOnResourceEvents() { TestCustomResource customResource1 = TestUtils.testCustomResource(); From e5ee911605b17b3ef44b497c4b370a1696adceaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 10 Mar 2022 09:15:14 +0100 Subject: [PATCH 0059/1319] fix: issue with reading from cache (#1008) --- .../event/source/controller/ControllerResourceEventSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 464cc3e500..b43666660c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -102,6 +102,6 @@ private void handleKubernetesClientException(Exception e) { @Override public Optional getAssociated(T primary) { - return manager().get(ResourceID.fromResource(primary)); + return get(ResourceID.fromResource(primary)); } } From c612c2706ec09f9ad8ad6b7f188c716f906ac150 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 10 Mar 2022 10:11:56 +0100 Subject: [PATCH 0060/1319] fix: debugThreadPool & shouldCheckCRDAndValidateLocalModel should work (#1016) Fixes #1014 --- .../operator/api/config/Utils.java | 21 +++++- .../operator/api/config/UtilsTest.java | 64 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index b36c0468cd..06bbcaa7a8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -61,10 +61,27 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return Boolean.getBoolean(System.getProperty(CHECK_CRD_ENV_KEY, "true")); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); } public static boolean debugThreadPool() { - return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); + return getBooleanFromSystemPropsOrDefault(DEBUG_THREAD_POOL_ENV_KEY, false); + } + + static boolean getBooleanFromSystemPropsOrDefault(String propertyName, boolean defaultValue) { + var property = System.getProperty(propertyName); + if (property == null) { + return defaultValue; + } else { + property = property.trim().toLowerCase(); + switch (property) { + case "true": + return true; + case "false": + return false; + default: + return defaultValue; + } + } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java new file mode 100644 index 0000000000..a17eb1ba59 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -0,0 +1,64 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UtilsTest { + + @Test + void shouldCheckCRDAndValidateLocalModelByDefault() { + assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + } + + @Test + void shouldNotDebugThreadPoolByDefault() { + assertFalse(Utils.debugThreadPool()); + } + + @Test + void askingForNonexistentPropertyShouldReturnDefault() { + final var key = "foo"; + assertNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } + + @Test + void askingForExistingPropertyShouldReturnItIfBoolean() { + final var key = "foo"; + try { + System.setProperty(key, "true"); + assertNotNull(System.getProperty(key)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, "TruE"); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \tTRUE "); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \nFalSe \t "); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } + + @Test + void askingForExistingNonBooleanPropertyShouldReturnDefaultValue() { + final var key = "foo"; + try { + System.setProperty(key, "bar"); + assertNotNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } +} From d81db18bdce549bdfad5760e1ce0c2c455c4187d Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 10 Mar 2022 10:12:10 +0100 Subject: [PATCH 0061/1319] fix: debugThreadPool & shouldCheckCRDAndValidateLocalModel should work (#1015) Fixes #1014 --- .../operator/api/config/Utils.java | 21 ++++++- .../operator/api/config/UtilsTest.java | 56 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index 8d9990ee9c..db882ae09e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -64,11 +64,28 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return Boolean.getBoolean(System.getProperty(CHECK_CRD_ENV_KEY, "true")); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); } public static boolean debugThreadPool() { - return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); + return getBooleanFromSystemPropsOrDefault(DEBUG_THREAD_POOL_ENV_KEY, false); + } + + static boolean getBooleanFromSystemPropsOrDefault(String propertyName, boolean defaultValue) { + var property = System.getProperty(propertyName); + if (property == null) { + return defaultValue; + } else { + property = property.trim().toLowerCase(); + switch (property) { + case "true": + return true; + case "false": + return false; + default: + return defaultValue; + } + } } public static Class getFirstTypeArgumentFromExtendedClass(Class clazz) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 88dd8db469..fbaa28ba23 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -12,6 +12,62 @@ class UtilsTest { + @Test + void shouldCheckCRDAndValidateLocalModelByDefault() { + assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + } + + @Test + void shouldNotDebugThreadPoolByDefault() { + assertFalse(Utils.debugThreadPool()); + } + + @Test + void askingForNonexistentPropertyShouldReturnDefault() { + final var key = "foo"; + assertNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } + + @Test + void askingForExistingPropertyShouldReturnItIfBoolean() { + final var key = "foo"; + try { + System.setProperty(key, "true"); + assertNotNull(System.getProperty(key)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, "TruE"); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \tTRUE "); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \nFalSe \t "); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } + + @Test + void askingForExistingNonBooleanPropertyShouldReturnDefaultValue() { + final var key = "foo"; + try { + System.setProperty(key, "bar"); + assertNotNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } + @Test void getsFirstTypeArgumentFromExtendedClass() { Class res = From 808228f1628160ca175176cc6bc50cf89706abe1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 10 Mar 2022 10:37:31 +0100 Subject: [PATCH 0062/1319] feat: add non-apt-tied behavior annotation and configuration to core (#1013) * feat: add non-apt-tied behavior annotation and configuration to core Fixes #1012 * fix: extract resource class from associated reconciler --- .../AnnotationControllerConfiguration.java | 186 ++++++++++++++++++ .../api/config/BaseConfigurationService.java | 44 +++++ .../AnnotationControllerConfiguration.java | 168 +--------------- .../runtime/DefaultConfigurationService.java | 47 ++--- 4 files changed, 251 insertions(+), 194 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java new file mode 100644 index 0000000000..5fd1f3dbfd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -0,0 +1,186 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; + +@SuppressWarnings("rawtypes") +public class AnnotationControllerConfiguration + implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { + + protected final Reconciler reconciler; + private final ControllerConfiguration annotation; + private ConfigurationService service; + private List specs; + + public AnnotationControllerConfiguration(Reconciler reconciler) { + this.reconciler = reconciler; + this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); + } + + @Override + public String getName() { + return ReconcilerUtils.getNameFor(reconciler); + } + + @Override + public String getFinalizer() { + if (annotation == null || annotation.finalizerName().isBlank()) { + return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); + } else { + final var finalizer = annotation.finalizerName(); + if (ReconcilerUtils.isFinalizerValid(finalizer)) { + return finalizer; + } else { + throw new IllegalArgumentException( + finalizer + + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); + } + } + } + + @Override + public boolean isGenerationAware() { + return valueOrDefault( + annotation, ControllerConfiguration::generationAwareEventProcessing, true); + } + + @Override + public Set getNamespaces() { + return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, new String[] {})); + } + + @Override + @SuppressWarnings("unchecked") + public Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromInterface(reconciler.getClass()); + } + + @Override + public String getLabelSelector() { + return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + this.service = service; + } + + @Override + public String getAssociatedReconcilerClassName() { + return reconciler.getClass().getCanonicalName(); + } + + @SuppressWarnings("unchecked") + @Override + public ResourceEventFilter getEventFilter() { + ResourceEventFilter answer = null; + + Class>[] filterTypes = + (Class>[]) valueOrDefault(annotation, + ControllerConfiguration::eventFilters, new Object[] {}); + if (filterTypes.length > 0) { + for (var filterType : filterTypes) { + try { + ResourceEventFilter filter = filterType.getConstructor().newInstance(); + + if (answer == null) { + answer = filter; + } else { + answer = answer.and(filter); + } + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } + return answer != null ? answer : ResourceEventFilters.passthrough(); + } + + @Override + public Optional reconciliationMaxInterval() { + if (annotation.reconciliationMaxInterval() != null) { + if (annotation.reconciliationMaxInterval().interval() <= 0) { + return Optional.empty(); + } + return Optional.of( + Duration.of( + annotation.reconciliationMaxInterval().interval(), + annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); + } else { + return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); + } + } + + public static T valueOrDefault( + ControllerConfiguration controllerConfiguration, + Function mapper, + T defaultValue) { + if (controllerConfiguration == null) { + return defaultValue; + } else { + return mapper.apply(controllerConfiguration); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public List getDependentResources() { + if (specs == null) { + final var dependents = + valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } + + specs = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); + final var labelSelector = + Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var addOwnerReference = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::addOwnerReference, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + KubernetesDependentResourceConfig config = + new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); + specs.add(new DependentResourceSpec(dependentType, config)); + } else { + specs.add(new DependentResourceSpec(dependentType)); + } + } + } + return specs; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index 894091ad2d..bfc4c6c588 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -3,6 +3,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; + public class BaseConfigurationService extends AbstractConfigurationService { private static final String LOGGER_NAME = "Default ConfigurationService implementation"; @@ -12,6 +15,10 @@ public BaseConfigurationService(Version version) { super(version); } + public BaseConfigurationService() { + this(Utils.loadFromProperties()); + } + @Override protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { logger.warn("Configuration for reconciler '{}' was not found. {}", reconcilerKey, @@ -25,4 +32,41 @@ public String getLoggerName() { protected Logger getLogger() { return logger; } + + @Override + public ControllerConfiguration getConfigurationFor( + Reconciler reconciler) { + var config = super.getConfigurationFor(reconciler); + if (config == null) { + if (createIfNeeded()) { + // create the configuration on demand and register it + config = configFor(reconciler); + register(config); + getLogger().info( + "Created configuration for reconciler {} with name {}", + reconciler.getClass().getName(), + config.getName()); + } + } else { + // check that we don't have a reconciler name collision + final var newControllerClassName = reconciler.getClass().getCanonicalName(); + if (!config.getAssociatedReconcilerClassName().equals(newControllerClassName)) { + throwExceptionOnNameCollision(newControllerClassName, config); + } + } + return config; + } + + protected ControllerConfiguration configFor(Reconciler reconciler) { + return new AnnotationControllerConfiguration<>(reconciler); + } + + protected boolean createIfNeeded() { + return true; + } + + @Override + public boolean checkCRDAndValidateLocalModel() { + return Utils.shouldCheckCRDAndValidateLocalModel(); + } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java index 67a8937ac4..e1e5e83fa7 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java @@ -1,183 +1,19 @@ package io.javaoperatorsdk.operator.config.runtime; -import java.time.Duration; import java.util.*; -import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Utils; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -@SuppressWarnings("rawtypes") public class AnnotationControllerConfiguration - implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { - - private final Reconciler reconciler; - private final ControllerConfiguration annotation; - private ConfigurationService service; - private List specs; + extends io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration { public AnnotationControllerConfiguration(Reconciler reconciler) { - this.reconciler = reconciler; - this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); - } - - @Override - public String getName() { - return ReconcilerUtils.getNameFor(reconciler); - } - - @Override - public String getFinalizer() { - if (annotation == null || annotation.finalizerName().isBlank()) { - return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); - } else { - final var finalizer = annotation.finalizerName(); - if (ReconcilerUtils.isFinalizerValid(finalizer)) { - return finalizer; - } else { - throw new IllegalArgumentException( - finalizer - + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); - } - } - } - - @Override - public boolean isGenerationAware() { - return valueOrDefault( - annotation, ControllerConfiguration::generationAwareEventProcessing, true); + super(reconciler); } @Override public Class getResourceClass() { return RuntimeControllerMetadata.getResourceClass(reconciler); } - - @Override - public Set getNamespaces() { - return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, new String[] {})); - } - - @Override - public String getLabelSelector() { - return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); - } - - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } - - @Override - public String getAssociatedReconcilerClassName() { - return reconciler.getClass().getCanonicalName(); - } - - @SuppressWarnings("unchecked") - @Override - public ResourceEventFilter getEventFilter() { - ResourceEventFilter answer = null; - - Class>[] filterTypes = - (Class>[]) valueOrDefault(annotation, - ControllerConfiguration::eventFilters, new Object[] {}); - if (filterTypes.length > 0) { - for (var filterType : filterTypes) { - try { - ResourceEventFilter filter = filterType.getConstructor().newInstance(); - - if (answer == null) { - answer = filter; - } else { - answer = answer.and(filter); - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - } - } - return answer != null ? answer : ResourceEventFilters.passthrough(); - } - - @Override - public Optional reconciliationMaxInterval() { - if (annotation.reconciliationMaxInterval() != null) { - if (annotation.reconciliationMaxInterval().interval() <= 0) { - return Optional.empty(); - } - return Optional.of( - Duration.of( - annotation.reconciliationMaxInterval().interval(), - annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); - } else { - return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); - } - } - - public static T valueOrDefault( - ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); - } - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Override - public List getDependentResources() { - if (specs == null) { - final var dependents = - valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); - if (dependents.length == 0) { - return Collections.emptyList(); - } - - specs = new ArrayList<>(dependents.length); - for (Dependent dependent : dependents) { - final Class dependentType = dependent.type(); - if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - final var namespaces = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::namespaces, - this.getNamespaces().toArray(new String[0])); - final var labelSelector = - Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); - final var addOwnerReference = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::addOwnerReference, - KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - KubernetesDependentResourceConfig config = - new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); - specs.add(new DependentResourceSpec(dependentType, config)); - } else { - specs.add(new DependentResourceSpec(dependentType)); - } - } - } - return specs; - } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java index 21af48cfd1..d34d6fee55 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java @@ -3,52 +3,43 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; public class DefaultConfigurationService extends BaseConfigurationService { private static final DefaultConfigurationService instance = new DefaultConfigurationService(); + private boolean createIfNeeded = super.createIfNeeded(); private DefaultConfigurationService() { - super(Utils.loadFromProperties()); + super(); } public static DefaultConfigurationService instance() { return instance; } - @Override - public ControllerConfiguration getConfigurationFor( - Reconciler reconciler) { - return getConfigurationFor(reconciler, true); - } - ControllerConfiguration getConfigurationFor( Reconciler reconciler, boolean createIfNeeded) { - var config = super.getConfigurationFor(reconciler); - if (config == null) { - if (createIfNeeded) { - // create the configuration on demand and register it - config = new AnnotationControllerConfiguration<>(reconciler); - register(config); - getLogger().info( - "Created configuration for reconciler {} with name {}", - reconciler.getClass().getName(), - config.getName()); - } - } else { - // check that we don't have a reconciler name collision - final var newControllerClassName = reconciler.getClass().getCanonicalName(); - if (!config.getAssociatedReconcilerClassName().equals(newControllerClassName)) { - throwExceptionOnNameCollision(newControllerClassName, config); - } + final var previous = createIfNeeded(); + setCreateIfNeeded(createIfNeeded); + try { + return super.getConfigurationFor(reconciler); + } finally { + setCreateIfNeeded(previous); } - return config; } @Override - public boolean checkCRDAndValidateLocalModel() { - return Utils.shouldCheckCRDAndValidateLocalModel(); + protected boolean createIfNeeded() { + return createIfNeeded; + } + + public void setCreateIfNeeded(boolean createIfNeeded) { + this.createIfNeeded = createIfNeeded; + } + + @Override + protected ControllerConfiguration configFor(Reconciler reconciler) { + return new AnnotationControllerConfiguration<>(reconciler); } } From a3a81efc2a9cccdcc500af8f62eee88b5008e862 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Mar 2022 09:09:43 +0100 Subject: [PATCH 0063/1319] chore(deps): bump maven-compiler-plugin from 3.10.0 to 3.10.1 (#1022) Bumps [maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.10.0 to 3.10.1. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.10.0...maven-compiler-plugin-3.10.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index dee39368d9..0e00c4c9ca 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.8.3 2.11 - 3.10.0 + 3.10.1 3.0.0-M5 3.3.2 3.2.0 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 6cf30996a6..eb7c937e95 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -105,7 +105,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.0 + 3.10.1 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 7b41dfb4b2..ae046ca991 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -90,7 +90,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.0 + 3.10.1 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index e79e07ac4f..0a7b7a71cf 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.0 + 3.10.1 From b3583299e86851eebd7b01af853b281665aa43ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 09:11:02 +0100 Subject: [PATCH 0064/1319] fix: renaming mapper primary <-> secondary (#1021) --- .../informer/InformerConfiguration.java | 30 +++++++++---------- .../KubernetesDependentResource.java | 12 ++++---- ...ier.java => PrimaryToSecondaryMapper.java} | 2 +- ...ver.java => SecondaryToPrimaryMapper.java} | 3 +- .../event/source/informer/Mappers.java | 14 ++++----- .../informer/InformerEventSourceTest.java | 10 +++---- .../dependent/SecretDependentResource.java | 6 ++-- .../operator/sample/WebappReconciler.java | 8 ++--- .../WebPageReconcilerDependentResources.java | 4 +-- 9 files changed, 43 insertions(+), 46 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{AssociatedSecondaryResourceIdentifier.java => PrimaryToSecondaryMapper.java} (75%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{PrimaryResourcesRetriever.java => SecondaryToPrimaryMapper.java} (83%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 0d08b65f99..3252c87925 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -10,8 +10,8 @@ import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public interface InformerConfiguration @@ -20,13 +20,13 @@ public interface InformerConfiguration extends DefaultResourceConfiguration implements InformerConfiguration { - private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private final AssociatedSecondaryResourceIdentifier

associatedWith; + private final SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; + private final PrimaryToSecondaryMapper

associatedWith; protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, + SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet, + PrimaryToSecondaryMapper

associatedWith, Set namespaces) { super(labelSelector, resourceClass, namespaces); setConfigurationService(service); @@ -38,24 +38,24 @@ protected DefaultInformerConfiguration(ConfigurationService service, String labe } - public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + public SecondaryToPrimaryMapper getPrimaryResourcesRetriever() { return secondaryToPrimaryResourcesIdSet; } - public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { + public PrimaryToSecondaryMapper

getAssociatedResourceIdentifier() { return associatedWith; } } - PrimaryResourcesRetriever getPrimaryResourcesRetriever(); + SecondaryToPrimaryMapper getPrimaryResourcesRetriever(); - AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier(); + PrimaryToSecondaryMapper

getAssociatedResourceIdentifier(); class InformerConfigurationBuilder { - private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private AssociatedSecondaryResourceIdentifier

associatedWith; + private SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; + private PrimaryToSecondaryMapper

associatedWith; private Set namespaces; private String labelSelector; private final Class resourceClass; @@ -68,13 +68,13 @@ private InformerConfigurationBuilder(Class resourceClass, } public InformerConfigurationBuilder withPrimaryResourcesRetriever( - PrimaryResourcesRetriever primaryResourcesRetriever) { - this.secondaryToPrimaryResourcesIdSet = primaryResourcesRetriever; + SecondaryToPrimaryMapper secondaryToPrimaryMapper) { + this.secondaryToPrimaryResourcesIdSet = secondaryToPrimaryMapper; return this; } public InformerConfigurationBuilder withAssociatedSecondaryResourceIdentifier( - AssociatedSecondaryResourceIdentifier

associatedWith) { + PrimaryToSecondaryMapper

associatedWith) { this.associatedWith = associatedWith; return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index c94cec73d8..5ff8bc6947 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -24,9 +24,9 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @@ -63,11 +63,11 @@ public void configureWith(KubernetesDependentResourceConfig config) { private void configureWith(ConfigurationService configService, String labelSelector, Set namespaces, boolean addOwnerReference) { final var primaryResourcesRetriever = - (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this + (this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper) this : Mappers.fromOwnerReference(); - final AssociatedSecondaryResourceIdentifier

secondaryResourceIdentifier = - (this instanceof AssociatedSecondaryResourceIdentifier) - ? (AssociatedSecondaryResourceIdentifier

) this + final PrimaryToSecondaryMapper

secondaryResourceIdentifier = + (this instanceof PrimaryToSecondaryMapper) + ? (PrimaryToSecondaryMapper

) this : ResourceID::fromResource; InformerConfiguration ic = InformerConfiguration.from(configService, resourceType()) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java similarity index 75% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java index b402429baa..5e88c39727 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java @@ -4,6 +4,6 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; @FunctionalInterface -public interface AssociatedSecondaryResourceIdentifier

{ +public interface PrimaryToSecondaryMapper

{ ResourceID associatedSecondaryID(P primary); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java similarity index 83% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java index 8f01a95bb3..7804ac0f82 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java @@ -5,7 +5,6 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; @FunctionalInterface -public interface PrimaryResourcesRetriever { - +public interface SecondaryToPrimaryMapper { Set associatedPrimaryResources(T dependentResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 21cd3b162a..1c0150b084 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -5,38 +5,38 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; public class Mappers { private Mappers() {} - public static PrimaryResourcesRetriever fromAnnotation( + public static SecondaryToPrimaryMapper fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); } - public static PrimaryResourcesRetriever fromAnnotation( + public static SecondaryToPrimaryMapper fromAnnotation( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, false); } - public static PrimaryResourcesRetriever fromLabel( + public static SecondaryToPrimaryMapper fromLabel( String nameKey) { return fromMetadata(nameKey, null, true); } - public static PrimaryResourcesRetriever fromLabel( + public static SecondaryToPrimaryMapper fromLabel( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, true); } - public static PrimaryResourcesRetriever fromOwnerReference() { + public static SecondaryToPrimaryMapper fromOwnerReference() { return resource -> ResourceID.fromFirstOwnerReference(resource).map(Set::of) .orElse(Collections.emptySet()); } - private static PrimaryResourcesRetriever fromMetadata( + private static SecondaryToPrimaryMapper fromMetadata( String nameKey, String namespaceKey, boolean isLabel) { return resource -> { final var metadata = resource.getMetadata(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 4caf9b70d2..131454b3d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -16,7 +16,7 @@ import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.any; @@ -50,16 +50,16 @@ void setup() { when(labeledResourceClientMock.runnableInformer(0)).thenReturn(informer); when(informerConfiguration.getPrimaryResourcesRetriever()) - .thenReturn(mock(PrimaryResourcesRetriever.class)); + .thenReturn(mock(SecondaryToPrimaryMapper.class)); informerEventSource = new InformerEventSource<>(informerConfiguration, clientMock); informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); informerEventSource.setEventHandler(eventHandlerMock); - PrimaryResourcesRetriever primaryResourcesRetriever = mock(PrimaryResourcesRetriever.class); + SecondaryToPrimaryMapper secondaryToPrimaryMapper = mock(SecondaryToPrimaryMapper.class); when(informerConfiguration.getPrimaryResourcesRetriever()) - .thenReturn(primaryResourcesRetriever); - when(primaryResourcesRetriever.associatedPrimaryResources(any())) + .thenReturn(secondaryToPrimaryMapper); + when(secondaryToPrimaryMapper.associatedPrimaryResources(any())) .thenReturn(Set.of(ResourceID.fromResource(testDeployment()))); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 6c71252bb5..1b26eaf47a 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -11,13 +11,11 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.sample.MySQLSchema; -import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; - public class SecretDependentResource extends KubernetesDependentResource - implements AssociatedSecondaryResourceIdentifier, Creator { + implements PrimaryToSecondaryMapper, Creator { public static final String SECRET_FORMAT = "%s-secret"; public static final String USERNAME_FORMAT = "%s-user"; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 9e10c39ca8..694ff07d02 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -26,9 +26,9 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -52,7 +52,7 @@ public List prepareEventSources(EventSourceContext context) * customResourceId of the WebApp resource we traverse the cache and identify it based on naming * convention. */ - final PrimaryResourcesRetriever webappsMatchingTomcatName = + final SecondaryToPrimaryMapper webappsMatchingTomcatName = (Tomcat t) -> context.getPrimaryCache() .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) .map(ResourceID::fromResource) @@ -61,7 +61,7 @@ public List prepareEventSources(EventSourceContext context) /* * We retrieve the Tomcat instance associated with out Webapp from its spec */ - final AssociatedSecondaryResourceIdentifier tomcatFromWebAppSpec = + final PrimaryToSecondaryMapper tomcatFromWebAppSpec = (Webapp webapp) -> new ResourceID( webapp.getSpec().getTomcat(), webapp.getMetadata().getNamespace()); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index b4f6db1395..08a02e8a2c 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -14,8 +14,8 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -163,7 +163,7 @@ public static String serviceName(WebPage webPage) { private class ConfigMapDependentResource extends CrudKubernetesDependentResource implements - AssociatedSecondaryResourceIdentifier { + PrimaryToSecondaryMapper { @Override protected ConfigMap desired(WebPage webPage, Context context) { From cfccc718003a5cebcf5bc07c68ad76fb2767f568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 09:11:19 +0100 Subject: [PATCH 0065/1319] feat: dependent resource inherits namespaces from controller config (#1020) --- .../informer/InformerConfiguration.java | 3 +- .../operator/api/reconciler/Context.java | 7 ++-- .../api/reconciler/DefaultContext.java | 12 +++--- .../api/reconciler/EventSourceContext.java | 22 +++++----- .../operator/api/reconciler/Reconciler.java | 4 +- .../dependent/AbstractDependentResource.java | 16 ++++---- .../api/reconciler/dependent/Creator.java | 2 +- .../api/reconciler/dependent/Deleter.java | 2 +- .../dependent/DependentResource.java | 4 +- .../dependent/DesiredEqualsMatcher.java | 2 +- .../api/reconciler/dependent/Matcher.java | 2 +- .../api/reconciler/dependent/Updater.java | 4 +- .../operator/processing/Controller.java | 41 +++++++++---------- .../dependent/DependentResourceManager.java | 4 +- .../AbstractSimpleDependentResource.java | 8 ++-- .../GenericKubernetesResourceMatcher.java | 5 ++- .../GenericResourceUpdatePreProcessor.java | 3 +- .../KubernetesDependentResource.java | 11 ++--- .../event/ReconciliationDispatcher.java | 4 +- .../GenericKubernetesResourceMatcherTest.java | 5 ++- ...GenericResourceUpdatePreProcessorTest.java | 5 ++- .../event/ReconciliationDispatcherTest.java | 2 +- .../ErrorStatusHandlerTestReconciler.java | 3 +- .../retry/RetryTestCustomReconciler.java | 2 +- .../StandaloneDependentTestReconciler.java | 3 +- .../sample/MySQLSchemaReconciler.java | 2 +- .../dependent/SchemaDependentResource.java | 6 +-- .../operator/sample/TomcatReconciler.java | 2 +- .../operator/sample/WebappReconciler.java | 4 +- .../operator/sample/WebPageReconciler.java | 2 +- .../WebPageReconcilerDependentResources.java | 2 +- 31 files changed, 102 insertions(+), 92 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 3252c87925..50f21629b8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -105,7 +105,8 @@ public InformerConfiguration build() { static InformerConfigurationBuilder from( EventSourceContext

context, Class resourceClass) { - return new InformerConfigurationBuilder<>(resourceClass, context.getConfigurationService()); + return new InformerConfigurationBuilder<>(resourceClass, context.getControllerConfiguration() + .getConfigurationService()); } static InformerConfigurationBuilder from(ConfigurationService configurationService, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 2f203fa07c..2c3fb6ff1e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,10 +2,11 @@ import java.util.Optional; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; -public interface Context { +public interface Context

{ Optional getRetryInfo(); @@ -15,7 +16,7 @@ default Optional getSecondaryResource(Class expectedType) { Optional getSecondaryResource(Class expectedType, String eventSourceName); - ConfigurationService getConfigurationService(); + ControllerConfiguration

getControllerConfiguration(); ManagedDependentResourceContext managedDependentResourceContext(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 33f640789b..96084ffd1f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -3,23 +3,23 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; -public class DefaultContext

implements Context { +public class DefaultContext

implements Context

{ private final RetryInfo retryInfo; private final Controller

controller; private final P primaryResource; - private final ConfigurationService configurationService; + private final ControllerConfiguration

controllerConfiguration; private ManagedDependentResourceContext managedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; - this.configurationService = controller.getConfiguration().getConfigurationService(); + this.controllerConfiguration = controller.getConfiguration(); this.managedDependentResourceContext = new ManagedDependentResourceContext( controller.getDependents()); } @@ -37,8 +37,8 @@ public Optional getSecondaryResource(Class expectedType, String eventS } @Override - public ConfigurationService getConfigurationService() { - return configurationService; + public ControllerConfiguration

getControllerConfiguration() { + return controllerConfiguration; } public ManagedDependentResourceContext managedDependentResourceContext() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index ba32f493ec..9f49c3b737 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; @@ -14,13 +14,14 @@ public class EventSourceContext

{ private final ResourceCache

primaryCache; - private final ConfigurationService configurationService; + private final ControllerConfiguration

controllerConfiguration; private final KubernetesClient client; public EventSourceContext(ResourceCache

primaryCache, - ConfigurationService configurationService, KubernetesClient client) { + ControllerConfiguration

controllerConfiguration, + KubernetesClient client) { this.primaryCache = primaryCache; - this.configurationService = configurationService; + this.controllerConfiguration = controllerConfiguration; this.client = client; } @@ -34,16 +35,13 @@ public ResourceCache

getPrimaryCache() { } /** - * Retrieves the {@link ConfigurationService} associated with the operator. This allows, in - * particular, to lookup global configuration information such as the configured - * {@link io.javaoperatorsdk.operator.api.monitoring.Metrics} or - * {@link io.javaoperatorsdk.operator.api.config.Cloner} implementations, which could be useful to - * event sources. + * Retrieves the {@link ControllerConfiguration} associated with the operator. This allows, in + * particular, to lookup controller and global configuration information such as the configured* * - * @return the {@link ConfigurationService} associated with the operator + * @return the {@link ControllerConfiguration} associated with the operator */ - public ConfigurationService getConfigurationService() { - return configurationService; + public ControllerConfiguration

getControllerConfiguration() { + return controllerConfiguration; } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 2b725afd73..0be4d58ae5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -13,7 +13,7 @@ public interface Reconciler { * @return UpdateControl to manage updates on the custom resource (usually the status) after * reconciliation. */ - UpdateControl reconcile(R resource, Context context); + UpdateControl reconcile(R resource, Context context); /** * Note that this method is used in combination with finalizers. If automatic finalizer handling @@ -37,7 +37,7 @@ public interface Reconciler { * finalizer to indicate that the resource should not be deleted after all, in which case * the controller should restore the resource's state appropriately. */ - default DeleteControl cleanup(R resource, Context context) { + default DeleteControl cleanup(R resource, Context context) { return DeleteControl.defaultDelete(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index af727b66eb..3e4414a009 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -36,7 +36,7 @@ public AbstractDependentResource() { } @Override - public void reconcile(P primary, Context context) { + public void reconcile(P primary, Context

context) { final var creatable = isCreatable(primary, context); final var updatable = isUpdatable(primary, context); if (creatable || updatable) { @@ -67,7 +67,7 @@ public void reconcile(P primary, Context context) { } } - protected void handleCreate(R desired, P primary, Context context) { + protected void handleCreate(R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R created = null; try { @@ -107,7 +107,7 @@ private void prepareEventFiltering(R desired, ResourceID resourceID) { } } - protected void handleUpdate(R actual, R desired, P primary, Context context) { + protected void handleUpdate(R actual, R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R updated = null; try { @@ -131,29 +131,29 @@ private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() } @Override - public void cleanup(P primary, Context context) { + public void cleanup(P primary, Context

context) { if (isDeletable(primary, context)) { deleter.delete(primary, context); } } - protected R desired(P primary, Context context) { + protected R desired(P primary, Context

context) { throw new IllegalStateException( "desired method must be implemented if this DependentResource can be created and/or updated"); } @SuppressWarnings("unused") - protected boolean isCreatable(P primary, Context context) { + protected boolean isCreatable(P primary, Context

context) { return creatable; } @SuppressWarnings("unused") - protected boolean isUpdatable(P primary, Context context) { + protected boolean isUpdatable(P primary, Context

context) { return updatable; } @SuppressWarnings("unused") - protected boolean isDeletable(P primary, Context context) { + protected boolean isDeletable(P primary, Context

context) { return deletable; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java index 223203b2b0..48af8fb61b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java @@ -5,5 +5,5 @@ @FunctionalInterface public interface Creator { - R create(R desired, P primary, Context context); + R create(R desired, P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java index b81ffc0380..0a1bfc1bab 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -5,5 +5,5 @@ @FunctionalInterface public interface Deleter

{ - void delete(P primary, Context context); + void delete(P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 239ac58d0b..8fc3cea73f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -6,9 +6,9 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; public interface DependentResource { - void reconcile(P primary, Context context); + void reconcile(P primary, Context

context); - default void cleanup(P primary, Context context) {} + default void cleanup(P primary, Context

context) {} Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java index 99470bf1d5..2cf8b155ff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java @@ -12,7 +12,7 @@ public DesiredEqualsMatcher(AbstractDependentResource abstractDependentRes } @Override - public Result match(R actualResource, P primary, Context context) { + public Result match(R actualResource, P primary, Context

context) { var desired = abstractDependentResource.desired(primary, context); return Result.computed(actualResource.equals(desired), desired); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java index f0c5bd7fd9..09e78911e6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java @@ -32,5 +32,5 @@ public Optional computedDesired() { } } - Result match(R actualResource, P primary, Context context); + Result match(R actualResource, P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java index 25e0567781..d49ccc632e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -5,7 +5,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; public interface Updater { - R update(R actual, R desired, P primary, Context context); + R update(R actual, R desired, P primary, Context

context); - Result match(R actualResource, P primary, Context context); + Result match(R actualResource, P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index a6c1979421..5c432bf95e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -36,21 +36,21 @@ @SuppressWarnings({"unchecked"}) @Ignore -public class Controller implements Reconciler, - LifecycleAware, EventSourceInitializer { +public class Controller

implements Reconciler

, + LifecycleAware, EventSourceInitializer

{ private static final Logger log = LoggerFactory.getLogger(Controller.class); - private final Reconciler reconciler; - private final ControllerConfiguration configuration; + private final Reconciler

reconciler; + private final ControllerConfiguration

configuration; private final KubernetesClient kubernetesClient; - private final EventSourceManager eventSourceManager; - private final DependentResourceManager dependents; + private final EventSourceManager

eventSourceManager; + private final DependentResourceManager

dependents; private ConfigurationService configurationService; - public Controller(Reconciler reconciler, - ControllerConfiguration configuration, + public Controller(Reconciler

reconciler, + ControllerConfiguration

configuration, KubernetesClient kubernetesClient) { this.reconciler = reconciler; this.configuration = configuration; @@ -61,7 +61,7 @@ public Controller(Reconciler reconciler, } @Override - public DeleteControl cleanup(R resource, Context context) { + public DeleteControl cleanup(P resource, Context

context) { dependents.cleanup(resource, context); return metrics().timeControllerExecution( @@ -89,7 +89,7 @@ public DeleteControl execute() { } @Override - public UpdateControl reconcile(R resource, Context context) { + public UpdateControl

reconcile(P resource, Context

context) { dependents.reconcile(resource, context); return metrics().timeControllerExecution( @@ -105,7 +105,7 @@ public String controllerName() { } @Override - public String successTypeName(UpdateControl result) { + public String successTypeName(UpdateControl

result) { String successType = "resource"; if (result.isUpdateStatus()) { successType = "status"; @@ -117,7 +117,7 @@ public String successTypeName(UpdateControl result) { } @Override - public UpdateControl execute() { + public UpdateControl

execute() { return reconciler.reconcile(resource, context); } }); @@ -130,13 +130,13 @@ private Metrics metrics() { } @Override - public List prepareEventSources(EventSourceContext context) { + public List prepareEventSources(EventSourceContext

context) { final var dependentSources = dependents.prepareEventSources(context); List sources = new LinkedList<>(dependentSources); // add manually defined event sources if (reconciler instanceof EventSourceInitializer) { - sources.addAll(((EventSourceInitializer) reconciler).prepareEventSources(context)); + sources.addAll(((EventSourceInitializer

) reconciler).prepareEventSources(context)); } return sources; } @@ -164,11 +164,11 @@ public String toString() { return "'" + configuration.getName() + "' Controller"; } - public Reconciler getReconciler() { + public Reconciler

getReconciler() { return reconciler; } - public ControllerConfiguration getConfiguration() { + public ControllerConfiguration

getConfiguration() { return configuration; } @@ -176,7 +176,7 @@ public KubernetesClient getClient() { return kubernetesClient; } - public MixedOperation, Resource> getCRClient() { + public MixedOperation, Resource

> getCRClient() { return kubernetesClient.resources(configuration.getResourceClass()); } @@ -189,7 +189,7 @@ public MixedOperation, Resource> getCRClient() { * @throws OperatorException if a problem occurred during the registration process */ public void start() throws OperatorException { - final Class resClass = configuration.getResourceClass(); + final Class

resClass = configuration.getResourceClass(); final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; @@ -215,8 +215,7 @@ public void start() throws OperatorException { } final var context = new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource(), - configurationService(), kubernetesClient); + eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); prepareEventSources(context).forEach(eventSourceManager::registerEventSource); @@ -273,7 +272,7 @@ private void failOnMissingCurrentNS() { } } - public EventSourceManager getEventSourceManager() { + public EventSourceManager

getEventSourceManager() { return eventSourceManager; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 554f019d81..3dd3a2860e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -63,14 +63,14 @@ public List prepareEventSources(EventSourceContext

context) { } @Override - public UpdateControl

reconcile(P resource, Context context) { + public UpdateControl

reconcile(P resource, Context

context) { initContextIfNeeded(resource, context); dependents.forEach(dependent -> dependent.reconcile(resource, context)); return UpdateControl.noUpdate(); } @Override - public DeleteControl cleanup(P resource, Context context) { + public DeleteControl cleanup(P resource, Context

context) { initContextIfNeeded(resource, context); dependents.forEach(dependent -> dependent.cleanup(resource, context)); return Reconciler.super.cleanup(resource, context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 564646b217..e7a0edd549 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -43,26 +43,26 @@ public Optional getResource(HasMetadata primaryResource) { public abstract Optional fetchResource(HasMetadata primaryResource); @Override - public void reconcile(P primary, Context context) { + public void reconcile(P primary, Context

context) { var resourceId = ResourceID.fromResource(primary); Optional resource = fetchResource(primary); resource.ifPresentOrElse(r -> cache.put(resourceId, r), () -> cache.remove(resourceId)); super.reconcile(primary, context); } - public void cleanup(P primary, Context context) { + public void cleanup(P primary, Context

context) { super.cleanup(primary, context); cache.remove(ResourceID.fromResource(primary)); } @Override - protected void handleCreate(R desired, P primary, Context context) { + protected void handleCreate(R desired, P primary, Context

context) { var res = this.creator.create(desired, primary, context); cache.put(ResourceID.fromResource(primary), res); } @Override - protected void handleUpdate(R actual, R desired, P primary, Context context) { + protected void handleUpdate(R actual, R desired, P primary, Context

context) { var res = updater.update(actual, desired, primary, context); cache.put(ResourceID.fromResource(primary), res); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 25d7c098d5..5bde08b0aa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -39,8 +39,9 @@ static Matcher matcherFor( } @Override - public Result match(R actualResource, P primary, Context context) { - final var objectMapper = context.getConfigurationService().getObjectMapper(); + public Result match(R actualResource, P primary, Context

context) { + final var objectMapper = + context.getControllerConfiguration().getConfigurationService().getObjectMapper(); final var desired = dependentResource.desired(primary, context); // reflection will be replaced by this: diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java index b33719f64e..ce73a409cd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -44,7 +44,8 @@ protected void updateClonedActual(R actual, R desired) { } public R replaceSpecOnActual(R actual, R desired, Context context) { - var clonedActual = context.getConfigurationService().getResourceCloner().clone(actual); + var clonedActual = context.getControllerConfiguration().getConfigurationService() + .getResourceCloner().clone(actual); updateClonedActual(clonedActual, desired); return clonedActual; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 5ff8bc6947..2bee0edf1b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -92,11 +92,11 @@ public void configureWith( this.addOwnerReference = addOwnerReference; } - public R create(R target, P primary, Context context) { + public R create(R target, P primary, Context

context) { return prepare(target, primary, "Creating").create(target); } - public R update(R actual, R target, P primary, Context context) { + public R update(R actual, R target, P primary, Context

context) { var updatedActual = processor.replaceSpecOnActual(actual, target, context); return prepare(target, primary, "Updating").replace(updatedActual); } @@ -105,7 +105,7 @@ public Result match(R actualResource, P primary, Context context) { return matcher.match(actualResource, primary, context); } - public void delete(P primary, Context context) { + public void delete(P primary, Context

context) { if (!addOwnerReference) { var resource = getResource(primary); resource.ifPresent(r -> client.resource(r).delete()); @@ -129,7 +129,8 @@ protected NonNamespaceOperation, Resource> prepa @Override public EventSource initEventSource(EventSourceContext

context) { if (informerEventSource == null) { - configureWith(context.getConfigurationService(), null, null, + configureWith(context.getControllerConfiguration().getConfigurationService(), null, + context.getControllerConfiguration().getNamespaces(), KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); log.warn("Using default configuration for " + resourceType().getSimpleName() + " KubernetesDependentResource, call configureWith to provide configuration"); @@ -159,7 +160,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { } @Override - protected R desired(P primary, Context context) { + protected R desired(P primary, Context

context) { return super.desired(primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 14ba12636a..2a14e8a69b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -163,11 +163,11 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { return createPostExecutionControl(updatedCustomResource, updateControl); } - private void handleErrorStatusHandler(R resource, Context context, + private void handleErrorStatusHandler(R resource, Context context, RuntimeException e) { if (isErrorStatusHandlerPresent()) { try { - var retryInfo = context.getRetryInfo().orElse(new RetryInfo() { + RetryInfo retryInfo = context.getRetryInfo().orElse(new RetryInfo() { @Override public int getAttemptCount() { return 0; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 3ae424cdfb..4621e4700e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,8 +23,10 @@ class GenericKubernetesResourceMatcherTest { private static final Context context = mock(Context.class); static { final var configurationService = mock(ConfigurationService.class); + final var controllerConfiguration = mock(ControllerConfiguration.class); when(configurationService.getObjectMapper()).thenReturn(new ObjectMapper()); - when(context.getConfigurationService()).thenReturn(configurationService); + when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); + when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index 941738ad7f..6c763d20fe 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; @@ -20,8 +21,10 @@ class GenericResourceUpdatePreProcessorTest { static { final var configurationService = mock(ConfigurationService.class); + final var controllerConfiguration = mock(ControllerConfiguration.class); + when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); when(configurationService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); - when(context.getConfigurationService()).thenReturn(configurationService); + when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } ResourceUpdatePreProcessor processor = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index f67909056d..f07d88db1e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -296,7 +296,7 @@ public boolean isLastAttempt() { ArgumentCaptor.forClass(Context.class); verify(reconciler, times(1)) .reconcile(any(), contextArgumentCaptor.capture()); - Context context = contextArgumentCaptor.getValue(); + Context context = contextArgumentCaptor.getValue(); final var retryInfo = context.getRetryInfo().get(); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); assertThat(retryInfo.isLastAttempt()).isEqualTo(true); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 0ce67d30c3..c3b8632e02 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -22,7 +22,8 @@ public class ErrorStatusHandlerTestReconciler @Override public UpdateControl reconcile( - ErrorStatusHandlerTestCustomResource resource, Context context) { + ErrorStatusHandlerTestCustomResource resource, + Context context) { var number = numberOfExecutions.addAndGet(1); var retryAttempt = -1; if (context.getRetryInfo().isPresent()) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index a76b5b0113..8ccd7c2160 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -31,7 +31,7 @@ public RetryTestCustomReconciler(int numberOfExecutionFails) { @Override public UpdateControl reconcile(RetryTestCustomResource resource, - Context context) { + Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 12b7f9506e..aa8689e471 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -85,7 +85,8 @@ private static class DeploymentDependentResource extends Updater { @Override - protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { + protected Deployment desired(StandaloneDependentTestCustomResource primary, + Context context) { Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 140047a3b4..eb66f918ab 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -35,7 +35,7 @@ public MySQLSchemaReconciler() {} @Override - public UpdateControl reconcile(MySQLSchema schema, Context context) { + public UpdateControl reconcile(MySQLSchema schema, Context context) { // we only need to update the status if we just built the schema, i.e. when it's present in the // context Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index f1fbc4b642..f5a5e9ea18 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -42,12 +42,12 @@ public void configureWith(ResourcePollerConfig config) { } @Override - public Schema desired(MySQLSchema primary, Context context) { + public Schema desired(MySQLSchema primary, Context context) { return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); } @Override - public Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { + public Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME)); @@ -70,7 +70,7 @@ private Connection getConnection() throws SQLException { } @Override - public void delete(MySQLSchema primary, Context context) { + public void delete(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; SchemaService.deleteSchemaAndRelatedUser(connection, primary.getMetadata().getName(), diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 416d7f3ee0..dfad4533e3 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -30,7 +30,7 @@ public class TomcatReconciler implements Reconciler { private final Logger log = LoggerFactory.getLogger(getClass()); @Override - public UpdateControl reconcile(Tomcat tomcat, Context context) { + public UpdateControl reconcile(Tomcat tomcat, Context context) { return context.getSecondaryResource(Deployment.class).map(deployment -> { Tomcat updatedTomcat = updateTomcatStatus(tomcat, deployment); log.info( diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 694ff07d02..58d17235ae 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -79,7 +79,7 @@ public List prepareEventSources(EventSourceContext context) * change. */ @Override - public UpdateControl reconcile(Webapp webapp, Context context) { + public UpdateControl reconcile(Webapp webapp, Context context) { if (webapp.getStatus() != null && Objects.equals(webapp.getSpec().getUrl(), webapp.getStatus().getDeployedArtifact())) { return UpdateControl.noUpdate(); @@ -120,7 +120,7 @@ public UpdateControl reconcile(Webapp webapp, Context context) { } @Override - public DeleteControl cleanup(Webapp webapp, Context context) { + public DeleteControl cleanup(Webapp webapp, Context context) { String[] command = new String[] {"rm", "/data/" + webapp.getSpec().getContextPath() + ".war"}; String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 90fb61251f..ef0c2f5d17 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -56,7 +56,7 @@ public List prepareEventSources(EventSourceContext context } @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + public UpdateControl reconcile(WebPage webPage, Context context) { log.info("Reconciling web page: {}", webPage); if (webPage.getSpec().getHtml().contains("error")) { throw new ErrorSimulationException("Simulating error"); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index 08a02e8a2c..00a0d50d3b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -128,7 +128,7 @@ protected Class resourceType() { new CrudKubernetesDependentResource<>() { @Override - protected Service desired(WebPage webPage, Context context) { + protected Service desired(WebPage webPage, Context context) { Service service = loadYaml(Service.class, getClass(), "service.yaml"); service.getMetadata().setName(serviceName(webPage)); service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); From dc0b6218aacd2464fa6e65112c1086a8233828c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 09:25:10 +0100 Subject: [PATCH 0066/1319] fix: dependent resource cache iller and filter issue (#1018) --- .../dependent/AbstractDependentResource.java | 38 ++++++++++++------- .../StandaloneDependentResourceIT.java | 6 ++- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 3e4414a009..95b82d50ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -14,22 +14,12 @@ public abstract class AbstractDependentResource private final boolean creatable = this instanceof Creator; private final boolean updatable = this instanceof Updater; private final boolean deletable = this instanceof Deleter; - private final boolean filteringEventSource; - private final boolean cachingEventSource; protected Creator creator; protected Updater updater; protected Deleter

deleter; @SuppressWarnings("unchecked") public AbstractDependentResource() { - if (this instanceof EventSourceProvider) { - final var eventSource = ((EventSourceProvider

) this).getEventSource(); - filteringEventSource = eventSource instanceof RecentOperationEventFilter; - cachingEventSource = eventSource instanceof RecentOperationCacheFiller; - } else { - filteringEventSource = false; - cachingEventSource = false; - } creator = creatable ? (Creator) this : null; updater = updatable ? (Updater) this : null; deleter = deletable ? (Deleter

) this : null; @@ -81,27 +71,27 @@ protected void handleCreate(R desired, P primary, Context

context) { } private void cleanupAfterEventFiltering(R desired, ResourceID resourceID, R created) { - if (filteringEventSource) { + if (isFilteringEventSource()) { eventSourceAsRecentOperationEventFilter() .cleanupOnCreateOrUpdateEventFiltering(resourceID, created); } } private void cacheAfterCreate(ResourceID resourceID, R created) { - if (cachingEventSource) { + if (isRecentOperationCacheFiller()) { eventSourceAsRecentOperationCacheFiller().handleRecentResourceCreate(resourceID, created); } } private void cacheAfterUpdate(R actual, ResourceID resourceID, R updated) { - if (cachingEventSource) { + if (isRecentOperationCacheFiller()) { eventSourceAsRecentOperationCacheFiller().handleRecentResourceUpdate(resourceID, updated, actual); } } private void prepareEventFiltering(R desired, ResourceID resourceID) { - if (filteringEventSource) { + if (isFilteringEventSource()) { eventSourceAsRecentOperationEventFilter().prepareForCreateOrUpdateEventFiltering(resourceID, desired); } @@ -130,6 +120,26 @@ private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() return (RecentOperationCacheFiller) ((EventSourceProvider

) this).getEventSource(); } + // this cannot be done in constructor since event source might be initialized later + protected boolean isFilteringEventSource() { + if (this instanceof EventSourceProvider) { + final var eventSource = ((EventSourceProvider

) this).getEventSource(); + return eventSource instanceof RecentOperationEventFilter; + } else { + return false; + } + } + + // this cannot be done in constructor since event source might be initialized later + protected boolean isRecentOperationCacheFiller() { + if (this instanceof EventSourceProvider) { + final var eventSource = ((EventSourceProvider

) this).getEventSource(); + return eventSource instanceof RecentOperationCacheFiller; + } else { + return false; + } + } + @Override public void cleanup(P primary, Context

context) { if (isDeletable(primary, context)) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index d86385f1f1..b7d88159e5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; @@ -53,8 +54,9 @@ void executeUpdateForTestingCacheUpdateForGetResource() { awaitForDeploymentReadyReplicas(1); - createdCR.getSpec().setReplicaCount(2); - operator.replace(StandaloneDependentTestCustomResource.class, createdCR); + var clonedCr = ConfigurationService.DEFAULT_CLONER.clone(createdCR); + clonedCr.getSpec().setReplicaCount(2); + operator.replace(StandaloneDependentTestCustomResource.class, clonedCr); awaitForDeploymentReadyReplicas(2); assertThat( From bd32e8782c20cd0bf02cc52209abf381d84005dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 10:14:30 +0100 Subject: [PATCH 0067/1319] ci: don't re run failing tests (#1019) --- pom.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pom.xml b/pom.xml index e25e5a1d41..7cc3ca78c6 100644 --- a/pom.xml +++ b/pom.xml @@ -206,9 +206,6 @@ org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} - - 3 - org.apache.maven.plugins From 0c4f0a5cbe59c35cd87df54709124d55704d8ec7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 11 Mar 2022 12:28:33 +0100 Subject: [PATCH 0068/1319] refactor: getDependentResources return type is more generic (#1023) --- .../config/AnnotationControllerConfiguration.java | 15 ++++++--------- .../api/config/ControllerConfiguration.java | 3 +-- .../config/ControllerConfigurationOverrider.java | 4 ++-- .../config/DefaultControllerConfiguration.java | 6 +++--- .../config/dependent/DependentResourceSpec.java | 4 ---- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 5fd1f3dbfd..a3241edf04 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -21,14 +21,13 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -@SuppressWarnings("rawtypes") public class AnnotationControllerConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { protected final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; - private List specs; + private List> specs; public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -147,7 +146,7 @@ public static T valueOrDefault( @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public List getDependentResources() { + public List> getDependentResources() { if (specs == null) { final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); @@ -155,6 +154,7 @@ public List getDependentResources() { return Collections.emptyList(); } + Object config = null; specs = new ArrayList<>(dependents.length); for (Dependent dependent : dependents) { final Class dependentType = dependent.type(); @@ -172,13 +172,10 @@ public List getDependentResources() { kubeDependent, KubernetesDependent::addOwnerReference, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - KubernetesDependentResourceConfig config = - new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); - specs.add(new DependentResourceSpec(dependentType, config)); - } else { - specs.add(new DependentResourceSpec(dependentType)); + config = new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); } + specs.add(new DependentResourceSpec(dependentType, config)); } } return specs; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index d269572ee7..5ebcaf6ed4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -12,7 +12,6 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -@SuppressWarnings("rawtypes") public interface ControllerConfiguration extends ResourceConfiguration { default String getName() { @@ -52,7 +51,7 @@ default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } - default List getDependentResources() { + default List> getDependentResources() { return Collections.emptyList(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index 239265af12..a4d194b876 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -22,7 +22,7 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; - private final List dependentResourceSpecs; + private final List> dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -115,7 +115,7 @@ public void addNewDependentResourceConfig(DependentResourceSpec dependentResourc dependentResourceSpecs.add(dependentResourceSpec); } - private Optional findConfigForDependentResourceClass( + private Optional> findConfigForDependentResourceClass( Class dependentResourceClass) { return dependentResourceSpecs.stream() .filter(dc -> dc.getDependentResourceClass().equals(dependentResourceClass)) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 8ce59dd01e..bba8c1ba12 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -21,7 +21,7 @@ public class DefaultControllerConfiguration private final boolean generationAware; private final RetryConfiguration retryConfiguration; private final ResourceEventFilter resourceEventFilter; - private final List dependents; + private final List> dependents; private final Duration reconciliationMaxInterval; // NOSONAR constructor is meant to provide all information @@ -38,7 +38,7 @@ public DefaultControllerConfiguration( Class resourceClass, Duration reconciliationMaxInterval, ConfigurationService service, - List dependents) { + List> dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -102,7 +102,7 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { + public List> getDependentResources() { return dependents; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java index 1c07a5c752..0684c04bef 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -10,10 +10,6 @@ public class DependentResourceSpec, C> { private final C dependentResourceConfig; - public DependentResourceSpec(Class dependentResourceClass) { - this(dependentResourceClass, null); - } - public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig) { this.dependentResourceClass = dependentResourceClass; this.dependentResourceConfig = dependentResourceConfig; From 13a6c8362d201664ae5afe2a523f69b50ef1570b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 11 Mar 2022 16:03:45 +0100 Subject: [PATCH 0069/1319] feat: retrieve ConfigurationService from ConfigurationServiceProvider (#1010) --- .../io/javaoperatorsdk/operator/Operator.java | 43 +++-- .../config/AbstractConfigurationService.java | 3 +- .../AnnotationControllerConfiguration.java | 15 +- .../config/ConfigurationServiceOverrider.java | 29 +-- .../config/ConfigurationServiceProvider.java | 67 +++++++ .../api/config/ControllerConfiguration.java | 4 + .../ControllerConfigurationOverrider.java | 1 - .../DefaultControllerConfiguration.java | 13 +- .../config/DefaultResourceConfiguration.java | 11 -- .../api/config/ExecutorServiceManager.java | 8 +- .../api/config/ResourceConfiguration.java | 12 +- .../informer/InformerConfiguration.java | 35 ++-- .../api/reconciler/DefaultContext.java | 2 +- .../dependent/ResourceUpdatePreProcessor.java | 2 +- .../operator/processing/Controller.java | 28 +-- .../GenericKubernetesResourceMatcher.java | 4 +- .../GenericResourceUpdatePreProcessor.java | 6 +- .../KubernetesDependentResource.java | 14 +- .../KubernetesDependentResourceConfig.java | 16 +- .../processing/event/EventProcessor.java | 18 +- .../event/ReconciliationDispatcher.java | 10 +- .../source/informer/InformerManager.java | 5 +- .../operator/ControllerManagerTest.java | 2 +- .../operator/OperatorTest.java | 55 +++--- .../ConfigurationServiceProviderTest.java | 62 +++++++ .../config/ControllerConfigurationTest.java | 8 - .../operator/processing/ControllerTest.java | 24 ++- .../GenericKubernetesResourceMatcherTest.java | 12 +- ...GenericResourceUpdatePreProcessorTest.java | 9 +- .../event/ReconciliationDispatcherTest.java | 168 +++++++++++------- .../source/CustomResourceSelectorTest.java | 55 ++---- .../event/source/ResourceEventFilterTest.java | 3 +- .../ControllerResourceEventSourceTest.java | 7 +- .../sample/simple/TestCustomResourceSpec.java | 20 +++ .../simple/TestCustomResourceStatus.java | 19 ++ .../junit/AbstractOperatorExtension.java | 5 +- .../runtime/DefaultConfigurationService.java | 8 +- .../operator/ConcurrencyIT.java | 6 +- .../operator/ControllerExecutionIT.java | 6 +- .../CreateUpdateDependentEventFilterIT.java | 2 - .../operator/CustomResourceFilterIT.java | 6 +- .../operator/ErrorStatusHandlerIT.java | 2 - .../operator/EventSourceIT.java | 6 +- .../operator/InformerEventSourceIT.java | 2 - .../KubernetesResourceStatusUpdateIT.java | 6 +- .../operator/MaxIntervalIT.java | 6 +- .../operator/MultiVersionCRDIT.java | 2 - .../ObservedGenerationHandlingIT.java | 6 +- .../io/javaoperatorsdk/operator/RetryIT.java | 2 - .../operator/RetryMaxAttemptIT.java | 2 - .../StandaloneDependentResourceIT.java | 6 +- .../operator/SubResourceUpdateIT.java | 6 +- .../operator/UpdatingResAndSubResIT.java | 6 +- .../DefaultConfigurationServiceTest.java | 13 +- .../operator/sample/MySQLSchemaOperator.java | 6 +- .../sample/MySQLSchemaOperatorE2E.java | 6 +- .../operator/sample/TomcatOperator.java | 3 +- .../operator/sample/TomcatOperatorE2E.java | 3 - .../operator/sample/WebPageOperator.java | 3 +- .../WebPageOperatorDependentResourcesE2E.java | 3 - .../operator/sample/WebPageOperatorE2E.java | 3 - .../sample/PureJavaApplicationRunner.java | 9 +- .../operator/sample/Config.java | 4 +- 63 files changed, 471 insertions(+), 457 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index adc3410405..bcd0d7ade0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -14,6 +14,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; @@ -25,14 +27,33 @@ public class Operator implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient kubernetesClient; - private final ConfigurationService configurationService; private final ControllerManager controllers = new ControllerManager(); + public Operator() { + this(new DefaultKubernetesClient(), ConfigurationServiceProvider.instance()); + } + + public Operator(KubernetesClient kubernetesClient) { + this(kubernetesClient, ConfigurationServiceProvider.instance()); + } + /** + * @deprecated Use {@link #Operator(Consumer)} instead + */ + @Deprecated public Operator(ConfigurationService configurationService) { this(new DefaultKubernetesClient(), configurationService); } + public Operator(Consumer overrider) { + this(new DefaultKubernetesClient(), overrider); + } + + public Operator(KubernetesClient client, Consumer overrider) { + this(client); + ConfigurationServiceProvider.overrideCurrent(overrider); + } + /** * Note that Operator by default closes the client on stop, this can be changed using * {@link ConfigurationService} @@ -42,7 +63,7 @@ public Operator(ConfigurationService configurationService) { */ public Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService) { this.kubernetesClient = kubernetesClient; - this.configurationService = configurationService; + ConfigurationServiceProvider.set(configurationService); } /** Adds a shutdown hook that automatically calls {@link #stop()} ()} when the app shuts down. */ @@ -54,10 +75,6 @@ public KubernetesClient getKubernetesClient() { return kubernetesClient; } - public ConfigurationService getConfigurationService() { - return configurationService; - } - public List getControllers() { return new ArrayList<>(controllers.controllers.values()); } @@ -70,7 +87,7 @@ public List getControllers() { public void start() { controllers.shouldStart(); - final var version = configurationService.getVersion(); + final var version = ConfigurationServiceProvider.instance().getVersion(); log.info( "Operator SDK {} (commit: {}) built on {} starting...", version.getSdkVersion(), @@ -80,12 +97,13 @@ public void start() { final var clientVersion = Version.clientVersion(); log.info("Client version: {}", clientVersion); - ExecutorServiceManager.init(configurationService); + ExecutorServiceManager.init(); controllers.start(); } @Override public void stop() throws OperatorException { + final var configurationService = ConfigurationServiceProvider.instance(); log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); @@ -107,7 +125,8 @@ public void stop() throws OperatorException { */ public void register(Reconciler reconciler) throws OperatorException { - final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + final var controllerConfiguration = + ConfigurationServiceProvider.instance().getConfigurationFor(reconciler); register(reconciler, controllerConfiguration); } @@ -132,7 +151,8 @@ public void register(Reconciler reconciler, "Cannot register reconciler with name " + reconciler.getClass().getCanonicalName() + " reconciler named " + ReconcilerUtils.getNameFor(reconciler) + " because its configuration cannot be found.\n" + - " Known reconcilers are: " + configurationService.getKnownReconcilerNames()); + " Known reconcilers are: " + + ConfigurationServiceProvider.instance().getKnownReconcilerNames()); } final var controller = new Controller<>(reconciler, configuration, kubernetesClient); @@ -158,7 +178,8 @@ public void register(Reconciler reconciler, */ public void register(Reconciler reconciler, Consumer> configOverrider) { - final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + final var controllerConfiguration = + ConfigurationServiceProvider.instance().getConfigurationFor(reconciler); var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration); configOverrider.accept(configToOverride); register(reconciler, configToOverride.build()); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 48c6b73f2b..2c9e01551e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -26,6 +26,7 @@ protected void replace(ControllerConfiguration config put(config, false); } + @SuppressWarnings("unchecked") private void put( ControllerConfiguration config, boolean failIfExisting) { final var name = config.getName(); @@ -36,7 +37,6 @@ private void put( } } configurations.put(name, config); - config.setConfigurationService(this); } protected void throwExceptionOnNameCollision( @@ -50,6 +50,7 @@ protected void throwExceptionOnNameCollision( + newReconcilerClassName); } + @SuppressWarnings("unchecked") @Override public ControllerConfiguration getConfigurationFor( Reconciler reconciler) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index a3241edf04..72d34e16a8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -26,7 +26,6 @@ public class AnnotationControllerConfiguration protected final Reconciler reconciler; private final ControllerConfiguration annotation; - private ConfigurationService service; private List> specs; public AnnotationControllerConfiguration(Reconciler reconciler) { @@ -77,16 +76,6 @@ public String getLabelSelector() { return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); } - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } - @Override public String getAssociatedReconcilerClassName() { return reconciler.getClass().getCanonicalName(); @@ -172,8 +161,8 @@ public static T valueOrDefault( kubeDependent, KubernetesDependent::addOwnerReference, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - config = new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); + config = + new KubernetesDependentResourceConfig(addOwnerReference, namespaces, labelSelector); } specs.add(new DependentResourceSpec(dependentType, config)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index e2251eea64..9beb583d76 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -2,12 +2,12 @@ import java.util.Set; import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; -import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +@SuppressWarnings("unused") public class ConfigurationServiceOverrider { private final ConfigurationService original; private Metrics metrics; @@ -19,8 +19,7 @@ public class ConfigurationServiceOverrider { private boolean closeClientOnStop; private ExecutorService executorService = null; - public ConfigurationServiceOverrider( - ConfigurationService original) { + ConfigurationServiceOverrider(ConfigurationService original) { this.original = original; this.clientConfig = original.getClientConfiguration(); this.checkCR = original.checkCRDAndValidateLocalModel(); @@ -73,26 +72,12 @@ public ConfigurationServiceOverrider withExecutorService(ExecutorService executo } public ConfigurationService build() { - return new ConfigurationService() { - @Override - public ControllerConfiguration getConfigurationFor( - Reconciler reconciler) { - ControllerConfiguration controllerConfiguration = - original.getConfigurationFor(reconciler); - controllerConfiguration.setConfigurationService(this); - return controllerConfiguration; - } - + return new BaseConfigurationService(original.getVersion()) { @Override public Set getKnownReconcilerNames() { return original.getKnownReconcilerNames(); } - @Override - public Version getVersion() { - return original.getVersion(); - } - @Override public Config getClientConfiguration() { return clientConfig; @@ -133,12 +118,16 @@ public ExecutorService getExecutorService() { if (executorService != null) { return executorService; } else { - return ConfigurationService.super.getExecutorService(); + return super.getExecutorService(); } } }; } + /** + * @deprecated Use {@link ConfigurationServiceProvider#overrideCurrent(Consumer)} instead + */ + @Deprecated(since = "2.2.0") public static ConfigurationServiceOverrider override(ConfigurationService original) { return new ConfigurationServiceOverrider(original); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java new file mode 100644 index 0000000000..1e6dd660cf --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java @@ -0,0 +1,67 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.function.Consumer; + +public class ConfigurationServiceProvider { + static final ConfigurationService DEFAULT = + new BaseConfigurationService(Utils.loadFromProperties()); + private static ConfigurationService instance; + private static ConfigurationService defaultConfigurationService = DEFAULT; + private static boolean alreadyConfigured = false; + + private ConfigurationServiceProvider() {} + + public synchronized static ConfigurationService instance() { + if (instance == null) { + set(defaultConfigurationService); + } + return instance; + } + + public synchronized static void set(ConfigurationService instance) { + set(instance, false); + } + + private static void set(ConfigurationService instance, boolean overriding) { + final var current = ConfigurationServiceProvider.instance; + if (!overriding) { + if (current != null && !current.equals(instance)) { + throw new IllegalStateException( + "A ConfigurationService has already been set and cannot be set again. Current: " + + current.getClass().getCanonicalName()); + } + } else { + if (alreadyConfigured) { + throw new IllegalStateException( + "The ConfigurationService has already been overridden once and cannot be changed again. Current: " + + current.getClass().getCanonicalName()); + } else { + alreadyConfigured = true; + } + } + + ConfigurationServiceProvider.instance = instance; + } + + public synchronized static void overrideCurrent( + Consumer overrider) { + final var toOverride = + new ConfigurationServiceOverrider(ConfigurationServiceProvider.instance()); + overrider.accept(toOverride); + ConfigurationServiceProvider.set(toOverride.build(), true); + } + + public synchronized static void setDefault(ConfigurationService defaultConfigurationService) { + ConfigurationServiceProvider.defaultConfigurationService = defaultConfigurationService; + } + + synchronized static ConfigurationService getDefault() { + return defaultConfigurationService; + } + + public synchronized static void reset() { + defaultConfigurationService = DEFAULT; + instance = null; + alreadyConfigured = false; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 5ebcaf6ed4..03485412b6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -58,4 +58,8 @@ default ResourceEventFilter getEventFilter() { default Optional reconciliationMaxInterval() { return Optional.of(Duration.ofHours(10L)); } + + default ConfigurationService getConfigurationService() { + return ConfigurationServiceProvider.instance(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index a4d194b876..87297b8d49 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -135,7 +135,6 @@ public ControllerConfiguration build() { customResourcePredicate, original.getResourceClass(), reconciliationMaxInterval, - original.getConfigurationService(), dependentResourceSpecs); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index bba8c1ba12..4545c9885e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -10,6 +10,7 @@ import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +@SuppressWarnings("rawtypes") public class DefaultControllerConfiguration extends DefaultResourceConfiguration implements ControllerConfiguration { @@ -37,7 +38,6 @@ public DefaultControllerConfiguration( ResourceEventFilter resourceEventFilter, Class resourceClass, Duration reconciliationMaxInterval, - ConfigurationService service, List> dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; @@ -52,7 +52,6 @@ public DefaultControllerConfiguration( : retryConfiguration; this.resourceEventFilter = resourceEventFilter; - setConfigurationService(service); this.dependents = dependents != null ? dependents : Collections.emptyList(); } @@ -86,16 +85,6 @@ public RetryConfiguration getRetryConfiguration() { return retryConfiguration; } - - @Override - public void setConfigurationService(ConfigurationService service) { - if (getConfigurationService() != null) { - throw new IllegalStateException("A ConfigurationService is already associated with '" + name - + "' ControllerConfiguration. Cannot change it once set!"); - } - super.setConfigurationService(service); - } - @Override public ResourceEventFilter getEventFilter() { return resourceEventFilter; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java index d7b5f76815..4959b827ad 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -12,7 +12,6 @@ public class DefaultResourceConfiguration private final Set namespaces; private final boolean watchAllNamespaces; private final Class resourceClass; - private ConfigurationService service; public DefaultResourceConfiguration(String labelSelector, Class resourceClass, String... namespaces) { @@ -48,18 +47,8 @@ public boolean watchAllNamespaces() { return watchAllNamespaces; } - @Override - public ConfigurationService getConfigurationService() { - return service; - } - @Override public Class getResourceClass() { return resourceClass; } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index 254a539ac3..3442466b5c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -25,11 +25,9 @@ private ExecutorServiceManager(InstrumentedExecutorService executor, this.terminationTimeoutSeconds = terminationTimeoutSeconds; } - public static void init(ConfigurationService configuration) { + public static void init() { if (instance == null) { - if (configuration == null) { - configuration = new BaseConfigurationService(Version.UNKNOWN); - } + final var configuration = ConfigurationServiceProvider.instance(); instance = new ExecutorServiceManager( new InstrumentedExecutorService(configuration.getExecutorService()), configuration.getTerminationTimeoutSeconds()); @@ -56,7 +54,7 @@ public static void stop() { public static ExecutorServiceManager instance() { if (instance == null) { // provide a default configuration if none has been provided by init - init(null); + init(); } return instance; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java index 3ebc3ae546..2d947050e7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -63,12 +63,8 @@ static boolean currentNamespaceWatched(Set namespaces) { default Set getEffectiveNamespaces() { var targetNamespaces = getNamespaces(); if (watchCurrentNamespace()) { - final var parent = getConfigurationService(); - if (parent == null) { - throw new IllegalStateException( - "Parent ConfigurationService must be set before calling this method"); - } - String namespace = parent.getClientConfiguration().getNamespace(); + final String namespace = + ConfigurationServiceProvider.instance().getClientConfiguration().getNamespace(); if (namespace == null) { throw new OperatorException( "Couldn't retrieve the currently connected namespace. Make sure it's correctly set in your ~/.kube/config file, using, e.g. 'kubectl config set-context --namespace='"); @@ -77,8 +73,4 @@ default Set getEffectiveNamespaces() { } return targetNamespaces; } - - ConfigurationService getConfigurationService(); - - void setConfigurationService(ConfigurationService service); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 50f21629b8..55c8216e96 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -5,7 +5,6 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; @@ -14,6 +13,7 @@ import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +@SuppressWarnings("rawtypes") public interface InformerConfiguration extends ResourceConfiguration { @@ -23,13 +23,12 @@ class DefaultInformerConfiguration private final SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; private final PrimaryToSecondaryMapper

associatedWith; - protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, + protected DefaultInformerConfiguration(String labelSelector, Class resourceClass, SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet, PrimaryToSecondaryMapper

associatedWith, Set namespaces) { super(labelSelector, resourceClass, namespaces); - setConfigurationService(service); this.secondaryToPrimaryResourcesIdSet = Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, Mappers.fromOwnerReference()); @@ -52,6 +51,7 @@ public PrimaryToSecondaryMapper

getAssociatedResourceIdentifier() { PrimaryToSecondaryMapper

getAssociatedResourceIdentifier(); + @SuppressWarnings("unused") class InformerConfigurationBuilder { private SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; @@ -59,12 +59,9 @@ class InformerConfigurationBuilder private Set namespaces; private String labelSelector; private final Class resourceClass; - private final ConfigurationService configurationService; - private InformerConfigurationBuilder(Class resourceClass, - ConfigurationService configurationService) { + private InformerConfigurationBuilder(Class resourceClass) { this.resourceClass = resourceClass; - this.configurationService = configurationService; } public InformerConfigurationBuilder withPrimaryResourcesRetriever( @@ -97,7 +94,7 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector } public InformerConfiguration build() { - return new DefaultInformerConfiguration<>(configurationService, labelSelector, resourceClass, + return new DefaultInformerConfiguration<>(labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, namespaces); } @@ -105,23 +102,21 @@ public InformerConfiguration build() { static InformerConfigurationBuilder from( EventSourceContext

context, Class resourceClass) { - return new InformerConfigurationBuilder<>(resourceClass, context.getControllerConfiguration() - .getConfigurationService()); + return new InformerConfigurationBuilder<>(resourceClass); } - static InformerConfigurationBuilder from(ConfigurationService configurationService, - Class resourceClass) { - return new InformerConfigurationBuilder<>(resourceClass, configurationService); + @SuppressWarnings({"rawtypes", "unchecked"}) + static InformerConfigurationBuilder from(Class resourceClass) { + return new InformerConfigurationBuilder<>(resourceClass); } static InformerConfigurationBuilder from( InformerConfiguration configuration) { - return new InformerConfigurationBuilder(configuration.getResourceClass(), - configuration.getConfigurationService()) - .withNamespaces(configuration.getNamespaces()) - .withLabelSelector(configuration.getLabelSelector()) - .withAssociatedSecondaryResourceIdentifier( - configuration.getAssociatedResourceIdentifier()) - .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + return new InformerConfigurationBuilder(configuration.getResourceClass()) + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 96084ffd1f..d3a5b7bc18 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -13,7 +13,7 @@ public class DefaultContext

implements Context

{ private final Controller

controller; private final P primaryResource; private final ControllerConfiguration

controllerConfiguration; - private ManagedDependentResourceContext managedDependentResourceContext; + private final ManagedDependentResourceContext managedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java index df2541aae9..55ebcca230 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java @@ -5,5 +5,5 @@ public interface ResourceUpdatePreProcessor { - R replaceSpecOnActual(R actual, R desired, Context context); + R replaceSpecOnActual(R actual, R desired, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 5c432bf95e..d769324be9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -16,10 +16,8 @@ import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -47,8 +45,6 @@ public class Controller

implements Reconciler

, private final EventSourceManager

eventSourceManager; private final DependentResourceManager

dependents; - private ConfigurationService configurationService; - public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, KubernetesClient kubernetesClient) { @@ -125,7 +121,7 @@ public UpdateControl

execute() { private Metrics metrics() { - final var metrics = configurationService().getMetrics(); + final var metrics = ConfigurationServiceProvider.instance().getMetrics(); return metrics != null ? metrics : Metrics.NOOP; } @@ -202,7 +198,7 @@ public void start() throws OperatorException { try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configurationService().checkCRDAndValidateLocalModel() + if (ConfigurationServiceProvider.instance().checkCRDAndValidateLocalModel() && CustomResource.class.isAssignableFrom(resClass)) { crd = kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) .get(); @@ -227,23 +223,6 @@ public void start() throws OperatorException { } } - private ConfigurationService configurationService() { - if (configurationService == null) { - configurationService = configuration.getConfigurationService(); - // make sure we always have a default configuration service - if (configurationService == null) { - // we shouldn't need to register the configuration with the default service - configurationService = new BaseConfigurationService(Version.UNKNOWN) { - @Override - public boolean checkCRDAndValidateLocalModel() { - return false; - } - }; - } - } - return configurationService; - } - private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { throw new MissingCRDException( crdName, @@ -282,6 +261,7 @@ public void stop() { } } + @SuppressWarnings("rawtypes") public List getDependents() { return dependents.getDependents(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 5bde08b0aa..0bbe19837e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.zjsonpatch.JsonDiff; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; @@ -40,8 +41,7 @@ static Matcher matcherFor( @Override public Result match(R actualResource, P primary, Context

context) { - final var objectMapper = - context.getControllerConfiguration().getConfigurationService().getObjectMapper(); + final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper(); final var desired = dependentResource.desired(primary, context); // reflection will be replaced by this: diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java index ce73a409cd..0e0624d183 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; @@ -43,9 +44,8 @@ protected void updateClonedActual(R actual, R desired) { } } - public R replaceSpecOnActual(R actual, R desired, Context context) { - var clonedActual = context.getControllerConfiguration().getConfigurationService() - .getResourceCloner().clone(actual); + public R replaceSpecOnActual(R actual, R desired, Context context) { + var clonedActual = ConfigurationServiceProvider.instance().getResourceCloner().clone(actual); updateClonedActual(clonedActual, desired); return clonedActual; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 2bee0edf1b..a471df8c09 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -55,13 +54,12 @@ public KubernetesDependentResource() { @Override public void configureWith(KubernetesDependentResourceConfig config) { - configureWith(config.getConfigurationService(), config.labelSelector(), - Set.of(config.namespaces()), config.addOwnerReference()); + configureWith(config.labelSelector(), Set.of(config.namespaces()), config.addOwnerReference()); } @SuppressWarnings("unchecked") - private void configureWith(ConfigurationService configService, String labelSelector, - Set namespaces, boolean addOwnerReference) { + private void configureWith(String labelSelector, Set namespaces, + boolean addOwnerReference) { final var primaryResourcesRetriever = (this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper) this : Mappers.fromOwnerReference(); @@ -70,7 +68,7 @@ private void configureWith(ConfigurationService configService, String labelSelec ? (PrimaryToSecondaryMapper

) this : ResourceID::fromResource; InformerConfiguration ic = - InformerConfiguration.from(configService, resourceType()) + InformerConfiguration.from(resourceType()) .withLabelSelector(labelSelector) .withNamespaces(namespaces) .withPrimaryResourcesRetriever(primaryResourcesRetriever) @@ -129,9 +127,7 @@ protected NonNamespaceOperation, Resource> prepa @Override public EventSource initEventSource(EventSourceContext

context) { if (informerEventSource == null) { - configureWith(context.getControllerConfiguration().getConfigurationService(), null, - context.getControllerConfiguration().getNamespaces(), - KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + configureWith(null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); log.warn("Using default configuration for " + resourceType().getSimpleName() + " KubernetesDependentResource, call configureWith to provide configuration"); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java index 4464f3fd7d..3a7bd45025 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; - import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT; @@ -10,16 +8,14 @@ public class KubernetesDependentResourceConfig { private boolean addOwnerReference = ADD_OWNER_REFERENCE_DEFAULT; private String[] namespaces = new String[0]; private String labelSelector = EMPTY_STRING; - private ConfigurationService configurationService; public KubernetesDependentResourceConfig() {} public KubernetesDependentResourceConfig(boolean addOwnerReference, String[] namespaces, - String labelSelector, ConfigurationService configurationService) { + String labelSelector) { this.addOwnerReference = addOwnerReference; this.namespaces = namespaces; this.labelSelector = labelSelector; - this.configurationService = configurationService; } public KubernetesDependentResourceConfig setAddOwnerReference( @@ -38,12 +34,6 @@ public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) return this; } - public KubernetesDependentResourceConfig setConfigurationService( - ConfigurationService configurationService) { - this.configurationService = configurationService; - return this; - } - public boolean addOwnerReference() { return addOwnerReference; } @@ -55,8 +45,4 @@ public String[] namespaces() { public String labelSelector() { return labelSelector; } - - public ConfigurationService getConfigurationService() { - return configurationService; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 7f4cae69a4..e44df906d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -15,6 +15,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; @@ -55,13 +56,7 @@ class EventProcessor implements EventHandler, LifecycleAw new ReconciliationDispatcher<>(eventSourceManager.getController()), GenericRetry.fromConfiguration( eventSourceManager.getController().getConfiguration().getRetryConfiguration()), - eventSourceManager.getController().getConfiguration().getConfigurationService() == null - ? Metrics.NOOP - : eventSourceManager - .getController() - .getConfiguration() - .getConfigurationService() - .getMetrics(), + ConfigurationServiceProvider.instance().getMetrics(), eventSourceManager); } @@ -209,11 +204,10 @@ void eventProcessingFinished( if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { - postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { - eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( - ResourceID.fromResource(r), r, - executionScope.getResource()); - }); + postExecutionControl.getUpdatedCustomResource().ifPresent( + r -> eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( + ResourceID.fromResource(r), r, + executionScope.getResource())); if (eventMarker.eventPresent(resourceID)) { submitReconciliationExecution(resourceID); } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 2a14e8a69b..088592b3a4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -9,7 +9,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.BaseControl; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -124,9 +124,8 @@ private PostExecutionControl handleReconcile( private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { - final var configurationService = configuration().getConfigurationService(); - return configurationService != null ? configurationService.getResourceCloner().clone(resource) - : ConfigurationService.DEFAULT_CLONER.clone(resource); + final var cloner = ConfigurationServiceProvider.instance().getResourceCloner(); + return cloner.clone(resource); } else { return resource; } @@ -163,6 +162,7 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { return createPostExecutionControl(updatedCustomResource, updateControl); } + @SuppressWarnings("unchecked") private void handleErrorStatusHandler(R resource, Context context, RuntimeException e) { if (isErrorStatusHandlerPresent()) { @@ -301,7 +301,7 @@ private R replace(R resource) { return customResourceFacade.replaceWithLock(resource); } - private ControllerConfiguration configuration() { + ControllerConfiguration configuration() { return controller.getConfiguration(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 33f962e7ed..a50eb34441 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -17,7 +17,7 @@ import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -41,8 +41,7 @@ public void start() throws OperatorException { void initSources(MixedOperation, Resource> client, C configuration, ResourceEventHandler eventHandler) { - final var service = configuration.getConfigurationService(); - cloner = service == null ? ConfigurationService.DEFAULT_CLONER : service.getResourceCloner(); + cloner = ConfigurationServiceProvider.instance().getResourceCloner(); final var targetNamespaces = configuration.getEffectiveNamespaces(); final var labelSelector = configuration.getLabelSelector(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index 910492a47f..ca02094d46 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -63,7 +63,7 @@ private static class TestControllerConfiguration public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, - null, null, null); + null, null); this.controller = controller; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index 99bd7e64b9..f4de6b2141 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -1,14 +1,16 @@ package io.javaoperatorsdk.operator; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.AbstractConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -17,27 +19,29 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings("rawtypes") class OperatorTest { - private final KubernetesClient kubernetesClient = - MockKubernetesClient.client(FooCustomResource.class); - private final ConfigurationService configurationService = mock(ConfigurationService.class); + private final KubernetesClient kubernetesClient = MockKubernetesClient.client(ConfigMap.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); - private final Operator operator = new Operator(kubernetesClient, configurationService); - private final FooReconciler fooReconciler = FooReconciler.create(); + private final Operator operator = new Operator(kubernetesClient); + private final FooReconciler fooReconciler = new FooReconciler(); + + @BeforeAll + @AfterAll + static void setUpConfigurationServiceProvider() { + ConfigurationServiceProvider.reset(); + } @Test @DisplayName("should register `Reconciler` to Controller") + @SuppressWarnings("unchecked") public void shouldRegisterReconcilerToController() { // given - when(configurationService.getConfigurationFor(fooReconciler)).thenReturn(configuration); - when(configuration.watchAllNamespaces()).thenReturn(true); - when(configuration.getName()).thenReturn("FOO"); - when(configuration.getResourceClass()).thenReturn(FooCustomResource.class); - when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); + when(configuration.getResourceClass()).thenReturn(ConfigMap.class); // when - operator.register(fooReconciler); + operator.register(fooReconciler, configuration); // then assertThat(operator.getControllers().size()).isEqualTo(1); @@ -47,28 +51,17 @@ public void shouldRegisterReconcilerToController() { @Test @DisplayName("should throw `OperationException` when Configuration is null") public void shouldThrowOperatorExceptionWhenConfigurationIsNull() { - Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler)); - } - - private static class FooCustomResource extends CustomResource { - } + // use a ConfigurationService that doesn't automatically create configurations + ConfigurationServiceProvider.reset(); + ConfigurationServiceProvider.set(new AbstractConfigurationService(null)); - private static class FooSpec { - } - - private static class FooStatus { + Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler)); } - private static class FooReconciler implements Reconciler { - - private FooReconciler() {} - - public static FooReconciler create() { - return new FooReconciler(); - } + private static class FooReconciler implements Reconciler { @Override - public UpdateControl reconcile(FooCustomResource resource, Context context) { + public UpdateControl reconcile(ConfigMap resource, Context context) { return UpdateControl.noUpdate(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java new file mode 100644 index 0000000000..f69dd0bc29 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ConfigurationServiceProviderTest { + @BeforeEach + void resetProvider() { + ConfigurationServiceProvider.reset(); + } + + @Test + void shouldProvideADefaultInstanceIfNoneIsSet() { + final var instance = ConfigurationServiceProvider.instance(); + assertNotNull(instance); + assertTrue(instance instanceof BaseConfigurationService); + } + + @Test + void shouldProvideTheSetDefaultInstanceIfProvided() { + final var defaultConfig = new AbstractConfigurationService(null); + ConfigurationServiceProvider.setDefault(defaultConfig); + assertEquals(defaultConfig, ConfigurationServiceProvider.instance()); + } + + @Test + void shouldProvideTheSetInstanceIfProvided() { + final var config = new AbstractConfigurationService(null); + ConfigurationServiceProvider.set(config); + assertEquals(config, ConfigurationServiceProvider.instance()); + } + + @Test + void shouldBePossibleToOverrideConfigOnce() { + final var config = new AbstractConfigurationService(null); + assertTrue(config.checkCRDAndValidateLocalModel()); + + ConfigurationServiceProvider.set(config); + var instance = ConfigurationServiceProvider.instance(); + assertEquals(config, instance); + + ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); + instance = ConfigurationServiceProvider.instance(); + assertNotEquals(config, instance); + assertFalse(instance.checkCRDAndValidateLocalModel()); + + assertThrows(IllegalStateException.class, + () -> ConfigurationServiceProvider.overrideCurrent(o -> o.withCloseClientOnStop(false))); + } + + @Test + void resetShouldResetAllState() { + shouldBePossibleToOverrideConfigOnce(); + + ConfigurationServiceProvider.reset(); + assertEquals(ConfigurationServiceProvider.DEFAULT, ConfigurationServiceProvider.getDefault()); + + shouldBePossibleToOverrideConfigOnce(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 65c73b6b31..05aa637f1f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -15,14 +15,6 @@ void getCustomResourceClass() { public String getAssociatedReconcilerClassName() { return null; } - - @Override - public ConfigurationService getConfigurationService() { - return null; - } - - @Override - public void setConfigurationService(ConfigurationService service) {} }; assertEquals(TestCustomResource.class, conf.getResourceClass()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 2954af5aa4..ba31d6634e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -17,16 +17,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@SuppressWarnings("unchecked") class ControllerTest { - @Test void crdShouldNotBeCheckedForNativeResources() { final var client = MockKubernetesClient.client(Secret.class); - final var configurationService = mock(ConfigurationService.class); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(Secret.class); - when(configuration.getConfigurationService()).thenReturn(configurationService); final var controller = new Controller(reconciler, configuration, client); controller.start(); @@ -36,27 +34,27 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { final var client = MockKubernetesClient.client(TestCustomResource.class); - final var configurationService = mock(ConfigurationService.class); - when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); - when(configuration.getConfigurationService()).thenReturn(configurationService); - final var controller = new Controller(reconciler, configuration, client); - controller.start(); - verify(client, never()).apiextensions(); + try { + ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); + final var controller = new Controller(reconciler, configuration, client); + controller.start(); + verify(client, never()).apiextensions(); + } finally { + ConfigurationServiceProvider.reset(); + } } @Test void crdShouldBeCheckedForCustomResourcesByDefault() { + ConfigurationServiceProvider.reset(); final var client = MockKubernetesClient.client(TestCustomResource.class); - final var configurationService = mock(ConfigurationService.class); - when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); - when(configuration.getConfigurationService()).thenReturn(configurationService); final var controller = new Controller(reconciler, configuration, client); // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 4621e4700e..1eb7ea1bb4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -2,30 +2,28 @@ import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; -import com.fasterxml.jackson.databind.ObjectMapper; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings({"unchecked", "rawtypes"}) class GenericKubernetesResourceMatcherTest { private static final Context context = mock(Context.class); - static { - final var configurationService = mock(ConfigurationService.class); + + @BeforeAll + static void setUp() { final var controllerConfiguration = mock(ControllerConfiguration.class); - when(configurationService.getObjectMapper()).thenReturn(new ObjectMapper()); - when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index 6c763d20fe..802be9c3d9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -2,11 +2,11 @@ import java.util.HashMap; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; @@ -15,15 +15,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings("rawtypes") class GenericResourceUpdatePreProcessorTest { private static final Context context = mock(Context.class); - static { - final var configurationService = mock(ConfigurationService.class); + @BeforeAll + static void setUp() { final var controllerConfiguration = mock(ControllerConfiguration.class); - when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); - when(configurationService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index f07d88db1e..5e230d0418 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -4,7 +4,10 @@ import java.util.ArrayList; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -16,7 +19,7 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -35,16 +38,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; +@SuppressWarnings({"unchecked", "rawtypes"}) class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; @@ -52,26 +57,48 @@ class ReconciliationDispatcherTest { public static final long RECONCILIATION_MAX_INTERVAL = 10L; private TestCustomResource testCustomResource; private ReconciliationDispatcher reconciliationDispatcher; - private final Reconciler reconciler = mock(Reconciler.class, - withSettings().extraInterfaces(ErrorStatusHandler.class)); - private final ConfigurationService configService = mock(ConfigurationService.class); + private TestReconciler reconciler; private final CustomResourceFacade customResourceFacade = mock(ReconciliationDispatcher.CustomResourceFacade.class); - private ControllerConfiguration configuration = mock(ControllerConfiguration.class); + + @BeforeAll + static void classSetup() { + /* + * We need this for mock reconcilers to properly generate the expected UpdateControl: without + * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), + * any())).thenReturn(UpdateControl.updateStatus(testCustomResource))` will return null because + * equals will fail on the two equal but NOT identical TestCustomResources because equals is not + * implemented on TestCustomResourceSpec or TestCustomResourceStatus + */ + ConfigurationServiceProvider.overrideCurrent(overrider -> { + overrider.checkingCRDAndValidateLocalModel(false) + .withResourceCloner(new Cloner() { + @Override + public R clone(R object) { + return object; + } + }); + }); + } + + @AfterAll + static void tearDown() { + ConfigurationServiceProvider.reset(); + } @BeforeEach void setup() { testCustomResource = TestUtils.testCustomResource(); + reconciler = spy(new TestReconciler()); reconciliationDispatcher = init(testCustomResource, reconciler, null, customResourceFacade, true); } private ReconciliationDispatcher init(R customResource, - Reconciler reconciler, ControllerConfiguration configuration, + Reconciler reconciler, ControllerConfiguration configuration, CustomResourceFacade customResourceFacade, boolean useFinalizer) { configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; - ReconciliationDispatcherTest.this.configuration = configuration; final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; when(configuration.getFinalizer()).thenReturn(finalizer); when(configuration.useFinalizer()).thenCallRealMethod(); @@ -81,25 +108,6 @@ private ReconciliationDispatcher init(R customResourc when(configuration.reconciliationMaxInterval()) .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); - when(configuration.getConfigurationService()).thenReturn(configService); - - - /* - * We need this for mock reconcilers to properly generate the expected UpdateControl: without - * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), - * any())).thenReturn(UpdateControl.updateStatus(testCustomResource))` will return null because - * equals will fail on the two equal but NOT identical TestCustomResources because equals is not - * implemented on TestCustomResourceSpec or TestCustomResourceStatus - */ - when(configService.getResourceCloner()).thenReturn(new Cloner() { - @Override - - public T clone(T object) { - return object; - } - }); - when(reconciler.cleanup(eq(customResource), any())) - .thenReturn(DeleteControl.defaultDelete()); Controller controller = new Controller<>(reconciler, configuration, MockKubernetesClient.client(customResource.getClass())); controller.start(); @@ -131,8 +139,7 @@ void callCreateOrUpdateOnNewResourceIfFinalizerSet() { void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateStatus(testCustomResource)); + reconciler.reconcile = (r, c) -> UpdateControl.updateStatus(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -144,8 +151,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateResourceAndStatus(testCustomResource)); + reconciler.reconcile = (r, c) -> UpdateControl.updateResourceAndStatus(testCustomResource); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -219,8 +225,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.cleanup(eq(testCustomResource), any())) - .thenReturn(DeleteControl.noFinalizerRemoval()); + reconciler.cleanup = (r, c) -> DeleteControl.noFinalizerRemoval(); markForDeletion(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -233,8 +238,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -244,8 +248,8 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); + + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -297,7 +301,7 @@ public boolean isLastAttempt() { verify(reconciler, times(1)) .reconcile(any(), contextArgumentCaptor.capture()); Context context = contextArgumentCaptor.getValue(); - final var retryInfo = context.getRetryInfo().get(); + final var retryInfo = context.getRetryInfo().orElseGet(() -> fail("Missing optional")); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); assertThat(retryInfo.isLastAttempt()).isEqualTo(true); } @@ -306,14 +310,14 @@ public boolean isLastAttempt() { void setReScheduleToPostExecutionControlFromUpdateControl() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn( - UpdateControl.updateStatus(testCustomResource).rescheduleAfter(1000L)); + reconciler.reconcile = + (r, c) -> UpdateControl.updateStatus(testCustomResource).rescheduleAfter(1000L); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); + assertThat(control.getReScheduleDelay().orElseGet(() -> fail("Missing optional"))) + .isEqualTo(1000L); } @Test @@ -321,13 +325,14 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(reconciler.cleanup(eq(testCustomResource), any())) - .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1, TimeUnit.SECONDS)); + reconciler.cleanup = + (r, c) -> DeleteControl.noFinalizerRemoval().rescheduleAfter(1, TimeUnit.SECONDS); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); + assertThat(control.getReScheduleDelay().orElseGet(() -> fail("Missing optional"))) + .isEqualTo(1000L); } @Test @@ -347,8 +352,9 @@ void setObservedGenerationForStatusIfNeeded() { PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) - .isEqualTo(1L); + assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional")) + .getStatus().getObservedGeneration()) + .isEqualTo(1L); } @Test @@ -367,8 +373,9 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) - .isEqualTo(1L); + assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional")) + .getStatus().getObservedGeneration()) + .isEqualTo(1L); } @Test @@ -388,20 +395,22 @@ void updateObservedGenerationOnCustomResourceUpdate() { PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) - .isEqualTo(1L); + assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional")) + .getStatus().getObservedGeneration()) + .isEqualTo(1L); } @Test void callErrorStatusHandlerIfImplemented() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(any(), any())) - .thenThrow(new IllegalStateException("Error Status Test")); - when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any(), any())).then(a -> { + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); return Optional.of(testCustomResource); - }); + }; reconciliationDispatcher.handleExecution( new ExecutionScope( @@ -427,12 +436,14 @@ public boolean isLastAttempt() { void callErrorStatusHandlerEvenOnFirstError() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(any(), any())) - .thenThrow(new IllegalStateException("Error Status Test")); - when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any(), any())).then(a -> { + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); return Optional.of(testCustomResource); - }); + }; + reconciliationDispatcher.handleExecution( new ExecutionScope( testCustomResource, null)); @@ -445,8 +456,7 @@ void callErrorStatusHandlerEvenOnFirstError() { void schedulesReconciliationIfMaxDelayIsSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -459,9 +469,8 @@ void schedulesReconciliationIfMaxDelayIsSet() { void canSkipSchedulingMaxDelayIf() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); - when(configuration.reconciliationMaxInterval()) + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); + when(reconciliationDispatcher.configuration().reconciliationMaxInterval()) .thenReturn(Optional.empty()); PostExecutionControl control = @@ -490,4 +499,35 @@ private void removeFinalizers(CustomResource customResource) { public ExecutionScope executionScopeWithCREvent(T resource) { return new ExecutionScope<>(resource, null); } + + private class TestReconciler + implements Reconciler, ErrorStatusHandler { + private BiFunction> reconcile; + private BiFunction cleanup; + private ErrorStatusHandler errorHandler; + + @Override + public UpdateControl reconcile(TestCustomResource resource, + Context context) { + if (reconcile != null && resource.equals(testCustomResource)) { + return reconcile.apply(resource, context); + } + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(TestCustomResource resource, Context context) { + if (cleanup != null && resource.equals(testCustomResource)) { + return cleanup.apply(resource, context); + } + return DeleteControl.defaultDelete(); + } + + @Override + public Optional updateErrorStatus(TestCustomResource resource, + RetryInfo retryInfo, RuntimeException e) { + return errorHandler != null ? errorHandler.updateErrorStatus(resource, retryInfo, e) + : Optional.empty(); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index 446e654501..e995d3b9c4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source; import java.util.Date; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -9,7 +8,6 @@ import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.internal.util.collections.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,7 +17,10 @@ import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Version; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -45,14 +46,17 @@ public class CustomResourceSelectorTest { KubernetesClient client; ConfigurationService configurationService; - @SuppressWarnings("unchecked") @BeforeEach void setUpResources() { + // need to reset the provider if we plan on changing the configuration service since it likely + // has already been set in previous tests + ConfigurationServiceProvider.reset(); + configurationService = spy(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); when(configurationService.getVersion()).thenReturn(new Version("1", "1", new Date())); - when(configurationService.getConfigurationFor(any(MyController.class))).thenReturn( - new MyConfiguration(configurationService, null)); + when(configurationService.getConfigurationFor(any(MyController.class))) + .thenReturn(new MyConfiguration()); } @Test @@ -78,7 +82,7 @@ void resourceWatchedByLabel() { c1err.incrementAndGet(); } }), - new MyConfiguration(configurationService, "app=foo")); + (overrider) -> overrider.settingNamespace(NAMESPACE).withLabelSelector("app=foo")); o1.start(); o2.register( new MyController( @@ -90,7 +94,7 @@ void resourceWatchedByLabel() { c2err.incrementAndGet(); } }), - new MyConfiguration(configurationService, "app=bar")); + (overrider) -> overrider.settingNamespace(NAMESPACE).withLabelSelector("app=bar")); o2.start(); client.resources(TestCustomResource.class).inNamespace(NAMESPACE).create(newMyResource("foo", @@ -127,40 +131,13 @@ public TestCustomResource newMyResource(String app, String namespace) { return resource; } - public static class MyConfiguration - implements - io.javaoperatorsdk.operator.api.config.ControllerConfiguration { - - private final String labelSelector; - private final ConfigurationService service; - - public MyConfiguration(ConfigurationService configurationService, String labelSelector) { - this.labelSelector = labelSelector; - this.service = configurationService; - } - - @Override - public String getLabelSelector() { - return labelSelector; - } - - @Override - public String getAssociatedReconcilerClassName() { - return MyController.class.getCanonicalName(); - } - - @Override - public Set getNamespaces() { - return Sets.newSet(NAMESPACE); - } + public static class MyConfiguration extends DefaultControllerConfiguration { - @Override - public ConfigurationService getConfigurationService() { - return service; + public MyConfiguration() { + super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.NO_FINALIZER, + false, null, + null, null, null, TestCustomResource.class, null, null); } - - @Override - public void setConfigurationService(ConfigurationService service) {} } @ControllerConfiguration(namespaces = NAMESPACE) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index 0dfb2d09d7..1c4c2bcce1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -140,7 +140,7 @@ public ControllerConfig(String finalizer, boolean generationAware, eventFilter, customResourceClass, null, - null, null); + null); } } @@ -150,6 +150,7 @@ public TestController(ControllerConfiguration configuration) super(null, configuration, MockKubernetesClient.client(TestCustomResource.class)); } + @SuppressWarnings("unchecked") @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 9782948dba..d06cf97fe8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -26,7 +26,7 @@ class ControllerResourceEventSourceTest extends public static final String FINALIZER = "finalizer"; - private TestController testController = new TestController(true); + private final TestController testController = new TestController(true); @BeforeEach public void setup() { @@ -109,9 +109,10 @@ public void callsBroadcastsOnResourceEvents() { eq(customResource1)); } + @SuppressWarnings("unchecked") private static class TestController extends Controller { - private EventSourceManager eventSourceManager = + private final EventSourceManager eventSourceManager = mock(EventSourceManager.class); public TestController(boolean generationAware) { @@ -141,7 +142,7 @@ public TestConfiguration(boolean generationAware) { null, TestCustomResource.class, null, - null, null); + null); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java index 5fd9f49084..5c23cc4c95 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample.simple; +import java.util.Objects; + public class TestCustomResourceSpec { private String configMapName; @@ -46,4 +48,22 @@ public String toString() { + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestCustomResourceSpec that = (TestCustomResourceSpec) o; + return Objects.equals(configMapName, that.configMapName) && Objects.equals( + key, that.key) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(configMapName, key, value); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java index 620bbaabd8..ab5559d80c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample.simple; +import java.util.Objects; + public class TestCustomResourceStatus { private String configMapStatus; @@ -16,4 +18,21 @@ public void setConfigMapStatus(String configMapStatus) { public String toString() { return "TestCustomResourceStatus{" + "configMapStatus='" + configMapStatus + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestCustomResourceStatus that = (TestCustomResourceStatus) o; + return Objects.equals(configMapStatus, that.configMapStatus); + } + + @Override + public int hashCode() { + return Objects.hash(configMapStatus); + } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index 839b550231..2c2a14b14d 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -22,9 +22,8 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Utils; -import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Version; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; public abstract class AbstractOperatorExtension implements HasKubernetesClient, BeforeAllCallback, @@ -200,7 +199,7 @@ public static abstract class AbstractBuilder> { protected boolean oneNamespacePerClass; protected AbstractBuilder() { - this.configurationService = new BaseConfigurationService(Version.UNKNOWN); + this.configurationService = ConfigurationServiceProvider.instance(); this.infrastructure = new ArrayList<>(); this.infrastructureTimeout = Duration.ofMinutes(1); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java index d34d6fee55..34e0b3f1d7 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java @@ -2,6 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -10,11 +11,16 @@ public class DefaultConfigurationService extends BaseConfigurationService { private static final DefaultConfigurationService instance = new DefaultConfigurationService(); private boolean createIfNeeded = super.createIfNeeded(); + static { + // register this as the default configuration service + ConfigurationServiceProvider.setDefault(instance); + } + private DefaultConfigurationService() { super(); } - public static DefaultConfigurationService instance() { + static DefaultConfigurationService instance() { return instance; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 12cf3ca4d8..a9fe44191e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -10,7 +10,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestReconciler; @@ -28,10 +27,7 @@ class ConcurrencyIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new TestReconciler(true)) - .build(); + OperatorExtension.builder().withReconciler(new TestReconciler(true)).build(); @Test void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index dbd015587e..0a3ed114a1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestReconciler; @@ -18,10 +17,7 @@ class ControllerExecutionIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new TestReconciler(true)) - .build(); + OperatorExtension.builder().withReconciler(new TestReconciler(true)).build(); @Test void configMapGetsCreatedForTestCustomResource() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java index 57fbc78838..58dc0a541d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java @@ -7,7 +7,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResource; import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResourceSpec; @@ -22,7 +21,6 @@ public class CreateUpdateDependentEventFilterIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new CreateUpdateEventFilterTestReconciler()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java index 70a3f04777..45387d2b97 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestReconciler; import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestResource; @@ -16,10 +15,7 @@ class CustomResourceFilterIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new CustomFilteringTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new CustomFilteringTestReconciler()).build(); @Test void doesCustomFiltering() throws InterruptedException { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java index fbf54a1ad1..8b0e7bab11 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.errorstatushandler.ErrorStatusHandlerTestCustomResource; @@ -23,7 +22,6 @@ class ErrorStatusHandlerIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(reconciler, new GenericRetry().setMaxAttempts(MAX_RETRY_ATTEMPTS).withLinearRetry()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index c91ebaad43..eea996cb3b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResource; @@ -19,10 +18,7 @@ class EventSourceIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(EventSourceTestCustomReconciler.class) - .build(); + OperatorExtension.builder().withReconciler(EventSourceTestCustomReconciler.class).build(); @Test void receivingPeriodicEvents() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 7ff36bd375..ddd4113c22 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; @@ -27,7 +26,6 @@ class InformerEventSourceIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new InformerEventSourceTestCustomReconciler()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java index a2c7378ff5..603dc2ab1a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.deployment.DeploymentReconciler; @@ -23,10 +22,7 @@ class KubernetesResourceStatusUpdateIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new DeploymentReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new DeploymentReconciler()).build(); @Test void testReconciliationOfNonCustomResourceAndStatusUpdate() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java index f284dbf407..69585340db 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestCustomResource; import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestReconciler; @@ -17,10 +16,7 @@ class MaxIntervalIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new MaxIntervalTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new MaxIntervalTestReconciler()).build(); @Test void reconciliationTriggeredBasedOnMaxInterval() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java index 930fc210b3..a6cf2d5591 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.multiversioncrd.*; @@ -22,7 +21,6 @@ class MultiVersionCRDIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(MultiVersionCRDTestReconciler1.class) .withReconciler(MultiVersionCRDTestReconciler2.class) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java index 5f86336a1c..4c57a977e3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestCustomResource; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestReconciler; @@ -17,10 +16,7 @@ class ObservedGenerationHandlingIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new ObservedGenerationTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new ObservedGenerationTestReconciler()).build(); @Test void testReconciliationOfNonCustomResourceAndStatusUpdate() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index c4ab025ac5..02b7b7af9e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomReconciler; @@ -27,7 +26,6 @@ class RetryIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler( new RetryTestCustomReconciler(NUMBER_FAILED_EXECUTIONS), new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java index 5ebb05eb0c..56a36cf6f9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomReconciler; @@ -23,7 +22,6 @@ class RetryMaxAttemptIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(reconciler, new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() .setMaxAttempts(MAX_RETRY_ATTEMPTS)) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index b7d88159e5..1d5f7e9d12 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResourceSpec; @@ -23,10 +22,7 @@ class StandaloneDependentResourceIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new StandaloneDependentTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new StandaloneDependentTestReconciler()).build(); @Test void dependentResourceManagesDeployment() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 75597643bf..06728063d4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResource; @@ -22,10 +21,7 @@ class SubResourceUpdateIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(SubResourceTestCustomReconciler.class) - .build(); + OperatorExtension.builder().withReconciler(SubResourceTestCustomReconciler.class).build(); @Test void updatesSubResourceStatus() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 6bad954400..d86ad51d60 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomReconciler; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResource; @@ -20,10 +19,7 @@ class UpdatingResAndSubResIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(DoubleUpdateTestCustomReconciler.class) - .build(); + OperatorExtension.builder().withReconciler(DoubleUpdateTestCustomReconciler.class).build(); @Test void updatesSubResourceStatus() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index bce957a399..6fd626b94b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -25,11 +25,10 @@ class DefaultConfigurationServiceTest { public static final String CUSTOM_FINALIZER_NAME = "a.custom/finalizer"; + final DefaultConfigurationService configurationService = DefaultConfigurationService.instance(); @Test void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { - final var configurationService = DefaultConfigurationService.instance(); - final LoggerContext context = LoggerContext.getContext(false); final PatternLayout layout = PatternLayout.createDefaultLayout(context.getConfiguration()); final ListAppender appender = new ListAppender("list", null, layout, false, false); @@ -75,8 +74,7 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { @Test void returnsValuesFromControllerAnnotationFinalizer() { final var reconciler = new TestCustomReconciler(); - final var configuration = - DefaultConfigurationService.instance().getConfigurationFor(reconciler); + final var configuration = configurationService.getConfigurationFor(reconciler); assertEquals(CustomResource.getCRDName(TestCustomResource.class), configuration.getResourceTypeName()); assertEquals( @@ -89,8 +87,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { @Test void returnCustomerFinalizerNameIfSet() { final var reconciler = new TestCustomFinalizerReconciler(); - final var configuration = - DefaultConfigurationService.instance().getConfigurationFor(reconciler); + final var configuration = configurationService.getConfigurationFor(reconciler); assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizer()); } @@ -99,9 +96,7 @@ void supportsInnerClassCustomResources() { final var reconciler = new TestCustomFinalizerReconciler(); assertDoesNotThrow( () -> { - DefaultConfigurationService.instance() - .getConfigurationFor(reconciler) - .getAssociatedReconcilerClassName(); + configurationService.getConfigurationFor(reconciler).getAssociatedReconcilerClassName(); }); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index 8abf73f3f6..0a5703a676 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -14,8 +14,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; @@ -31,9 +29,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, - new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) - .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) - .build()); + overrider -> overrider.withMetrics(new MicrometerMetrics(new LoggingMeterRegistry()))); MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index ed0ff40489..b8e8b0566a 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -17,7 +17,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; @@ -39,7 +38,7 @@ class MySQLSchemaOperatorE2E { static final String MY_SQL_NS = "mysql"; - private static List infrastructure = new ArrayList<>(); + private final static List infrastructure = new ArrayList<>(); static { infrastructure.add( @@ -60,10 +59,10 @@ boolean isLocal() { } @RegisterExtension + @SuppressWarnings("unchecked") AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler( new MySQLSchemaReconciler(), c -> { @@ -75,7 +74,6 @@ boolean isLocal() { .withInfrastructure(infrastructure) .build() : E2EOperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) .withInfrastructure(infrastructure) .build(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index 09a7394e5b..6b894a3348 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -14,7 +14,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class TomcatOperator { @@ -24,7 +23,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + Operator operator = new Operator(client); operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); operator.installShutdownHook(); diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index dabcbdfa4b..2f37638655 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -10,7 +10,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.*; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.InClusterCurl; @@ -43,13 +42,11 @@ boolean isLocal() { @RegisterExtension AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new TomcatReconciler()) .withReconciler(new WebappReconciler(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment( client.load(new FileInputStream("k8s/operator.yaml")).get()) .build(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index 0b643462b1..0277da8199 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -14,7 +14,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class WebPageOperator { public static final String WEBPAGE_RECONCILER_ENV = "WEBPAGE_RECONCILER"; @@ -27,7 +26,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + Operator operator = new Operator(client); if (WEBPAGE_RECONCILER_ENV_VALUE.equals(System.getenv(WEBPAGE_RECONCILER_ENV))) { operator.register(new WebPageReconciler(client)); } else { diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java index 580a2022c7..3bfdd3f45b 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; @@ -19,12 +18,10 @@ public WebPageOperatorDependentResourcesE2E() throws FileNotFoundException {} isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new WebPageReconcilerDependentResources(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) .build(); diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java index a096b03965..b7c8dae6af 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java @@ -9,7 +9,6 @@ import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; @@ -27,12 +26,10 @@ public WebPageOperatorE2E() throws FileNotFoundException {} isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new WebPageReconciler(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get(), resources -> { Deployment deployment = (Deployment) resources.stream() diff --git a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java index 19d0a471b7..0a3b59d290 100644 --- a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java @@ -3,18 +3,13 @@ import java.util.concurrent.Executors; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class PureJavaApplicationRunner { public static void main(String[] args) { Operator operator = - new Operator( - ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) - .withExecutorService(Executors.newCachedThreadPool()) - .withConcurrentReconciliationThreads(2) - .build()); + new Operator(overrider -> overrider.withExecutorService(Executors.newCachedThreadPool()) + .withConcurrentReconciliationThreads(2)); operator.register(new CustomServiceReconciler()); operator.start(); } diff --git a/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java index 07dd4b50c0..7eed511d18 100644 --- a/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java +++ b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java @@ -7,7 +7,6 @@ import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @Configuration public class Config { @@ -19,8 +18,9 @@ public CustomServiceReconciler customServiceController() { // Register all controller beans @Bean(initMethod = "start", destroyMethod = "stop") + @SuppressWarnings("rawtypes") public Operator operator(List controllers) { - Operator operator = new Operator(DefaultConfigurationService.instance()); + Operator operator = new Operator(); controllers.forEach(operator::register); return operator; } From a89832e83f6eda9102dbb63eb11eeef762d40f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 09:32:20 +0100 Subject: [PATCH 0070/1319] Docs: Add glossary (#1024) --- docs/_data/sidebar.yml | 2 ++ docs/documentation/glossary.md | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 docs/documentation/glossary.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 5bd1f2ce22..0dc51bddec 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -5,6 +5,8 @@ url: /docs/getting-started - title: How to use Samples url: /docs/using-samples + - title: Glossary + url: /docs/glossary - title: Features url: /docs/features - title: Patterns and Best Practices diff --git a/docs/documentation/glossary.md b/docs/documentation/glossary.md new file mode 100644 index 0000000000..3e48bf7efa --- /dev/null +++ b/docs/documentation/glossary.md @@ -0,0 +1,17 @@ +--- +title: Glossary description: Glossary layout: docs permalink: /docs/glossary +--- + +# Glossary + +- Primary Resource - the resource that represents the desired state that the controller is working + to achieve. While this is often a Custom Resource, this can also be a Kubernetes native resource. +- Secondary Resource - any resource that the controller needs to achieve the desired state + represented by the primary resource. These resources can be created, updated, deleted or simply + read depending on the use case. For example, the `Deployment` controller manages `ReplicatSet` + instances when trying to realize the state represented by the `Deployment`. In this scenario, + the `Deployment` is the primary resource while `ReplicaSet` is one of the secondary resources + managed by the `Deployment` controller. +- Dependent Resource - a feature of JOSDK, aimed at making easier to manage secondary resources. A + dependent resource is therefore a secondary resource implemented using this specific JOSDK + feature. \ No newline at end of file From 511a59e2c19a2360576b1e5201f0da4a91da1ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 15:00:40 +0100 Subject: [PATCH 0071/1319] fix: unit tests (#1032) --- .../processing/event/EventProcessorTest.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index a716a49898..0e968e13a7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.answers.AnswersWithDelay; +import org.mockito.internal.stubbing.answers.Returns; import org.mockito.stubbing.Answer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,14 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class EventProcessorTest { @@ -40,6 +35,7 @@ class EventProcessorTest { public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; public static final String TEST_NAMESPACE = "default-event-handler-test"; + private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); @@ -188,16 +184,19 @@ void scheduleTimedEventIfInstructedByPostExecutionControl() { } @Test - void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { + void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() throws InterruptedException { var testDelay = 10000L; - when(reconciliationDispatcherMock.handleExecution(any())) - .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); - - eventProcessor.handleEvent(prepareCREvent()); - eventProcessor.handleEvent(prepareCREvent()); - - verify(retryTimerEventSourceMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(0)) - .scheduleOnce(any(), eq(testDelay)); + doAnswer(new AnswersWithDelay(FAKE_CONTROLLER_EXECUTION_DURATION, + new Returns(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)))) + .when(reconciliationDispatcherMock).handleExecution(any()); + var resourceId = new ResourceID("test1", "default"); + eventProcessor.handleEvent(prepareCREvent(resourceId)); + Thread.sleep(FAKE_CONTROLLER_EXECUTION_DURATION / 3); + eventProcessor.handleEvent(prepareCREvent(resourceId)); + + verify(retryTimerEventSourceMock, + after((long) (FAKE_CONTROLLER_EXECUTION_DURATION * 1.5)).times(0)) + .scheduleOnce(any(), eq(testDelay)); } @Test @@ -205,7 +204,7 @@ void doNotFireEventsIfClosing() { eventProcessor.stop(); eventProcessor.handleEvent(prepareCREvent()); - verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); + verify(reconciliationDispatcherMock, after(50).times(0)).handleExecution(any()); } @Test From 261a38c09b47d41ade4c2d132aad5cdc74c32c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 15:00:50 +0100 Subject: [PATCH 0072/1319] fix: flaky integration test (#1029) --- .../operator/SubResourceUpdateIT.java | 15 ++++++++++----- .../SubResourceTestCustomReconciler.java | 7 ++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 06728063d4..85e11c7287 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -19,6 +19,9 @@ class SubResourceUpdateIT { + public static final int WAIT_AFTER_EXECUTION = 500; + public static final int EVENT_RECEIVE_WAIT = 200; + @RegisterExtension OperatorExtension operator = OperatorExtension.builder().withReconciler(SubResourceTestCustomReconciler.class).build(); @@ -30,7 +33,7 @@ void updatesSubResourceStatus() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(2); @@ -45,7 +48,7 @@ void updatesSubResourceStatusNoFinalizer() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(2); @@ -60,7 +63,7 @@ void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(2); @@ -77,13 +80,15 @@ void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); operator.create(SubResourceTestCustomResource.class, resource); + // waits for the resource to start processing + waitXms(EVENT_RECEIVE_WAIT); resource.getSpec().setValue("new value"); operator.resources(SubResourceTestCustomResource.class).createOrReplace(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(500); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(3); @@ -117,7 +122,7 @@ public SubResourceTestCustomResource createTestCustomResource(String id) { return resource; } - private void waitXms(int x) { + public static void waitXms(int x) { try { Thread.sleep(x); } catch (InterruptedException e) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 081100beb3..8bfe6dd67d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -12,16 +12,21 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; +import static io.javaoperatorsdk.operator.support.TestUtils.waitXms; + @ControllerConfiguration(generationAwareEventProcessing = false) public class SubResourceTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { + public static final int RECONCILER_MIN_EXEC_TIME = 300; + public static final String FINALIZER_NAME = ReconcilerUtils.getDefaultFinalizerName(SubResourceTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + @Override public UpdateControl reconcile( SubResourceTestCustomResource resource, Context context) { @@ -33,7 +38,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(SubResourceTestCustomResourceStatus.State.SUCCESS); - + waitXms(RECONCILER_MIN_EXEC_TIME); return UpdateControl.updateStatus(resource); } From 6c8affcec0033b01041abd35a98fc8f1bd5bcc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 15:01:02 +0100 Subject: [PATCH 0073/1319] Reconcile api proposal (#1030) --- .../dependent/AbstractDependentResource.java | 27 +++++++------ .../dependent/DependentResource.java | 2 +- .../reconciler/dependent/ReconcileResult.java | 38 +++++++++++++++++++ .../AbstractSimpleDependentResource.java | 11 ++++-- 4 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 95b82d50ec..0a750d836c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -26,25 +26,27 @@ public AbstractDependentResource() { } @Override - public void reconcile(P primary, Context

context) { - final var creatable = isCreatable(primary, context); - final var updatable = isUpdatable(primary, context); - if (creatable || updatable) { - var maybeActual = getResource(primary); + public ReconcileResult reconcile(P primary, Context

context) { + final var isCreatable = isCreatable(primary, context); + final var isUpdatable = isUpdatable(primary, context); + var maybeActual = getResource(primary); + if (isCreatable || isUpdatable) { if (maybeActual.isEmpty()) { - if (creatable) { + if (isCreatable) { var desired = desired(primary, context); log.debug("Creating dependent {} for primary {}", desired, primary); - handleCreate(desired, primary, context); + var createdResource = handleCreate(desired, primary, context); + return ReconcileResult.resourceCreated(createdResource); } } else { final var actual = maybeActual.get(); - if (updatable) { + if (isUpdatable) { final var match = updater.match(actual, primary, context); if (!match.matched()) { final var desired = match.computedDesired().orElse(desired(primary, context)); log.debug("Updating dependent {} for primary {}", desired, primary); - handleUpdate(actual, desired, primary, context); + var updatedResource = handleUpdate(actual, desired, primary, context); + return ReconcileResult.resourceUpdated(updatedResource); } } else { log.debug("Update skipped for dependent {} as it matched the existing one", actual); @@ -55,15 +57,17 @@ public void reconcile(P primary, Context

context) { "Dependent {} is read-only, implement Creator and/or Updater interfaces to modify it", getClass().getSimpleName()); } + return ReconcileResult.noOperation(maybeActual.orElse(null)); } - protected void handleCreate(R desired, P primary, Context

context) { + protected R handleCreate(R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R created = null; try { prepareEventFiltering(desired, resourceID); created = creator.create(desired, primary, context); cacheAfterCreate(resourceID, created); + return created; } catch (RuntimeException e) { cleanupAfterEventFiltering(desired, resourceID, created); throw e; @@ -97,13 +101,14 @@ private void prepareEventFiltering(R desired, ResourceID resourceID) { } } - protected void handleUpdate(R actual, R desired, P primary, Context

context) { + protected R handleUpdate(R actual, R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R updated = null; try { prepareEventFiltering(desired, resourceID); updated = updater.update(actual, desired, primary, context); cacheAfterUpdate(actual, resourceID, updated); + return updated; } catch (RuntimeException e) { cleanupAfterEventFiltering(desired, resourceID, updated); throw e; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 8fc3cea73f..377e4aea22 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -6,7 +6,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; public interface DependentResource { - void reconcile(P primary, Context

context); + ReconcileResult reconcile(P primary, Context

context); default void cleanup(P primary, Context

context) {} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java new file mode 100644 index 0000000000..392b213e81 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +public class ReconcileResult { + + private R resource; + private Operation operation; + + public static ReconcileResult resourceCreated(T resource) { + return new ReconcileResult<>(resource, Operation.CREATED); + } + + public static ReconcileResult resourceUpdated(T resource) { + return new ReconcileResult<>(resource, Operation.UPDATED); + } + + public static ReconcileResult noOperation(T resource) { + return new ReconcileResult<>(resource, Operation.NONE); + } + + private ReconcileResult(R resource, Operation operation) { + this.resource = resource; + this.operation = operation; + } + + public Optional getResource() { + return Optional.ofNullable(resource); + } + + public Operation getOperation() { + return operation; + } + + public enum Operation { + CREATED, UPDATED, NONE + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index e7a0edd549..096e4265f6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -7,6 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; @@ -43,11 +44,11 @@ public Optional getResource(HasMetadata primaryResource) { public abstract Optional fetchResource(HasMetadata primaryResource); @Override - public void reconcile(P primary, Context

context) { + public ReconcileResult reconcile(P primary, Context

context) { var resourceId = ResourceID.fromResource(primary); Optional resource = fetchResource(primary); resource.ifPresentOrElse(r -> cache.put(resourceId, r), () -> cache.remove(resourceId)); - super.reconcile(primary, context); + return super.reconcile(primary, context); } public void cleanup(P primary, Context

context) { @@ -56,15 +57,17 @@ public void cleanup(P primary, Context

context) { } @Override - protected void handleCreate(R desired, P primary, Context

context) { + protected R handleCreate(R desired, P primary, Context

context) { var res = this.creator.create(desired, primary, context); cache.put(ResourceID.fromResource(primary), res); + return res; } @Override - protected void handleUpdate(R actual, R desired, P primary, Context

context) { + protected R handleUpdate(R actual, R desired, P primary, Context

context) { var res = updater.update(actual, desired, primary, context); cache.put(ResourceID.fromResource(primary), res); + return res; } public Matcher.Result match(R actualResource, P primary, Context context) { From be327a73aa6f34a5c96250cf2dfc0bb8a80aff25 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 15 Mar 2022 09:59:25 +0000 Subject: [PATCH 0074/1319] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index a2308d86e9..f5f6e12daa 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 4262525b45..bf5ee47e20 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 89a631f0ad..aad9fd5048 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 0b16c01603..fccc558a17 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 0e00c4c9ca..965eb5206d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index eb7c937e95..ca7a9e1f54 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 933395a4fb..d2ad343e99 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ae046ca991..187c72fb61 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 0a7b7a71cf..86ee11f906 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 6cf1f8d991..79aabae280 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 8a9f424049..ca65419c62 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index f56c811cbd..eb511a925c 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index f97e16f347..3a29992b9d 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 5f1e5374cc20206250bcad759783bf861a8e2123 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Tue, 15 Mar 2022 13:29:03 +0000 Subject: [PATCH 0075/1319] More flexible in cluster curl (#1034) --- .../io/javaoperatorsdk/operator/junit/InClusterCurl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java index 927723209c..dd9748383b 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java @@ -21,11 +21,15 @@ public InClusterCurl(https://github.com/KubernetesClient%20client,%20String%20namespace) { } public String checkUrl(String url) { + return checkUrl("-s", "-o", "/dev/null", "-w", "%{http_code}", url); + } + + public String checkUrl(String... args) { String podName = KubernetesResourceUtil.sanitizeName("curl-" + UUID.randomUUID()); try { Pod curlPod = client.run().inNamespace(namespace) .withRunConfig(new RunConfigBuilder() - .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withArgs(args) .withName(podName) .withImage("curlimages/curl:7.78.0") .withRestartPolicy("Never") From dd6e19df45f79b5c184a183b426660a457a70acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 15 Mar 2022 15:44:02 +0100 Subject: [PATCH 0076/1319] docs: adding migration guide skeleton (#1036) * docs: adding migration guide skeleton * docs: bugix --- docs/_data/sidebar.yml | 3 ++- docs/documentation/glossary.md | 5 ++++- docs/documentation/v3-migration.md | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/documentation/v3-migration.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 0dc51bddec..3f3f1734b7 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -19,4 +19,5 @@ url: /docs/contributing - title: Migrating from v1 to v2 url: /docs/v2-migration - + - title: Migrating from v2 to v3 + url: /docs/v3-migration diff --git a/docs/documentation/glossary.md b/docs/documentation/glossary.md index 3e48bf7efa..7716b0ed37 100644 --- a/docs/documentation/glossary.md +++ b/docs/documentation/glossary.md @@ -1,5 +1,8 @@ --- -title: Glossary description: Glossary layout: docs permalink: /docs/glossary +title: Glossary +description: Glossary +layout: docs +permalink: /docs/glossary --- # Glossary diff --git a/docs/documentation/v3-migration.md b/docs/documentation/v3-migration.md new file mode 100644 index 0000000000..716d1704d5 --- /dev/null +++ b/docs/documentation/v3-migration.md @@ -0,0 +1,8 @@ +--- +title: Migrating from v2 to v3 +description: Migrating from v2 to v3 +layout: docs +permalink: /docs/v3-migration +--- + +# Migrating from v2 to v3 From 4056ba312d7bd19e47e4846ce4eb14215b9649af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 15 Mar 2022 18:04:05 +0100 Subject: [PATCH 0077/1319] feat: error handler improvements (#1033) * feat: adding context to error handler; using exception instead runtime exception * fix: error handling compilation issue * feat: added error status update control * Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java Co-authored-by: Chris Laprun * fix: improvements on error handling * fix: exception wrapping * fix: addind missing override * fix: unit tests * docs: update Co-authored-by: Chris Laprun --- docs/documentation/features.md | 10 +- .../micrometer/MicrometerMetrics.java | 12 ++- .../operator/OperatorException.java | 4 + .../operator/api/monitoring/Metrics.java | 6 +- .../api/reconciler/DefaultContext.java | 7 +- .../api/reconciler/ErrorStatusHandler.java | 10 +- .../reconciler/ErrorStatusUpdateControl.java | 41 +++++++ .../operator/api/reconciler/Reconciler.java | 2 +- .../operator/processing/Controller.java | 46 ++++---- .../processing/event/EventProcessor.java | 2 +- .../event/PostExecutionControl.java | 8 +- .../event/ReconciliationDispatcher.java | 45 +++++--- .../event/ReconciliationDispatcherTest.java | 101 ++++++++++++------ .../ErrorStatusHandlerTestReconciler.java | 11 +- .../StandaloneDependentTestReconciler.java | 9 +- .../sample/MySQLSchemaReconciler.java | 16 +-- .../operator/sample/WebPageReconciler.java | 6 +- .../WebPageReconcilerDependentResources.java | 8 +- 18 files changed, 230 insertions(+), 114 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 95924e24dd..35deab4664 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -275,9 +275,9 @@ In order to facilitate error reporting Reconciler can implement the following [interface](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java): ```java -public interface ErrorStatusHandler { +public interface ErrorStatusHandler

{ - Optional updateErrorStatus(T resource, RetryInfo retryInfo, RuntimeException e); + ErrorStatusUpdateControl

updateErrorStatus(P resource, Context

context, Exception e); } ``` @@ -294,6 +294,12 @@ will also produce an event, and will result in a reconciliation if the controlle The scope of this feature is only the `reconcile` method of the reconciler, since there should not be updates on custom resource after it is marked for deletion. +Retry can be skipped for the cases of unrecoverable errors: + +```java + ErrorStatusUpdateControl.updateStatus(customResource).withNoRetry(); +``` + ### Correctness and Automatic Retries There is a possibility to turn off the automatic retries. This is not desirable, unless there is a very specific reason. diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java index d08f1c7669..25058e198e 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Optional; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.event.Event; @@ -33,7 +34,13 @@ public T timeControllerExecution(ControllerExecution execution) { .publishPercentileHistogram() .register(registry); try { - final var result = timer.record(execution::execute); + final var result = timer.record(() -> { + try { + return execution.execute(); + } catch (Exception e) { + throw new OperatorException(e); + } + }); final var successType = execution.successTypeName(result); registry .counter(execName + ".success", "controller", name, "type", successType) @@ -58,6 +65,7 @@ public void cleanupDoneFor(ResourceID customResourceUid) { incrementCounter(customResourceUid, "events.delete"); } + @Override public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable) { Optional retryInfo = Optional.ofNullable(retryInfoNullable); incrementCounter(resourceID, RECONCILIATIONS + "started", @@ -72,7 +80,7 @@ public void finishedReconciliation(ResourceID resourceID) { incrementCounter(resourceID, RECONCILIATIONS + "success"); } - public void failedReconciliation(ResourceID resourceID, RuntimeException exception) { + public void failedReconciliation(ResourceID resourceID, Exception exception) { var cause = exception.getCause(); if (cause == null) { cause = exception; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java index b11ff0a61a..895d643fcb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java @@ -8,6 +8,10 @@ public OperatorException(String message) { super(message); } + public OperatorException(Throwable cause) { + super(cause); + } + public OperatorException(String message, Throwable cause) { super(message, cause); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java index d7daba49ae..b1be115bc4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java @@ -13,7 +13,7 @@ default void receivedEvent(Event event) {} default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) {} - default void failedReconciliation(ResourceID resourceID, RuntimeException exception) {} + default void failedReconciliation(ResourceID resourceID, Exception exception) {} default void cleanupDoneFor(ResourceID customResourceUid) {} @@ -27,10 +27,10 @@ interface ControllerExecution { String successTypeName(T result); - T execute(); + T execute() throws Exception; } - default T timeControllerExecution(ControllerExecution execution) { + default T timeControllerExecution(ControllerExecution execution) throws Exception { return execution.execute(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index d3a5b7bc18..d80be8b3ae 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -9,7 +9,7 @@ public class DefaultContext

implements Context

{ - private final RetryInfo retryInfo; + private RetryInfo retryInfo; private final Controller

controller; private final P primaryResource; private final ControllerConfiguration

controllerConfiguration; @@ -44,4 +44,9 @@ public ControllerConfiguration

getControllerConfiguration() { public ManagedDependentResourceContext managedDependentResourceContext() { return managedDependentResourceContext; } + + public DefaultContext

setRetryInfo(RetryInfo retryInfo) { + this.retryInfo = retryInfo; + return this; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 22a16e4ccd..c7bd09a930 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -1,16 +1,14 @@ package io.javaoperatorsdk.operator.api.reconciler; -import java.util.Optional; - import io.fabric8.kubernetes.api.model.HasMetadata; -public interface ErrorStatusHandler { +public interface ErrorStatusHandler

{ /** *

* Reconciler can implement this interface in order to update the status sub-resource in the case * an exception in thrown. In that case - * {@link #updateErrorStatus(HasMetadata, RetryInfo, RuntimeException)} is called automatically. + * {@link #updateErrorStatus(HasMetadata, Context, Exception)} is called automatically. *

* The result of the method call is used to make a status update on the custom resource. This is * always a sub-resource update request, so no update on custom resource itself (like spec of @@ -21,10 +19,10 @@ public interface ErrorStatusHandler { * should not be updates on custom resource after it is marked for deletion. * * @param resource to update the status on - * @param retryInfo the current retry status + * @param context the current context * @param e exception thrown from the reconciler * @return the updated resource */ - Optional updateErrorStatus(T resource, RetryInfo retryInfo, RuntimeException e); + ErrorStatusUpdateControl

updateErrorStatus(P resource, Context

context, Exception e); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java new file mode 100644 index 0000000000..beb8ddc028 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class ErrorStatusUpdateControl

{ + + private final P resource; + private boolean noRetry = false; + + public static ErrorStatusUpdateControl updateStatus(T resource) { + return new ErrorStatusUpdateControl<>(resource); + } + + public static ErrorStatusUpdateControl noStatusUpdate() { + return new ErrorStatusUpdateControl<>(null); + } + + private ErrorStatusUpdateControl(P resource) { + this.resource = resource; + } + + /** + * Instructs the controller to not retry the error. This is useful for non-recoverable errors. + * + * @return ErrorStatusUpdateControl + */ + public ErrorStatusUpdateControl

withNoRetry() { + this.noRetry = true; + return this; + } + + public Optional

getResource() { + return Optional.ofNullable(resource); + } + + public boolean isNoRetry() { + return noRetry; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 0be4d58ae5..446df36ca1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -13,7 +13,7 @@ public interface Reconciler { * @return UpdateControl to manage updates on the custom resource (usually the status) after * reconciliation. */ - UpdateControl reconcile(R resource, Context context); + UpdateControl reconcile(R resource, Context context) throws Exception; /** * Note that this method is used in combination with finalizers. If automatic finalizer handling diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index d769324be9..5fc1b9ede7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -60,32 +60,36 @@ public Controller(Reconciler

reconciler, public DeleteControl cleanup(P resource, Context

context) { dependents.cleanup(resource, context); - return metrics().timeControllerExecution( - new ControllerExecution<>() { - @Override - public String name() { - return "cleanup"; - } + try { + return metrics().timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "cleanup"; + } - @Override - public String controllerName() { - return configuration.getName(); - } + @Override + public String controllerName() { + return configuration.getName(); + } - @Override - public String successTypeName(DeleteControl deleteControl) { - return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; - } + @Override + public String successTypeName(DeleteControl deleteControl) { + return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; + } - @Override - public DeleteControl execute() { - return reconciler.cleanup(resource, context); - } - }); + @Override + public DeleteControl execute() { + return reconciler.cleanup(resource, context); + } + }); + } catch (Exception e) { + throw new OperatorException(e); + } } @Override - public UpdateControl

reconcile(P resource, Context

context) { + public UpdateControl

reconcile(P resource, Context

context) throws Exception { dependents.reconcile(resource, context); return metrics().timeControllerExecution( @@ -113,7 +117,7 @@ public String successTypeName(UpdateControl

result) { } @Override - public UpdateControl

execute() { + public UpdateControl

execute() throws Exception { return reconciler.reconcile(resource, context); } }); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index e44df906d8..4e7ffa4660 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -242,7 +242,7 @@ TimerEventSource retryEventSource() { * according to the retry timing if there was an exception. */ private void handleRetryOnException( - ExecutionScope executionScope, RuntimeException exception) { + ExecutionScope executionScope, Exception exception) { RetryExecution execution = getOrInitRetryExecution(executionScope); var customResourceID = executionScope.getCustomResourceID(); boolean eventPresent = eventMarker.eventPresent(customResourceID); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java index 5d9a623e20..1171a6d03c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java @@ -8,14 +8,14 @@ final class PostExecutionControl { private final boolean onlyFinalizerHandled; private final R updatedCustomResource; - private final RuntimeException runtimeException; + private final Exception runtimeException; private Long reScheduleDelay = null; private PostExecutionControl( boolean onlyFinalizerHandled, R updatedCustomResource, - RuntimeException runtimeException) { + Exception runtimeException) { this.onlyFinalizerHandled = onlyFinalizerHandled; this.updatedCustomResource = updatedCustomResource; this.runtimeException = runtimeException; @@ -35,7 +35,7 @@ public static PostExecutionControl customResourceUpda } public static PostExecutionControl exceptionDuringExecution( - RuntimeException exception) { + Exception exception) { return new PostExecutionControl<>(false, null, exception); } @@ -60,7 +60,7 @@ public PostExecutionControl withReSchedule(long delay) { return this; } - public Optional getRuntimeException() { + public Optional getRuntimeException() { return Optional.ofNullable(runtimeException); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 088592b3a4..180fbc2d28 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -48,13 +48,14 @@ public ReconciliationDispatcher(Controller controller) { public PostExecutionControl handleExecution(ExecutionScope executionScope) { try { return handleDispatch(executionScope); - } catch (RuntimeException e) { + } catch (Exception e) { log.error("Error during event processing {} failed.", executionScope, e); return PostExecutionControl.exceptionDuringExecution(e); } } - private PostExecutionControl handleDispatch(ExecutionScope executionScope) { + private PostExecutionControl handleDispatch(ExecutionScope executionScope) + throws Exception { R resource = executionScope.getResource(); log.debug("Handling dispatch for resource {}", getName(resource)); @@ -67,7 +68,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) return PostExecutionControl.defaultDispatch(); } - Context context = new DefaultContext<>(executionScope.getRetryInfo(), controller, resource); + Context context = new DefaultContext<>(executionScope.getRetryInfo(), controller, resource); if (markedForDeletion) { return handleCleanup(resource, context); } else { @@ -91,7 +92,7 @@ private boolean shouldNotDispatchToDelete(R resource) { } private PostExecutionControl handleReconcile( - ExecutionScope executionScope, R originalResource, Context context) { + ExecutionScope executionScope, R originalResource, Context context) throws Exception { if (configuration().useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizer())) { /* @@ -105,11 +106,10 @@ private PostExecutionControl handleReconcile( } else { try { var resourceForExecution = - cloneResourceForErrorStatusHandlerIfNeeded(originalResource, context); + cloneResourceForErrorStatusHandlerIfNeeded(originalResource); return reconcileExecution(executionScope, resourceForExecution, originalResource, context); - } catch (RuntimeException e) { - handleErrorStatusHandler(originalResource, context, e); - throw e; + } catch (Exception e) { + return handleErrorStatusHandler(originalResource, context, e); } } } @@ -121,7 +121,7 @@ private PostExecutionControl handleReconcile( * resource is changed during an execution, and it's much cleaner to have to original resource in * place for status update. */ - private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { + private R cloneResourceForErrorStatusHandlerIfNeeded(R resource) { if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { final var cloner = ConfigurationServiceProvider.instance().getResourceCloner(); @@ -132,7 +132,7 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context } private PostExecutionControl reconcileExecution(ExecutionScope executionScope, - R resourceForExecution, R originalResource, Context context) { + R resourceForExecution, R originalResource, Context context) throws Exception { log.debug( "Reconciling resource {} with version: {} with execution scope: {}", getName(resourceForExecution), @@ -163,8 +163,8 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { } @SuppressWarnings("unchecked") - private void handleErrorStatusHandler(R resource, Context context, - RuntimeException e) { + private PostExecutionControl handleErrorStatusHandler(R resource, Context context, + Exception e) throws Exception { if (isErrorStatusHandlerPresent()) { try { RetryInfo retryInfo = context.getRetryInfo().orElse(new RetryInfo() { @@ -178,13 +178,28 @@ public boolean isLastAttempt() { return controller.getConfiguration().getRetryConfiguration() == null; } }); - var updatedResource = ((ErrorStatusHandler) controller.getReconciler()) - .updateErrorStatus(resource, retryInfo, e); - updatedResource.ifPresent(customResourceFacade::updateStatus); + ((DefaultContext) context).setRetryInfo(retryInfo); + var errorStatusUpdateControl = ((ErrorStatusHandler) controller.getReconciler()) + .updateErrorStatus(resource, context, e); + + R updatedResource = null; + if (errorStatusUpdateControl.getResource().isPresent()) { + updatedResource = + customResourceFacade + .updateStatus(errorStatusUpdateControl.getResource().orElseThrow()); + } + if (errorStatusUpdateControl.isNoRetry()) { + if (updatedResource != null) { + return PostExecutionControl.customResourceUpdated(updatedResource); + } else { + return PostExecutionControl.defaultDispatch(); + } + } } catch (RuntimeException ex) { log.error("Error during error status handling.", ex); } } + throw e; } private boolean isErrorStatusHandlerPresent() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 5e230d0418..2e6ee4aa0d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -22,13 +22,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; @@ -116,7 +110,7 @@ private ReconciliationDispatcher init(R customResourc } @Test - void addFinalizerOnNewResource() { + void addFinalizerOnNewResource() throws Exception { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, never()) @@ -128,7 +122,7 @@ void addFinalizerOnNewResource() { } @Test - void callCreateOrUpdateOnNewResourceIfFinalizerSet() { + void callCreateOrUpdateOnNewResourceIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, times(1)) @@ -136,7 +130,7 @@ void callCreateOrUpdateOnNewResourceIfFinalizerSet() { } @Test - void updatesOnlyStatusSubResourceIfFinalizerSet() { + void updatesOnlyStatusSubResourceIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateStatus(testCustomResource); @@ -148,7 +142,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { } @Test - void updatesBothResourceAndStatusIfFinalizerSet() { + void updatesBothResourceAndStatusIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateResourceAndStatus(testCustomResource); @@ -161,7 +155,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { } @Test - void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { + void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -235,7 +229,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { } @Test - void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { + void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -246,7 +240,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { } @Test - void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { + void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() throws Exception { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -269,7 +263,8 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { } @Test - void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() { + void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() + throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -278,7 +273,7 @@ void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet } @Test - void propagatesRetryInfoToContextIfFinalizerSet() { + void propagatesRetryInfoToContextIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution( @@ -307,7 +302,7 @@ public boolean isLastAttempt() { } @Test - void setReScheduleToPostExecutionControlFromUpdateControl() { + void setReScheduleToPostExecutionControlFromUpdateControl() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = @@ -336,7 +331,7 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { } @Test - void setObservedGenerationForStatusIfNeeded() { + void setObservedGenerationForStatusIfNeeded() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); @@ -358,7 +353,7 @@ void setObservedGenerationForStatusIfNeeded() { } @Test - void updatesObservedGenerationOnNoUpdateUpdateControl() { + void updatesObservedGenerationOnNoUpdateUpdateControl() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); @@ -379,7 +374,7 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { } @Test - void updateObservedGenerationOnCustomResourceUpdate() { + void updateObservedGenerationOnCustomResourceUpdate() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); @@ -401,7 +396,7 @@ void updateObservedGenerationOnCustomResourceUpdate() { } @Test - void callErrorStatusHandlerIfImplemented() { + void callErrorStatusHandlerIfImplemented() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> { @@ -409,7 +404,7 @@ void callErrorStatusHandlerIfImplemented() { }; reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); - return Optional.of(testCustomResource); + return ErrorStatusUpdateControl.updateStatus(testCustomResource); }; reconciliationDispatcher.handleExecution( @@ -433,7 +428,7 @@ public boolean isLastAttempt() { } @Test - void callErrorStatusHandlerEvenOnFirstError() { + void callErrorStatusHandlerEvenOnFirstError() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> { @@ -441,19 +436,62 @@ void callErrorStatusHandlerEvenOnFirstError() { }; reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); - return Optional.of(testCustomResource); + return ErrorStatusUpdateControl.updateStatus(testCustomResource); }; - reconciliationDispatcher.handleExecution( + var postExecControl = reconciliationDispatcher.handleExecution( + new ExecutionScope( + testCustomResource, null)); + verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), + any(), any()); + assertThat(postExecControl.exceptionDuringExecution()).isTrue(); + } + + @Test + void errorHandlerCanInstructNoRetryWithUpdate() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { + testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); + return ErrorStatusUpdateControl.updateStatus(testCustomResource).withNoRetry(); + }; + + var postExecControl = reconciliationDispatcher.handleExecution( new ExecutionScope( testCustomResource, null)); + + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), + any(), any()); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + assertThat(postExecControl.exceptionDuringExecution()).isFalse(); + } + + @Test + void errorHandlerCanInstructNoRetryNoUpdate() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { + testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); + return ErrorStatusUpdateControl.noStatusUpdate().withNoRetry(); + }; + + var postExecControl = reconciliationDispatcher.handleExecution( + new ExecutionScope( + testCustomResource, null)); + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); + verify(customResourceFacade, times(0)).updateStatus(testCustomResource); + assertThat(postExecControl.exceptionDuringExecution()).isFalse(); } @Test - void schedulesReconciliationIfMaxDelayIsSet() { + void schedulesReconciliationIfMaxDelayIsSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -466,7 +504,7 @@ void schedulesReconciliationIfMaxDelayIsSet() { } @Test - void canSkipSchedulingMaxDelayIf() { + void canSkipSchedulingMaxDelayIf() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -524,10 +562,11 @@ public DeleteControl cleanup(TestCustomResource resource, Context context) { } @Override - public Optional updateErrorStatus(TestCustomResource resource, - RetryInfo retryInfo, RuntimeException e) { - return errorHandler != null ? errorHandler.updateErrorStatus(resource, retryInfo, e) - : Optional.empty(); + public ErrorStatusUpdateControl updateErrorStatus( + TestCustomResource resource, + Context context, Exception e) { + return errorHandler != null ? errorHandler.updateErrorStatus(resource, context, e) + : ErrorStatusUpdateControl.noStatusUpdate(); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index c3b8632e02..17ea106104 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample.errorstatushandler; -import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; @@ -47,11 +46,13 @@ public int getNumberOfExecutions() { } @Override - public Optional updateErrorStatus( - ErrorStatusHandlerTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + ErrorStatusHandlerTestCustomResource resource, + Context context, Exception e) { log.info("Setting status."); ensureStatusExists(resource); - resource.getStatus().getMessages().add(ERROR_STATUS_MESSAGE + retryInfo.getAttemptCount()); - return Optional.of(resource); + resource.getStatus().getMessages() + .add(ERROR_STATUS_MESSAGE + context.getRetryInfo().orElseThrow().getAttemptCount()); + return ErrorStatusUpdateControl.updateStatus(resource); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index aa8689e471..5e3d41b32f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -65,14 +65,15 @@ public KubernetesClient getKubernetesClient() { } @Override - public Optional updateErrorStatus( - StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + StandaloneDependentTestCustomResource resource, + Context context, Exception e) { // this can happen when a namespace is terminated in test if (e instanceof KubernetesClientException) { - return Optional.empty(); + return ErrorStatusUpdateControl.noStatusUpdate(); } errorOccurred = true; - return Optional.empty(); + return ErrorStatusUpdateControl.noStatusUpdate(); } public boolean isErrorOccurred() { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index eb66f918ab..b52d7931da 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -1,17 +1,10 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Optional; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource; @@ -50,15 +43,16 @@ public UpdateControl reconcile(MySQLSchema schema, Context updateErrorStatus(MySQLSchema schema, RetryInfo retryInfo, - RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus(MySQLSchema schema, + Context context, + Exception e) { SchemaStatus status = new SchemaStatus(); status.setUrl(null); status.setUserName(null); status.setSecretName(null); status.setStatus("ERROR: " + e.getMessage()); schema.setStatus(status); - return Optional.empty(); + return ErrorStatusUpdateControl.updateStatus(schema); } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index ef0c2f5d17..bc7bb83894 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -210,9 +210,9 @@ private static String serviceName(WebPage nginx) { } @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context context, Exception e) { resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + return ErrorStatusUpdateControl.updateStatus(resource); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index 00a0d50d3b..ad3ef36840 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -75,10 +75,10 @@ private WebPageStatus createStatus(String configMapName) { } @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context retryInfo, Exception e) { resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + return ErrorStatusUpdateControl.updateStatus(resource); } private void createDependentResources(KubernetesClient client) { @@ -91,7 +91,7 @@ private void createDependentResources(KubernetesClient client) { new CrudKubernetesDependentResource<>() { @Override - protected Deployment desired(WebPage webPage, Context context) { + protected Deployment desired(WebPage webPage, Context context) { var deploymentName = deploymentName(webPage); Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); deployment.getMetadata().setName(deploymentName); From 83393ad4beb1f03e5543505209f0e43806241324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20CROCQUESEL?= <88554524+scrocquesel@users.noreply.github.com> Date: Tue, 15 Mar 2022 21:01:29 +0100 Subject: [PATCH 0078/1319] refactor: fix Context is a raw type (#1037) * refactor: fix Context is a raw type * style: reformat --- .../operator/api/reconciler/ContextInitializer.java | 2 +- .../external/AbstractSimpleDependentResource.java | 2 +- .../kubernetes/KubernetesDependentResource.java | 2 +- .../processing/event/ReconciliationDispatcher.java | 2 +- .../javaoperatorsdk/operator/api/config/UtilsTest.java | 2 +- .../external/AbstractSimpleDependentResourceTest.java | 10 ++++++---- .../event/source/CustomResourceSelectorTest.java | 2 +- .../operator/sample/simple/DuplicateCRController.java | 2 +- .../operator/sample/simple/TestCustomReconciler.java | 4 ++-- .../sample/simple/TestCustomReconcilerOtherV1.java | 2 +- .../runtime/AnnotationControllerConfigurationTest.java | 4 ++-- .../runtime/DefaultConfigurationServiceTest.java | 6 +++--- .../CreateUpdateEventFilterTestReconciler.java | 3 ++- .../customfilter/CustomFilteringTestReconciler.java | 2 +- .../sample/deployment/DeploymentReconciler.java | 2 +- .../doubleupdate/DoubleUpdateTestCustomReconciler.java | 2 +- .../sample/event/EventSourceTestCustomReconciler.java | 2 +- .../InformerEventSourceTestCustomReconciler.java | 3 ++- .../sample/maxinterval/MaxIntervalTestReconciler.java | 2 +- .../MultiVersionCRDTestReconciler1.java | 3 ++- .../MultiVersionCRDTestReconciler2.java | 3 ++- .../ObservedGenerationTestReconciler.java | 3 ++- .../operator/sample/simple/TestReconciler.java | 4 ++-- .../StandaloneDependentTestReconciler.java | 3 ++- .../subresource/SubResourceTestCustomReconciler.java | 2 +- .../sample/dependent/SecretDependentResource.java | 4 ++-- .../operator/sample/DeploymentDependentResource.java | 2 +- .../operator/sample/ServiceDependentResource.java | 2 +- .../sample/WebPageReconcilerDependentResources.java | 7 ++++--- .../operator/sample/CustomServiceReconciler.java | 4 ++-- 30 files changed, 51 insertions(+), 42 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java index e076723d0c..c1213cce67 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java @@ -3,5 +3,5 @@ import io.fabric8.kubernetes.api.model.HasMetadata; public interface ContextInitializer

{ - void initContext(P primary, Context context); + void initContext(P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 096e4265f6..850b290c12 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -70,7 +70,7 @@ protected R handleUpdate(R actual, R desired, P primary, Context

context) { return res; } - public Matcher.Result match(R actualResource, P primary, Context context) { + public Matcher.Result match(R actualResource, P primary, Context

context) { return matcher.match(actualResource, primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index a471df8c09..7ceadaf13c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -99,7 +99,7 @@ public R update(R actual, R target, P primary, Context

context) { return prepare(target, primary, "Updating").replace(updatedActual); } - public Result match(R actualResource, P primary, Context context) { + public Result match(R actualResource, P primary, Context

context) { return matcher.match(actualResource, primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 180fbc2d28..6ca6959bbb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -258,7 +258,7 @@ private void updatePostExecutionControlWithReschedule( } - private PostExecutionControl handleCleanup(R resource, Context context) { + private PostExecutionControl handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index fbaa28ba23..8895922e82 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -79,7 +79,7 @@ public static class TestKubernetesDependentResource extends KubernetesDependentResource { @Override - protected Deployment desired(TestCustomResource primary, Context context) { + protected Deployment desired(TestCustomResource primary, Context context) { return null; } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java index 00557e7e61..b654ac5d1e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java @@ -113,7 +113,8 @@ public Optional fetchResource(HasMetadata primaryResourc @Override public SampleExternalResource create( - SampleExternalResource desired, TestCustomResource primary, Context context) { + SampleExternalResource desired, TestCustomResource primary, + Context context) { return SampleExternalResource.testResource1(); } @@ -122,15 +123,16 @@ public SampleExternalResource update( SampleExternalResource actual, SampleExternalResource desired, TestCustomResource primary, - Context context) { + Context context) { return SampleExternalResource.testResource1(); } @Override - public void delete(TestCustomResource primary, Context context) {} + public void delete(TestCustomResource primary, Context context) {} @Override - protected SampleExternalResource desired(TestCustomResource primary, Context context) { + protected SampleExternalResource desired(TestCustomResource primary, + Context context) { return SampleExternalResource.testResource1(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index e995d3b9c4..fd08a614af 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -151,7 +151,7 @@ public MyController(Consumer consumer) { @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { LOGGER.info("Received event on: {}", resource); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java index 86f0c36261..6b5b6ab08d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java @@ -10,7 +10,7 @@ public class DuplicateCRController implements Reconciler { @Override public UpdateControl reconcile(TestCustomResource resource, - Context context) { + Context context) { return UpdateControl.noUpdate(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java index ec0e9bffbd..9595f3c13f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java @@ -36,7 +36,7 @@ public TestCustomReconciler(KubernetesClient kubernetesClient, boolean updateSta @Override public DeleteControl cleanup( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { Boolean delete = kubernetesClient .configMaps() @@ -59,7 +59,7 @@ public DeleteControl cleanup( @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java index 9a45fbbf93..97f2fb9098 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java @@ -10,7 +10,7 @@ public class TestCustomReconcilerOtherV1 implements Reconciler reconcile(TestCustomResourceOtherV1 resource, - Context context) { + Context context) { return UpdateControl.noUpdate(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java index b9556aba1c..fc44c3622e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -43,7 +43,7 @@ private static class OneDepReconciler implements Reconciler { public static final String CONFIGURED_NS = "foo"; @Override - public UpdateControl reconcile(ConfigMap resource, Context context) { + public UpdateControl reconcile(ConfigMap resource, Context context) { return null; } } @@ -51,7 +51,7 @@ public UpdateControl reconcile(ConfigMap resource, Context context) { private static class NoDepReconciler implements Reconciler { @Override - public UpdateControl reconcile(ConfigMap resource, Context context) { + public UpdateControl reconcile(ConfigMap resource, Context context) { return null; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 6fd626b94b..d123aa4899 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -106,7 +106,7 @@ static class TestCustomFinalizerReconciler @Override public UpdateControl reconcile( - InnerCustomResource resource, Context context) { + InnerCustomResource resource, Context context) { return null; } @@ -123,7 +123,7 @@ static class NotAutomaticallyCreated implements Reconciler { @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { return null; } } @@ -133,7 +133,7 @@ static class TestCustomReconciler implements Reconciler { @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { return null; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index 015f7a18bd..19e00e8ba2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -30,7 +30,8 @@ public class CreateUpdateEventFilterTestReconciler @Override public UpdateControl reconcile( - CreateUpdateEventFilterTestCustomResource resource, Context context) { + CreateUpdateEventFilterTestCustomResource resource, + Context context) { numberOfExecutions.incrementAndGet(); ConfigMap configMap = diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java index 2a13dc3092..1e42e8e6e1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java @@ -14,7 +14,7 @@ public class CustomFilteringTestReconciler implements Reconciler reconcile(CustomFilteringTestResource resource, - Context context) { + Context context) { numberOfExecutions.incrementAndGet(); return UpdateControl.noUpdate(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java index 3992920884..62697d7e88 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java @@ -26,7 +26,7 @@ public class DeploymentReconciler @Override public UpdateControl reconcile( - Deployment resource, Context context) { + Deployment resource, Context context) { log.info("Reconcile deployment: {}", resource); numberOfExecutions.incrementAndGet(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java index bd398d0a1c..11f0a54f3e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java @@ -24,7 +24,7 @@ public class DoubleUpdateTestCustomReconciler @Override public UpdateControl reconcile( - DoubleUpdateTestCustomResource resource, Context context) { + DoubleUpdateTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); log.info("Value: " + resource.getSpec().getValue()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index 55eb61634f..cc4d083372 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -19,7 +19,7 @@ public class EventSourceTestCustomReconciler @Override public UpdateControl reconcile( - EventSourceTestCustomResource resource, Context context) { + EventSourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); ensureStatusExists(resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index d61aa7f0f2..97a99eed35 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -48,7 +48,8 @@ public List prepareEventSources( @Override public UpdateControl reconcile( - InformerEventSourceTestCustomResource resource, Context context) { + InformerEventSourceTestCustomResource resource, + Context context) { numberOfExecutions.incrementAndGet(); resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java index a5343c27a4..f9a34371ce 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java @@ -18,7 +18,7 @@ public class MaxIntervalTestReconciler @Override public UpdateControl reconcile( - MaxIntervalTestCustomResource resource, Context context) { + MaxIntervalTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); return UpdateControl.noUpdate(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java index ab8ad417be..de733ec6db 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java @@ -18,7 +18,8 @@ public class MultiVersionCRDTestReconciler1 @Override public UpdateControl reconcile( - MultiVersionCRDTestCustomResource1 resource, Context context) { + MultiVersionCRDTestCustomResource1 resource, + Context context) { log.info("Reconcile MultiVersionCRDTestCustomResource1: {}", resource.getMetadata().getName()); resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java index d25297d1c6..138647029e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java @@ -20,7 +20,8 @@ public class MultiVersionCRDTestReconciler2 @Override public UpdateControl reconcile( - MultiVersionCRDTestCustomResource2 resource, Context context) { + MultiVersionCRDTestCustomResource2 resource, + Context context) { log.info("Reconcile MultiVersionCRDTestCustomResource2: {}", resource.getMetadata().getName()); resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java index 65d7be4851..7a7964623f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -15,7 +15,8 @@ public class ObservedGenerationTestReconciler @Override public UpdateControl reconcile( - ObservedGenerationTestCustomResource resource, Context context) { + ObservedGenerationTestCustomResource resource, + Context context) { log.info("Reconcile ObservedGenerationTestCustomResource: {}", resource.getMetadata().getName()); return UpdateControl.updateStatus(resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 94a7d36868..146c96c7db 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -59,7 +59,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { @Override public DeleteControl cleanup( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { Boolean delete = kubernetesClient .configMaps() @@ -82,7 +82,7 @@ public DeleteControl cleanup( @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 5e3d41b32f..4a236e6e00 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -39,7 +39,8 @@ public List prepareEventSources( @Override public UpdateControl reconcile( - StandaloneDependentTestCustomResource primary, Context context) { + StandaloneDependentTestCustomResource primary, + Context context) { deploymentDependent.reconcile(primary, context); Optional deployment = deploymentDependent.getResource(primary); if (deployment.isEmpty()) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 8bfe6dd67d..e520e94411 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -29,7 +29,7 @@ public class SubResourceTestCustomReconciler @Override public UpdateControl reconcile( - SubResourceTestCustomResource resource, Context context) { + SubResourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 1b26eaf47a..15fff4d65e 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -27,7 +27,7 @@ private static String encode(String value) { } @Override - protected Secret desired(MySQLSchema schema, Context context) { + protected Secret desired(MySQLSchema schema, Context context) { final var password = RandomStringUtils .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here final var name = schema.getMetadata().getName(); @@ -49,7 +49,7 @@ private String getSecretName(String name) { } @Override - public Result match(Secret actual, MySQLSchema primary, Context context) { + public Result match(Secret actual, MySQLSchema primary, Context context) { final var desiredSecretName = getSecretName(primary.getMetadata().getName()); return Result.nonComputed(actual.getMetadata().getName().equals(desiredSecretName)); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 56b80b6c82..b408a07ac9 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -19,7 +19,7 @@ private static String tomcatImage(Tomcat tomcat) { } @Override - protected Deployment desired(Tomcat tomcat, Context context) { + protected Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 66ddebf31e..3b618906ab 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -13,7 +13,7 @@ public class ServiceDependentResource extends KubernetesDependentResource, Updater { @Override - protected Service desired(Tomcat tomcat, Context context) { + protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); return new ServiceBuilder(ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml")) .editMetadata() diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index ad3ef36840..d9efec3b19 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -51,7 +51,7 @@ public List prepareEventSources(EventSourceContext context } @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + public UpdateControl reconcile(WebPage webPage, Context context) { if (webPage.getSpec().getHtml().contains("error")) { // special case just to showcase error if doing a demo throw new ErrorSimulationException("Simulating error"); @@ -166,7 +166,7 @@ private class ConfigMapDependentResource PrimaryToSecondaryMapper { @Override - protected ConfigMap desired(WebPage webPage, Context context) { + protected ConfigMap desired(WebPage webPage, Context context) { Map data = new HashMap<>(); data.put("index.html", webPage.getSpec().getHtml()); return new ConfigMapBuilder() @@ -180,7 +180,8 @@ protected ConfigMap desired(WebPage webPage, Context context) { } @Override - public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, + Context context) { var res = super.update(actual, target, primary, context); var ns = actual.getMetadata().getNamespace(); log.info("Restarting pods because HTML has changed in {}", ns); diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index 5e3348fadd..a899c50756 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -30,14 +30,14 @@ public CustomServiceReconciler(KubernetesClient kubernetesClient) { } @Override - public DeleteControl cleanup(CustomService resource, Context context) { + public DeleteControl cleanup(CustomService resource, Context context) { log.info("Cleaning up for: {}", resource.getMetadata().getName()); return Reconciler.super.cleanup(resource, context); } @Override public UpdateControl reconcile( - CustomService resource, Context context) { + CustomService resource, Context context) { log.info("Reconciling: {}", resource.getMetadata().getName()); ServicePort servicePort = new ServicePort(); From 8a9cb345d5c6adcdd5dfe40e5306c5222b0a03c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Mar 2022 09:28:44 +0100 Subject: [PATCH 0079/1319] doc: fix typo (#1041) --- docs/documentation/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 95924e24dd..60f3756440 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -196,7 +196,7 @@ custom resource, the reconciliation could be skipped. This is supported out of t default is not triggered if the change to the main custom resource does not increase the `.metadata.generation` field. Note that the increase of `.metada.generation` is handled automatically by Kubernetes. -To turn on this feature set `generationAwareEventProcessing` to `false` for the `Reconciler`. +To turn off this feature set `generationAwareEventProcessing` to `false` for the `Reconciler`. ## Support for Well Known (non-custom) Kubernetes Resources From 4b3178f6546494faff048b9dcef1e00244586fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 17 Mar 2022 09:42:30 +0100 Subject: [PATCH 0080/1319] fix: flaky test (#1044) --- .../operator/processing/event/EventProcessor.java | 4 ++++ .../processing/event/EventProcessorTest.java | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 4e7ffa4660..96d6a28fb2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -364,4 +364,8 @@ public String toString() { return controllerName + " -> " + executionScope; } } + + public synchronized boolean isUnderProcessing(ResourceID resourceID) { + return underProcessing.contains(resourceID); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 0e968e13a7..0a124ea21d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -24,6 +25,7 @@ import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; @@ -152,16 +154,20 @@ void successfulExecutionResetsTheRetry() { eventProcessorWithRetry.handleEvent(event); verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) .handleExecution(any()); + waitUntilProcessingFinished(eventProcessorWithRetry, event.getRelatedCustomResourceID()); eventProcessorWithRetry.handleEvent(event); verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) .handleExecution(any()); + waitUntilProcessingFinished(eventProcessorWithRetry, event.getRelatedCustomResourceID()); eventProcessorWithRetry.handleEvent(event); verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(3)) .handleExecution(executionScopeArgumentCaptor.capture()); + waitUntilProcessingFinished(eventProcessorWithRetry, event.getRelatedCustomResourceID()); log.info("Finished successfulExecutionResetsTheRetry"); + List executionScopes = executionScopeArgumentCaptor.getAllValues(); assertThat(executionScopes).hasSize(3); @@ -171,6 +177,12 @@ void successfulExecutionResetsTheRetry() { assertThat(executionScopes.get(1).getRetryInfo().isLastAttempt()).isEqualTo(false); } + private void waitUntilProcessingFinished(EventProcessor eventProcessor, + ResourceID relatedCustomResourceID) { + await().atMost(Duration.ofSeconds(3)) + .until(() -> !eventProcessor.isUnderProcessing(relatedCustomResourceID)); + } + @Test void scheduleTimedEventIfInstructedByPostExecutionControl() { var testDelay = 10000L; From c28078047846f2c0e315d8e107c597b63d4c6913 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Mar 2022 07:31:45 +0100 Subject: [PATCH 0081/1319] chore(deps): bump micrometer-core from 1.8.3 to 1.8.4 (#1045) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 965eb5206d..58c3e2fb5f 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.22.0 4.2.0 2.6.4 - 1.8.3 + 1.8.4 2.11 3.10.1 From f408a14b4e9a1285b5c2e6f4d7cd75cd57876660 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 18 Mar 2022 15:31:12 +0100 Subject: [PATCH 0082/1319] feat: improvements for Quarkus extension (#1043) * feat: extract DependentResource instantiation to DependentResourceFactory * feat: add AbstractPollingDependentResource * feat: add ResourceTypeAware interface * refactor: create DependentResources in Controller and remove DependentResourceManager --- .../api/config/ConfigurationService.java | 6 + .../dependent/AbstractDependentResource.java | 2 + .../dependent/DependentResourceFactory.java | 19 +++ .../dependent/ResourceTypeAware.java | 6 + .../operator/processing/Controller.java | 57 +++++++-- .../dependent/DependentResourceManager.java | 112 ------------------ .../AbstractCachingDependentResource.java | 15 ++- .../AbstractPollingDependentResource.java | 27 +++++ .../PerResourcePollingDependentResource.java | 27 +---- .../external/PollingDependentResource.java | 20 +--- .../CrudKubernetesDependentResource.java | 8 +- .../KubernetesDependentResource.java | 18 +-- .../operator/api/config/UtilsTest.java | 35 +++++- .../GenericKubernetesResourceMatcherTest.java | 2 +- .../sample/readonly/ReadOnlyDependent.java | 4 + .../StandaloneDependentTestReconciler.java | 13 +- .../dependent/SchemaDependentResource.java | 7 +- .../dependent/SecretDependentResource.java | 4 + .../sample/DeploymentDependentResource.java | 4 + .../sample/ServiceDependentResource.java | 4 + .../WebPageReconcilerDependentResources.java | 39 +++--- 21 files changed, 235 insertions(+), 194 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 150af208bd..f13ae118f8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +20,7 @@ public interface ConfigurationService { ObjectMapper OBJECT_MAPPER = new ObjectMapper(); Cloner DEFAULT_CLONER = new Cloner() { + @SuppressWarnings("unchecked") @Override public HasMetadata clone(HasMetadata object) { try { @@ -126,4 +128,8 @@ default boolean closeClientOnStop() { default ObjectMapper getObjectMapper() { return OBJECT_MAPPER; } + + default DependentResourceFactory dependentResourceFactory() { + return new DependentResourceFactory() {}; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 0a750d836c..799355185c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -125,6 +125,7 @@ private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() return (RecentOperationCacheFiller) ((EventSourceProvider

) this).getEventSource(); } + @SuppressWarnings("unchecked") // this cannot be done in constructor since event source might be initialized later protected boolean isFilteringEventSource() { if (this instanceof EventSourceProvider) { @@ -135,6 +136,7 @@ protected boolean isFilteringEventSource() { } } + @SuppressWarnings("unchecked") // this cannot be done in constructor since event source might be initialized later protected boolean isRecentOperationCacheFiller() { if (this instanceof EventSourceProvider) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java new file mode 100644 index 0000000000..ffd0ba496f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.lang.reflect.InvocationTargetException; + +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; + +public interface DependentResourceFactory { + + default > T createFrom(DependentResourceSpec spec) { + try { + return spec.getDependentResourceClass().getConstructor().newInstance(); + } catch (InstantiationException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException e) { + throw new IllegalArgumentException("Cannot instantiate DependentResource " + + spec.getDependentResourceClass().getCanonicalName(), e); + } + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java new file mode 100644 index 0000000000..e83d39aebd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +public interface ResourceTypeAware { + + Class resourceType(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 5fc1b9ede7..de71d54171 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -2,6 +2,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,9 +19,11 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; @@ -28,11 +31,13 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.dependent.DependentResourceManager; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -@SuppressWarnings({"unchecked"}) +@SuppressWarnings({"unchecked", "rawtypes"}) @Ignore public class Controller

implements Reconciler

, LifecycleAware, EventSourceInitializer

{ @@ -43,7 +48,8 @@ public class Controller

implements Reconciler

, private final ControllerConfiguration

configuration; private final KubernetesClient kubernetesClient; private final EventSourceManager

eventSourceManager; - private final DependentResourceManager

dependents; + private final List dependents; + private final boolean contextInitializer; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -51,14 +57,42 @@ public Controller(Reconciler

reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + contextInitializer = reconciler instanceof ContextInitializer; eventSourceManager = new EventSourceManager<>(this); - dependents = new DependentResourceManager<>(this); + + dependents = configuration.getDependentResources().stream() + .map(drs -> createAndConfigureFrom(drs, kubernetesClient)) + .collect(Collectors.toList()); + } + + @SuppressWarnings("rawtypes") + private DependentResource createAndConfigureFrom(DependentResourceSpec spec, + KubernetesClient client) { + final var dependentResource = + ConfigurationServiceProvider.instance().dependentResourceFactory().createFrom(spec); + + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); + } + + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + spec.getDependentResourceConfiguration().ifPresent(configurator::configureWith); + } + return dependentResource; + } + + private void initContextIfNeeded(P resource, Context

context) { + if (contextInitializer) { + ((ContextInitializer

) reconciler).initContext(resource, context); + } } @Override public DeleteControl cleanup(P resource, Context

context) { - dependents.cleanup(resource, context); + initContextIfNeeded(resource, context); + dependents.forEach(dependent -> dependent.cleanup(resource, context)); try { return metrics().timeControllerExecution( @@ -90,7 +124,8 @@ public DeleteControl execute() { @Override public UpdateControl

reconcile(P resource, Context

context) throws Exception { - dependents.reconcile(resource, context); + initContextIfNeeded(resource, context); + dependents.forEach(dependent -> dependent.reconcile(resource, context)); return metrics().timeControllerExecution( new ControllerExecution<>() { @@ -131,8 +166,12 @@ private Metrics metrics() { @Override public List prepareEventSources(EventSourceContext

context) { - final var dependentSources = dependents.prepareEventSources(context); - List sources = new LinkedList<>(dependentSources); + List sources = new LinkedList<>(); + dependents.stream() + .filter(dependentResource -> dependentResource instanceof EventSourceProvider) + .map(EventSourceProvider.class::cast) + .map(provider -> provider.initEventSource(context)) + .forEach(sources::add); // add manually defined event sources if (reconciler instanceof EventSourceInitializer) { @@ -267,6 +306,6 @@ public void stop() { @SuppressWarnings("rawtypes") public List getDependents() { - return dependents.getDependents(); + return dependents; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java deleted file mode 100644 index 3dd3a2860e..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent; - -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -@SuppressWarnings({"rawtypes", "unchecked"}) -@Ignore -public class DependentResourceManager

- implements EventSourceInitializer

, Reconciler

{ - - private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); - - private final Reconciler

reconciler; - private final ControllerConfiguration

controllerConfiguration; - private List dependents; - - public DependentResourceManager(Controller

controller) { - this.reconciler = controller.getReconciler(); - this.controllerConfiguration = controller.getConfiguration(); - } - - @Override - public List prepareEventSources(EventSourceContext

context) { - final var dependentResources = controllerConfiguration.getDependentResources(); - final var sources = new ArrayList(dependentResources.size()); - dependents = - dependentResources.stream() - .map( - drc -> { - final var dependentResource = createAndConfigureFrom(drc, context.getClient()); - if (dependentResource instanceof EventSourceProvider) { - EventSourceProvider provider = (EventSourceProvider) dependentResource; - sources.add(provider.initEventSource(context)); - } - return dependentResource; - }) - .collect(Collectors.toList()); - return sources; - } - - @Override - public UpdateControl

reconcile(P resource, Context

context) { - initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.reconcile(resource, context)); - return UpdateControl.noUpdate(); - } - - @Override - public DeleteControl cleanup(P resource, Context

context) { - initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.cleanup(resource, context)); - return Reconciler.super.cleanup(resource, context); - } - - private void initContextIfNeeded(P resource, Context context) { - if (reconciler instanceof ContextInitializer) { - final var initializer = (ContextInitializer

) reconciler; - initializer.initContext(resource, context); - } - } - - private DependentResource createAndConfigureFrom(DependentResourceSpec dependentResourceSpec, - KubernetesClient client) { - try { - DependentResource dependentResource = - (DependentResource) dependentResourceSpec.getDependentResourceClass() - .getConstructor().newInstance(); - - if (dependentResource instanceof KubernetesClientAware) { - ((KubernetesClientAware) dependentResource).setKubernetesClient(client); - } - - if (dependentResource instanceof DependentResourceConfigurator) { - final var configurator = (DependentResourceConfigurator) dependentResource; - dependentResourceSpec.getDependentResourceConfiguration() - .ifPresent(configurator::configureWith); - } - return dependentResource; - } catch (InstantiationException | NoSuchMethodException | IllegalAccessException - | InvocationTargetException e) { - throw new IllegalStateException(e); - } - } - - public List getDependents() { - return dependents; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java index 86f2c23d54..04b49bf0bf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -3,16 +3,22 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; public abstract class AbstractCachingDependentResource - extends AbstractDependentResource implements EventSourceProvider

{ + extends AbstractDependentResource + implements EventSourceProvider

, ResourceTypeAware { protected ExternalResourceCachingEventSource eventSource; + private final Class resourceType; + + protected AbstractCachingDependentResource(Class resourceType) { + this.resourceType = resourceType; + } public Optional fetchResource(P primaryResource) { return eventSource.getAssociated(primaryResource); @@ -23,8 +29,9 @@ public EventSource getEventSource() { return eventSource; } - protected Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + @Override + public Class resourceType() { + return resourceType; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java new file mode 100644 index 0000000000..8c91dea15d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java @@ -0,0 +1,27 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public abstract class AbstractPollingDependentResource + extends AbstractCachingDependentResource { + + public static final int DEFAULT_POLLING_PERIOD = 5000; + private long pollingPeriod; + + protected AbstractPollingDependentResource(Class resourceType) { + this(resourceType, DEFAULT_POLLING_PERIOD); + } + + public AbstractPollingDependentResource(Class resourceType, long pollingPeriod) { + super(resourceType); + this.pollingPeriod = pollingPeriod; + } + + public void setPollingPeriod(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + } + + public long getPollingPeriod() { + return pollingPeriod; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java index c5f9e2c888..11b18832a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java @@ -5,36 +5,21 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; -import static io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource.DEFAULT_POLLING_PERIOD; - public abstract class PerResourcePollingDependentResource - extends AbstractCachingDependentResource + extends AbstractPollingDependentResource implements PerResourcePollingEventSource.ResourceFetcher { - - protected long pollingPeriod; - - public PerResourcePollingDependentResource() { - this(DEFAULT_POLLING_PERIOD); + public PerResourcePollingDependentResource(Class resourceType) { + super(resourceType); } - public PerResourcePollingDependentResource(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; + public PerResourcePollingDependentResource(Class resourceType, long pollingPeriod) { + super(resourceType, pollingPeriod); } @Override public EventSource initEventSource(EventSourceContext

context) { eventSource = new PerResourcePollingEventSource<>(this, context.getPrimaryCache(), - pollingPeriod, resourceType()); + getPollingPeriod(), resourceType()); return eventSource; } - - public PerResourcePollingDependentResource setPollingPeriod(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; - return this; - } - - public long getPollingPeriod() { - return pollingPeriod; - } - } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java index 18e0c66b60..58723a6f11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java @@ -10,27 +10,19 @@ import io.javaoperatorsdk.operator.processing.event.source.polling.PollingEventSource; public abstract class PollingDependentResource - extends AbstractCachingDependentResource implements Supplier> { + extends AbstractPollingDependentResource implements Supplier> { - public static final int DEFAULT_POLLING_PERIOD = 5000; - protected long pollingPeriod; - - public PollingDependentResource() { - this(DEFAULT_POLLING_PERIOD); + public PollingDependentResource(Class resourceType) { + super(resourceType); } - public PollingDependentResource(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; + public PollingDependentResource(Class resourceType, long pollingPeriod) { + super(resourceType, pollingPeriod); } @Override public EventSource initEventSource(EventSourceContext

context) { - eventSource = new PollingEventSource<>(this, pollingPeriod, resourceType()); + eventSource = new PollingEventSource<>(this, getPollingPeriod(), resourceType()); return eventSource; } - - public PollingDependentResource setPollingPeriod(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; - return this; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java index 88e9bdc7ae..b363e81b51 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java @@ -12,6 +12,10 @@ * @param

Primary Resource */ public abstract class CrudKubernetesDependentResource - extends - KubernetesDependentResource implements Creator, Updater, Deleter

{ + extends KubernetesDependentResource + implements Creator, Updater, Deleter

{ + + public CrudKubernetesDependentResource(Class resourceType) { + super(resourceType); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 7ceadaf13c..9439d74865 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; @@ -21,6 +20,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -31,7 +31,7 @@ public abstract class KubernetesDependentResource extends AbstractDependentResource - implements KubernetesClientAware, EventSourceProvider

, + implements KubernetesClientAware, EventSourceProvider

, ResourceTypeAware, DependentResourceConfigurator { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -41,15 +41,17 @@ public abstract class KubernetesDependentResource matcher; private final ResourceUpdatePreProcessor processor; + private final Class resourceType; @SuppressWarnings("unchecked") - public KubernetesDependentResource() { + public KubernetesDependentResource(Class resourceType) { + this.resourceType = resourceType; matcher = this instanceof Matcher ? (Matcher) this - : GenericKubernetesResourceMatcher.matcherFor(resourceType(), this); + : GenericKubernetesResourceMatcher.matcherFor(resourceType, this); processor = this instanceof ResourceUpdatePreProcessor ? (ResourceUpdatePreProcessor) this - : GenericResourceUpdatePreProcessor.processorFor(resourceType()); + : GenericResourceUpdatePreProcessor.processorFor(resourceType); } @Override @@ -140,9 +142,9 @@ public KubernetesDependentResource setInformerEventSource( return this; } - @SuppressWarnings("unchecked") - protected Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + @Override + public Class resourceType() { + return resourceType; } @Override diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 8895922e82..0a24d4f26d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -1,14 +1,21 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Optional; + import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class UtilsTest { @@ -75,12 +82,32 @@ void getsFirstTypeArgumentFromExtendedClass() { assertThat(res).isEqualTo(Deployment.class); } - public static class TestKubernetesDependentResource - extends KubernetesDependentResource { + @Test + void getsFirstTypeArgumentFromInterface() { + assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class)) + .isEqualTo(Deployment.class); + } + + public static class TestDependentResource + implements DependentResource { @Override - protected Deployment desired(TestCustomResource primary, Context context) { + public ReconcileResult reconcile(TestCustomResource primary, + Context context) { return null; } + + @Override + public Optional getResource(TestCustomResource primaryResource) { + return Optional.empty(); + } + } + + public static class TestKubernetesDependentResource + extends KubernetesDependentResource { + + public TestKubernetesDependentResource() { + super(Deployment.class); + } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 1eb7ea1bb4..bad10d551d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -32,7 +32,7 @@ void checksIfDesiredValuesAreTheSame() { var actual = createDeployment(); final var desired = createDeployment(); final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class, - new KubernetesDependentResource<>() { + new KubernetesDependentResource<>(Deployment.class) { @Override protected Deployment desired(HasMetadata primary, Context context) { final var currentCase = Optional.ofNullable(primary) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java index 0285d47b83..15fb02a0b6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java @@ -4,4 +4,8 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; public class ReadOnlyDependent extends KubernetesDependentResource { + + public ReadOnlyDependent() { + super(ConfigMap.class); + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 4a236e6e00..e740032d6e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -7,7 +7,14 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; @@ -86,6 +93,10 @@ private static class DeploymentDependentResource extends implements Creator, Updater { + public DeploymentDependentResource() { + super(Deployment.class); + } + @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index f5a5e9ea18..273cd086a1 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -16,7 +16,8 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; -import io.javaoperatorsdk.operator.sample.*; +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; +import io.javaoperatorsdk.operator.sample.MySQLSchema; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -35,6 +36,10 @@ public class SchemaDependentResource private MySQLDbConfig dbConfig; + public SchemaDependentResource() { + super(Schema.class); + } + @Override public void configureWith(ResourcePollerConfig config) { this.dbConfig = config.getMySQLDbConfig(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 15fff4d65e..60c4c4d2b0 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -22,6 +22,10 @@ public class SecretDependentResource extends KubernetesDependentResource implements Creator, Updater { + public DeploymentDependentResource() { + super(Deployment.class); + } + private static String tomcatImage(Tomcat tomcat) { return "tomcat:" + tomcat.getSpec().getVersion(); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 3b618906ab..c52553851d 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -12,6 +12,10 @@ public class ServiceDependentResource extends KubernetesDependentResource implements Creator, Updater { + public ServiceDependentResource() { + super(Service.class); + } + @Override protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index d9efec3b19..90f747d158 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -1,15 +1,27 @@ package io.javaoperatorsdk.operator.sample; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CrudKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; @@ -88,7 +100,7 @@ private void createDependentResources(KubernetesClient client) { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.deploymentDR = - new CrudKubernetesDependentResource<>() { + new CrudKubernetesDependentResource<>(Deployment.class) { @Override protected Deployment desired(WebPage webPage, Context context) { @@ -114,18 +126,13 @@ protected Deployment desired(WebPage webPage, Context context) { new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); return deployment; } - - @Override - protected Class resourceType() { - return Deployment.class; - } }; deploymentDR.setKubernetesClient(client); deploymentDR.configureWith(new KubernetesDependentResourceConfig() .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.serviceDR = - new CrudKubernetesDependentResource<>() { + new CrudKubernetesDependentResource<>(Service.class) { @Override protected Service desired(WebPage webPage, Context context) { @@ -137,11 +144,6 @@ protected Service desired(WebPage webPage, Context context) { service.getSpec().setSelector(labels); return service; } - - @Override - protected Class resourceType() { - return Service.class; - } }; serviceDR.setKubernetesClient(client); serviceDR.configureWith(new KubernetesDependentResourceConfig() @@ -162,8 +164,11 @@ public static String serviceName(WebPage webPage) { private class ConfigMapDependentResource extends CrudKubernetesDependentResource - implements - PrimaryToSecondaryMapper { + implements PrimaryToSecondaryMapper { + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } @Override protected ConfigMap desired(WebPage webPage, Context context) { From 392c36562bbc866472c199d5cf68fd4689a3518c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 18 Mar 2022 15:46:04 +0100 Subject: [PATCH 0083/1319] fix: remove deprecated api (#842) --- .../processing/event/ReconciliationDispatcher.java | 2 +- .../operator/SubResourceUpdateIT.java | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 6ca6959bbb..235a27481d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -335,7 +335,7 @@ public R updateStatus(R resource) { return resourceOperation .inNamespace(resource.getMetadata().getNamespace()) .withName(getName(resource)) - .updateStatus(resource); + .replaceStatus(resource); } public R replaceWithLock(R resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 85e11c7287..bd335c9ae7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -70,15 +70,13 @@ void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { } /** - * Not that here status sub-resource update will fail on optimistic locking. This solves a tricky - * situation: If this would not happen (no optimistic locking on status sub-resource) we could - * receive and store an event while processing the controller method. But this event would always - * fail since its resource version is outdated already. + * The update status actually does optimistic locking in the background but fabric8 client retries + * it with an up-to-date resource version. */ @Test void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); - operator.create(SubResourceTestCustomResource.class, resource); + resource = operator.create(SubResourceTestCustomResource.class, resource); // waits for the resource to start processing waitXms(EVENT_RECEIVE_WAIT); @@ -89,9 +87,9 @@ void updateCustomResourceAfterSubResourceChange() { // wait for sure, there are no more events waitXms(WAIT_AFTER_EXECUTION); - // there is no event on status update processed - assertThat(TestUtils.getNumberOfExecutions(operator)) - .isEqualTo(3); + // note that both is valid, since after the update of the status the event receive lags, + // that will result in a third execution + assertThat(TestUtils.getNumberOfExecutions(operator)).isBetween(2, 3); } void awaitStatusUpdated(String name) { From 5a9bbe287952b032e84745329b46b11c8eb3ae1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 18 Mar 2022 20:32:23 +0100 Subject: [PATCH 0084/1319] feat: separate interface for cleanup part of reconciler (#1035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Chris Laprun Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> --- docs/documentation/features.md | 14 ++-- .../operator/ReconcilerUtils.java | 2 +- .../AnnotationControllerConfiguration.java | 2 +- .../api/config/ControllerConfiguration.java | 7 +- .../ControllerConfigurationOverrider.java | 2 +- .../DefaultControllerConfiguration.java | 2 +- .../operator/api/reconciler/Cleaner.java | 30 +++++++ .../operator/api/reconciler/Constants.java | 1 - .../operator/api/reconciler/Context.java | 2 +- .../reconciler/ControllerConfiguration.java | 7 +- .../api/reconciler/DefaultContext.java | 2 +- .../operator/api/reconciler/Reconciler.java | 25 ------ .../api/reconciler/dependent/Deleter.java | 6 ++ .../dependent/DependentResource.java | 2 - .../reconciler/dependent/ReconcileResult.java | 4 +- .../DependentResourceConfigurator.java | 2 +- .../{ => managed}/KubernetesClientAware.java | 2 +- .../ManagedDependentResourceContext.java | 3 +- .../operator/processing/Controller.java | 84 ++++++++++++------- .../dependent/AbstractDependentResource.java | 17 +--- .../dependent/Creator.java | 2 +- .../dependent/DesiredEqualsMatcher.java | 2 +- .../dependent/Matcher.java | 2 +- .../dependent/Updater.java | 4 +- .../AbstractCachingDependentResource.java | 2 +- .../AbstractSimpleDependentResource.java | 12 +-- ...a => CRUDKubernetesDependentResource.java} | 14 ++-- .../CRUKubernetesDependentResource.java | 21 +++++ .../GenericKubernetesResourceMatcher.java | 2 +- .../GenericResourceUpdatePreProcessor.java | 1 - .../KubernetesDependentResource.java | 11 ++- .../ResourceUpdatePreProcessor.java | 2 +- .../event/ReconciliationDispatcher.java | 33 +++----- .../ControllerResourceEventSource.java | 2 +- .../controller/ResourceEventFilter.java | 26 +++--- .../controller/ResourceEventFilters.java | 10 +-- .../operator/ReconcilerUtilsTest.java | 6 -- .../operator/processing/ControllerTest.java | 30 ++++--- .../AbstractSimpleDependentResourceTest.java | 13 +-- ...GenericResourceUpdatePreProcessorTest.java | 1 - .../event/ReconciliationDispatcherTest.java | 15 ++-- .../source/CustomResourceSelectorTest.java | 2 +- .../ControllerResourceEventSourceTest.java | 13 ++- .../sample/simple/TestCustomReconciler.java | 3 +- .../operator/CleanerForReconcilerIT.java | 49 +++++++++++ .../operator/ControllerExecutionIT.java | 17 ++++ ...terForManagedDependentResourcesOnlyIT.java | 54 ++++++++++++ .../operator/EventSourceIT.java | 1 - .../io/javaoperatorsdk/operator/RetryIT.java | 1 - .../operator/SubResourceUpdateIT.java | 2 - .../operator/UpdatingResAndSubResIT.java | 3 - .../DefaultConfigurationServiceTest.java | 4 +- .../CleanerForReconcilerCustomResource.java | 17 ++++ .../CleanerForReconcilerTestReconciler.java | 39 +++++++++ ...anerForManagedDependentCustomResource.java | 17 ++++ ...anerForManagedDependentTestReconciler.java | 28 +++++++ .../ConfigMapDependentResource.java | 44 ++++++++++ ...CreateUpdateEventFilterTestReconciler.java | 4 +- .../sample/customfilter/CustomFlagFilter.java | 4 +- .../customfilter/CustomFlagFilter2.java | 4 +- .../ErrorStatusHandlerTestReconciler.java | 4 +- ...formerEventSourceTestCustomReconciler.java | 4 +- .../MaxIntervalTestReconciler.java | 7 +- .../MultiVersionCRDTestReconciler1.java | 4 +- .../MultiVersionCRDTestReconciler2.java | 6 +- .../ObservedGenerationTestReconciler.java | 9 +- .../retry/RetryTestCustomReconciler.java | 6 -- .../sample/simple/TestReconciler.java | 17 ++-- .../StandaloneDependentTestReconciler.java | 17 +--- .../SubResourceTestCustomReconciler.java | 6 -- .../ReconcilerImplemented2Interfaces.java | 10 +-- .../sample/MySQLSchemaReconciler.java | 4 +- .../dependent/SchemaDependentResource.java | 13 +-- .../dependent/SecretDependentResource.java | 4 +- .../sample/MySQLSchemaOperatorE2E.java | 38 +++++---- .../sample/DeploymentDependentResource.java | 4 +- .../sample/ServiceDependentResource.java | 4 +- .../operator/sample/TomcatReconciler.java | 3 - .../operator/sample/WebappReconciler.java | 15 +--- .../operator/sample/WebPageReconciler.java | 7 +- .../WebPageReconcilerDependentResources.java | 12 +-- .../sample/CustomServiceReconciler.java | 25 +++--- 82 files changed, 604 insertions(+), 348 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/{ => managed}/DependentResourceConfigurator.java (55%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/{ => managed}/KubernetesClientAware.java (69%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/{ => managed}/ManagedDependentResourceContext.java (96%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/AbstractDependentResource.java (92%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/Creator.java (79%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/DesiredEqualsMatcher.java (91%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/Matcher.java (92%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/Updater.java (68%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{CrudKubernetesDependentResource.java => CRUDKubernetesDependentResource.java} (50%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler/dependent => processing/dependent/kubernetes}/ResourceUpdatePreProcessor.java (78%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 3f2a8843c3..7b6143ca02 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -37,9 +37,13 @@ execution. [Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) make sure that a reconciliation happens when a custom resource is instructed to be deleted. Typical case when it's useful, when an operator is down (pod not running). Without a finalizer the reconciliation - thus the cleanup - -i.e. [`Reconciler.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java#L31) +i.e. [`Cleaner.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b82c1f106968cb3eb18835c5e9cd1e4d5c40362e/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java#L28-L28) would not happen if a custom resource is deleted. +To use finalizers the reconciler have to implement [`Cleaner

`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b82c1f106968cb3eb18835c5e9cd1e4d5c40362e/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) interface. +In other words, finalizer is added if the `Reconciler` implements `Cleaner` interface. If not, no +finalizer is added and/or removed. + Finalizers are automatically added by the framework as the first step, thus after a custom resource is created, but before the first reconciliation. The finalizer is added via a separate Kubernetes API call. As a result of this update, the finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. @@ -50,7 +54,6 @@ in some specific corner cases, when there would be a long waiting period for som The name of the finalizers can be specified, in case it is not, a name will be generated. -Automatic finalizer handling can be turned off, so when configured no finalizer will be added or removed. See [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) annotation for more details. @@ -66,7 +69,7 @@ When automatic finalizer handling is turned off, the `Reconciler.cleanup(...)` m case when a delete event received. So it does not make sense to implement this method and turn off finalizer at the same time. -## The `reconcile` and `cleanup` Methods of [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) +## The `reconcile` and [`cleanup`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b82c1f106968cb3eb18835c5e9cd1e4d5c40362e/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) Methods on a [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) The lifecycle of a custom resource can be clearly separated into two phases from the perspective of an operator. When a custom resource is created or update, or on the other hand when the custom resource is deleted - or rather marked for @@ -75,9 +78,8 @@ deletion in case a finalizer is used. This separation-related logic is automatically handled by the framework. The framework will always call `reconcile` method, unless the custom resource is [marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work) -. From the point when the custom resource is marked from deletion, only the `cleanup` method is called. - -If there is **no finalizer** in place (see Finalizer Support section), the `cleanup` method is **not called**. +. From the point when the custom resource is marked from deletion, only the `cleanup` method is called, of course +only if the reconciler implements the `Cleaner` interface. ### Using [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) and [`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 98a43492f9..a99abe4d8d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -42,7 +42,7 @@ public void setApiVersion(String s) { throw new UnsupportedOperationException(); } }; - return Constants.NO_FINALIZER.equals(finalizer) || validator.isFinalizerValid(finalizer); + return validator.isFinalizerValid(finalizer); } public static String getResourceTypeNameWithVersion(Class resourceClass) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 72d34e16a8..eb57d2c71a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -39,7 +39,7 @@ public String getName() { } @Override - public String getFinalizer() { + public String getFinalizerName() { if (annotation == null || annotation.finalizerName().isBlank()) { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 03485412b6..aff1d04e8b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -18,7 +17,7 @@ default String getName() { return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } - default String getFinalizer() { + default String getFinalizerName() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } @@ -32,10 +31,6 @@ default RetryConfiguration getRetryConfiguration() { return RetryConfiguration.DEFAULT; } - default boolean useFinalizer() { - return !Constants.NO_FINALIZER.equals(getFinalizer()); - } - /** * Allow controllers to filter events before they are passed to the * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index 87297b8d49..da0b6d0a52 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -25,7 +25,7 @@ public class ControllerConfigurationOverrider { private final List> dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { - finalizer = original.getFinalizer(); + finalizer = original.getFinalizerName(); generationAware = original.isGenerationAware(); namespaces = new HashSet<>(original.getNamespaces()); retry = original.getRetryConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 4545c9885e..890254d830 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -66,7 +66,7 @@ public String getResourceTypeName() { } @Override - public String getFinalizer() { + public String getFinalizerName() { return finalizer; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java new file mode 100644 index 0000000000..e2e70a246d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface Cleaner

{ + + /** + * Note that this method turns on automatic finalizer usage. + * + * The implementation should delete the associated component(s). This method is called when an + * object is marked for deletion. After it's executed the custom resource finalizer is + * automatically removed by the framework; unless the return value is + * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined + * that the resource should not be deleted yet. This is usually a corner case, when a cleanup is + * tried again eventually. + * + *

+ * It's important for implementations of this method to be idempotent, since it can be called + * several times. + * + * @param resource the resource that is marked for deletion + * @param context the context with which the operation is executed + * @return {@link DeleteControl#defaultDelete()} - so the finalizer is automatically removed after + * the call. {@link DeleteControl#noFinalizerRemoval()} if you don't want to remove the + * finalizer to indicate that the resource should not be deleted after all, in which case + * the controller should restore the resource's state appropriately. + */ + DeleteControl cleanup(P resource, Context

context); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index 85b3a00807..e08e168ca4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -4,7 +4,6 @@ public final class Constants { public static final String EMPTY_STRING = ""; public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; - public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER"; public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L; private Constants() {} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 2c3fb6ff1e..b2c2270f0b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext; public interface Context

{ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index 0feaa34b2d..fe69738b1a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -15,9 +15,10 @@ String name() default Constants.EMPTY_STRING; /** - * Optional finalizer name, if it is not provided, one will be automatically generated. If the - * provided value is the value specified by {@link Constants#NO_FINALIZER}, then no finalizer will - * be added to custom resources. + * Optional finalizer name, if it is not provided, one will be automatically generated. Note that + * finalizers are only added when Reconciler implement {@link Cleaner} interface, or at least one + * managed dependent resource implement + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter} interface. * * @return the finalizer name */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index d80be8b3ae..0a7b36339d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; public class DefaultContext

implements Context

{ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 446df36ca1..771b169fd1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -15,29 +15,4 @@ public interface Reconciler { */ UpdateControl reconcile(R resource, Context context) throws Exception; - /** - * Note that this method is used in combination with finalizers. If automatic finalizer handling - * is turned off for the controller, this method is not called. - * - * The implementation should delete the associated component(s). This method is called when an - * object is marked for deletion. After it's executed the custom resource finalizer is - * automatically removed by the framework; unless the return value is - * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined - * that the resource should not be deleted yet. This is usually a corner case, when a cleanup is - * tried again eventually. - * - *

- * It's important for implementations of this method to be idempotent, since it can be called - * several times. - * - * @param resource the resource that is marked for deletion - * @param context the context with which the operation is executed - * @return {@link DeleteControl#defaultDelete()} - so the finalizer is automatically removed after - * the call. {@link DeleteControl#noFinalizerRemoval()} if you don't want to remove the - * finalizer to indicate that the resource should not be deleted after all, in which case - * the controller should restore the resource's state appropriately. - */ - default DeleteControl cleanup(R resource, Context context) { - return DeleteControl.defaultDelete(); - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java index 0a1bfc1bab..f6bd2682ca 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -3,6 +3,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +/** + * DependentResource can implement this interface to denote it requires explicit logic to clean up + * resources. + * + * @param

primary resource type + */ @FunctionalInterface public interface Deleter

{ void delete(P primary, Context

context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 377e4aea22..1dcfbe57d5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -8,7 +8,5 @@ public interface DependentResource { ReconcileResult reconcile(P primary, Context

context); - default void cleanup(P primary, Context

context) {} - Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java index 392b213e81..86a931c480 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java @@ -4,8 +4,8 @@ public class ReconcileResult { - private R resource; - private Operation operation; + private final R resource; + private final Operation operation; public static ReconcileResult resourceCreated(T resource) { return new ReconcileResult<>(resource, Operation.CREATED); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java similarity index 55% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java index b51de9f35f..bbb4f75da9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; public interface DependentResourceConfigurator { void configureWith(C config); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java similarity index 69% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java index b8ef888ec3..0e787753a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; import io.fabric8.kubernetes.client.KubernetesClient; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java similarity index 96% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java index 9543b7d582..745f1e3151 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; import java.util.Collections; import java.util.List; @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; /** * Contextual information related to {@link DependentResource} either to retrieve the actual diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index de71d54171..7ee63d760f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -22,6 +22,7 @@ import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -30,16 +31,17 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"unchecked", "rawtypes"}) @Ignore -public class Controller

implements Reconciler

, +public class Controller

implements Reconciler

, Cleaner

, LifecycleAware, EventSourceInitializer

{ private static final Logger log = LoggerFactory.getLogger(Controller.class); @@ -50,6 +52,8 @@ public class Controller

implements Reconciler

, private final EventSourceManager

eventSourceManager; private final List dependents; private final boolean contextInitializer; + private final boolean hasDeleterDependents; + private final boolean isCleaner; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -61,9 +65,19 @@ public Controller(Reconciler

reconciler, eventSourceManager = new EventSourceManager<>(this); + final var hasDeleterHolder = new boolean[] {false}; dependents = configuration.getDependentResources().stream() .map(drs -> createAndConfigureFrom(drs, kubernetesClient)) + .peek(d -> { + // check if any dependent implements Deleter to record that fact + if (!hasDeleterHolder[0] && d instanceof Deleter) { + hasDeleterHolder[0] = true; + } + }) .collect(Collectors.toList()); + + hasDeleterDependents = hasDeleterHolder[0]; + isCleaner = reconciler instanceof Cleaner; } @SuppressWarnings("rawtypes") @@ -92,31 +106,40 @@ private void initContextIfNeeded(P resource, Context

context) { @Override public DeleteControl cleanup(P resource, Context

context) { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.cleanup(resource, context)); - try { - return metrics().timeControllerExecution( - new ControllerExecution<>() { - @Override - public String name() { - return "cleanup"; - } - - @Override - public String controllerName() { - return configuration.getName(); - } - - @Override - public String successTypeName(DeleteControl deleteControl) { - return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; - } - - @Override - public DeleteControl execute() { - return reconciler.cleanup(resource, context); - } - }); + return metrics() + .timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "cleanup"; + } + + @Override + public String controllerName() { + return configuration.getName(); + } + + @Override + public String successTypeName(DeleteControl deleteControl) { + return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; + } + + @Override + public DeleteControl execute() { + if (hasDeleterDependents) { + dependents.stream() + .filter(d -> d instanceof Deleter) + .map(Deleter.class::cast) + .forEach(deleter -> deleter.delete(resource, context)); + } + if (isCleaner) { + return ((Cleaner

) reconciler).cleanup(resource, context); + } else { + return DeleteControl.defaultDelete(); + } + } + }); } catch (Exception e) { throw new OperatorException(e); } @@ -125,8 +148,6 @@ public DeleteControl execute() { @Override public UpdateControl

reconcile(P resource, Context

context) throws Exception { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.reconcile(resource, context)); - return metrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -153,6 +174,7 @@ public String successTypeName(UpdateControl

result) { @Override public UpdateControl

execute() throws Exception { + dependents.forEach(dependent -> dependent.reconcile(resource, context)); return reconciler.reconcile(resource, context); } }); @@ -304,6 +326,10 @@ public void stop() { } } + public boolean useFinalizer() { + return isCleaner || hasDeleterDependents; + } + @SuppressWarnings("rawtypes") public List getDependents() { return dependents; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java similarity index 92% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index 799355185c..4758ba5eab 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -1,10 +1,11 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; public abstract class AbstractDependentResource @@ -13,16 +14,13 @@ public abstract class AbstractDependentResource private final boolean creatable = this instanceof Creator; private final boolean updatable = this instanceof Updater; - private final boolean deletable = this instanceof Deleter; protected Creator creator; protected Updater updater; - protected Deleter

deleter; @SuppressWarnings("unchecked") public AbstractDependentResource() { creator = creatable ? (Creator) this : null; updater = updatable ? (Updater) this : null; - deleter = deletable ? (Deleter

) this : null; } @Override @@ -147,13 +145,6 @@ protected boolean isRecentOperationCacheFiller() { } } - @Override - public void cleanup(P primary, Context

context) { - if (isDeletable(primary, context)) { - deleter.delete(primary, context); - } - } - protected R desired(P primary, Context

context) { throw new IllegalStateException( "desired method must be implemented if this DependentResource can be created and/or updated"); @@ -169,8 +160,4 @@ protected boolean isUpdatable(P primary, Context

context) { return updatable; } - @SuppressWarnings("unused") - protected boolean isDeletable(P primary, Context

context) { - return deletable; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Creator.java similarity index 79% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Creator.java index 48af8fb61b..b4143906bd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Creator.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java similarity index 91% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java index 2cf8b155ff..459d7951d6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java similarity index 92% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java index 09e78911e6..84290fe464 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import java.util.Optional; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java similarity index 68% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java index d49ccc632e..828f9ad785 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java @@ -1,8 +1,8 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; +import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; public interface Updater { R update(R actual, R desired, P primary, Context

context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java index 04b49bf0bf..c45e50db9a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -3,9 +3,9 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; +import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 850b290c12..2b91e6d75f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -4,10 +4,10 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.DesiredEqualsMatcher; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; @@ -51,11 +51,13 @@ public ReconcileResult reconcile(P primary, Context

context) { return super.reconcile(primary, context); } - public void cleanup(P primary, Context

context) { - super.cleanup(primary, context); + public final void delete(P primary, Context

context) { + deleteResource(primary, context); cache.remove(ResourceID.fromResource(primary)); } + protected abstract void deleteResource(P primary, Context

context); + @Override protected R handleCreate(R desired, P primary, Context

context) { var res = this.creator.create(desired, primary, context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java similarity index 50% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java index b363e81b51..05195e39b9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java @@ -1,21 +1,21 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; /** - * Adaptor Class for standalone mode for resources that manages Create, Update and Delete + * Adaptor Class for standalone mode for resources that manages Create, Read, Update and Delete * * @param Managed resource * @param

Primary Resource */ -public abstract class CrudKubernetesDependentResource - extends KubernetesDependentResource - implements Creator, Updater, Deleter

{ +public abstract class CRUDKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater, Deleter

{ - public CrudKubernetesDependentResource(Class resourceType) { + public CRUDKubernetesDependentResource(Class resourceType) { super(resourceType); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java new file mode 100644 index 0000000000..7670367936 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; + +/** + * Adaptor Class for standalone mode for resources that manages Create, Read and Update + * + * @param Managed resource + * @param

Primary Resource + */ +public abstract class CRUKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater { + + + public CRUKubernetesDependentResource(Class resourceType) { + super(resourceType); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 0bbe19837e..f2c789e267 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -7,7 +7,7 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; public class GenericKubernetesResourceMatcher implements Matcher { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java index 0e0624d183..e35913af68 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -6,7 +6,6 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; public abstract class GenericResourceUpdatePreProcessor implements ResourceUpdatePreProcessor { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 9439d74865..233deb020d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -14,14 +14,13 @@ import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java similarity index 78% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java index 55ebcca230..3b88884e6a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 235a27481d..2a0f0997f0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -16,7 +16,6 @@ import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -60,9 +59,9 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) log.debug("Handling dispatch for resource {}", getName(resource)); final var markedForDeletion = resource.isMarkedForDeletion(); - if (markedForDeletion && shouldNotDispatchToDelete(resource)) { + if (markedForDeletion && shouldNotDispatchToCleanup(resource)) { log.debug( - "Skipping delete of resource {} because finalizer(s) {} don't allow processing yet", + "Skipping cleanup of resource {} because finalizer(s) {} don't allow processing yet", getName(resource), resource.getMetadata().getFinalizers()); return PostExecutionControl.defaultDispatch(); @@ -76,25 +75,17 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) } } - /** - * Determines whether the given resource should be dispatched to the controller's - * {@link Reconciler#cleanup(HasMetadata, Context)} method - * - * @param resource the resource to be potentially deleted - * @return {@code true} if the resource should be handed to the controller's - * {@link Reconciler#cleanup(HasMetadata, Context)} method, {@code false} otherwise - */ - private boolean shouldNotDispatchToDelete(R resource) { - // we don't dispatch to delete if the controller is configured to use a finalizer but that + private boolean shouldNotDispatchToCleanup(R resource) { + // we don't dispatch to cleanup if the controller is configured to use a finalizer but that // finalizer is not present (which means it's already been removed) - return !configuration().useFinalizer() || (configuration().useFinalizer() - && !resource.hasFinalizer(configuration().getFinalizer())); + return !controller.useFinalizer() || (controller.useFinalizer() + && !resource.hasFinalizer(configuration().getFinalizerName())); } private PostExecutionControl handleReconcile( ExecutionScope executionScope, R originalResource, Context context) throws Exception { - if (configuration().useFinalizer() - && !originalResource.hasFinalizer(configuration().getFinalizer())) { + if (controller.useFinalizer() + && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* * We always add the finalizer if missing and the controller is configured to use a finalizer. * We execute the controller processing only for processing the event sent as a results of the @@ -265,12 +256,12 @@ private PostExecutionControl handleCleanup(R resource, Context context) { getVersion(resource)); DeleteControl deleteControl = controller.cleanup(resource, context); - final var useFinalizer = configuration().useFinalizer(); + final var useFinalizer = controller.useFinalizer(); if (useFinalizer) { // note that we don't reschedule here even if instructed. Removing finalizer means that // cleanup is finished, nothing left to done if (deleteControl.isRemoveFinalizer() - && resource.hasFinalizer(configuration().getFinalizer())) { + && resource.hasFinalizer(configuration().getFinalizerName())) { R customResource = removeFinalizer(resource); return PostExecutionControl.customResourceUpdated(customResource); } @@ -289,7 +280,7 @@ private PostExecutionControl handleCleanup(R resource, Context context) { private void updateCustomResourceWithFinalizer(R resource) { log.debug( "Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource)); - resource.addFinalizer(configuration().getFinalizer()); + resource.addFinalizer(configuration().getFinalizerName()); replace(resource); } @@ -304,7 +295,7 @@ private R removeFinalizer(R resource) { "Removing finalizer on resource: {} with version: {}", getUID(resource), getVersion(resource)); - resource.removeFinalizer(configuration().getFinalizer()); + resource.removeFinalizer(configuration().getFinalizerName()); return customResourceFacade.replaceWithLock(resource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index b43666660c..fe9d802630 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -59,7 +59,7 @@ public void eventReceived(ResourceAction action, T resource, T oldResource) { log.debug("Event received for resource: {}", getName(resource)); MDCUtils.addResourceInfo(resource); controller.getEventSourceManager().broadcastOnResourceEvent(action, resource, oldResource); - if (filter.acceptChange(controller.getConfiguration(), oldResource, resource)) { + if (filter.acceptChange(controller, oldResource, resource)) { getEventHandler().handleEvent( new ResourceEvent(action, ResourceID.fromResource(resource))); } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java index 497c9016b7..17cb27fd71 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java @@ -1,29 +1,29 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; /** * A functional interface to determine whether resource events should be processed by the SDK. This * allows users to more finely tuned which events trigger a reconciliation than was previously * possible (where the logic was limited to generation-based checking). * - * @param the type of custom resources handled by this filter + * @param

the type of custom resources handled by this filter */ @FunctionalInterface -public interface ResourceEventFilter { +public interface ResourceEventFilter

{ /** * Determines whether the change between the old version of the resource and the new one needs to * be propagated to the controller or not. * - * @param configuration the target controller's configuration + * @param controller the target controller * @param oldResource the old version of the resource, null if no old resource available * @param newResource the new version of the resource * @return {@code true} if the change needs to be propagated to the controller, {@code false} * otherwise */ - boolean acceptChange(ControllerConfiguration configuration, T oldResource, T newResource); + boolean acceptChange(Controller

controller, P oldResource, P newResource); /** * Combines this filter with the provided one with an AND logic, i.e. the resulting filter will @@ -32,11 +32,11 @@ public interface ResourceEventFilter { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the AND logic between this and the provided filter */ - default ResourceEventFilter and(ResourceEventFilter other) { + default ResourceEventFilter

and(ResourceEventFilter

other) { return other == null ? this - : (ControllerConfiguration configuration, T oldResource, T newResource) -> { - boolean result = acceptChange(configuration, oldResource, newResource); - return result && other.acceptChange(configuration, oldResource, newResource); + : (Controller

controller, P oldResource, P newResource) -> { + boolean result = acceptChange(controller, oldResource, newResource); + return result && other.acceptChange(controller, oldResource, newResource); }; } @@ -48,11 +48,11 @@ default ResourceEventFilter and(ResourceEventFilter other) { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the OR logic between this and the provided filter */ - default ResourceEventFilter or(ResourceEventFilter other) { + default ResourceEventFilter

or(ResourceEventFilter

other) { return other == null ? this - : (ControllerConfiguration configuration, T oldResource, T newResource) -> { - boolean result = acceptChange(configuration, oldResource, newResource); - return result || other.acceptChange(configuration, oldResource, newResource); + : (Controller

controller, P oldResource, P newResource) -> { + boolean result = acceptChange(controller, oldResource, newResource); + return result || other.acceptChange(controller, oldResource, newResource); }; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java index c314af7f2e..5a22cafac8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java @@ -8,9 +8,9 @@ public final class ResourceEventFilters { private static final ResourceEventFilter USE_FINALIZER = - (configuration, oldResource, newResource) -> { - if (configuration.useFinalizer()) { - final var finalizer = configuration.getFinalizer(); + (controller, oldResource, newResource) -> { + if (controller.useFinalizer()) { + final var finalizer = controller.getConfiguration().getFinalizerName(); boolean oldFinalizer = oldResource == null || oldResource.hasFinalizer(finalizer); boolean newFinalizer = newResource.hasFinalizer(finalizer); @@ -21,8 +21,8 @@ public final class ResourceEventFilters { }; private static final ResourceEventFilter GENERATION_AWARE = - (configuration, oldResource, newResource) -> { - final var generationAware = configuration.isGenerationAware(); + (controller, oldResource, newResource) -> { + final var generationAware = controller.getConfiguration().isGenerationAware(); return oldResource == null || !generationAware || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); }; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index e137a2ff11..5081767980 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -7,7 +7,6 @@ import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; -import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -40,11 +39,6 @@ void defaultFinalizerShouldWork() { assertTrue(isFinalizerValid(getDefaultFinalizerName(TestCustomResource.class))); } - @Test - void noFinalizerMarkerShouldWork() { - assertTrue(isFinalizerValid(Constants.NO_FINALIZER)); - } - @Test void equalsSpecObject() { var d1 = createTestDeployment(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index ba31d6634e..2f5b9bf4ba 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -7,23 +7,24 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @SuppressWarnings("unchecked") class ControllerTest { + + final ControllerConfiguration configuration = mock(ControllerConfiguration.class); + final Reconciler reconciler = mock(Reconciler.class); + @Test void crdShouldNotBeCheckedForNativeResources() { final var client = MockKubernetesClient.client(Secret.class); - final var reconciler = mock(Reconciler.class); - final var configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(Secret.class); final var controller = new Controller(reconciler, configuration, client); @@ -34,8 +35,6 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { final var client = MockKubernetesClient.client(TestCustomResource.class); - final var reconciler = mock(Reconciler.class); - final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); try { @@ -52,8 +51,6 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { void crdShouldBeCheckedForCustomResourcesByDefault() { ConfigurationServiceProvider.reset(); final var client = MockKubernetesClient.client(TestCustomResource.class); - final var reconciler = mock(Reconciler.class); - final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); final var controller = new Controller(reconciler, configuration, client); @@ -62,4 +59,15 @@ void crdShouldBeCheckedForCustomResourcesByDefault() { assertThrows(MissingCRDException.class, controller::start); verify(client, times(1)).apiextensions(); } + + @Test + void usesFinalizerIfThereIfReconcilerImplementsCleaner() { + Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); + when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); + + final var controller = new Controller(reconciler, + configuration, MockKubernetesClient.client(TestCustomResource.class)); + + assertThat(controller.useFinalizer()).isTrue(); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java index b654ac5d1e..0b9517eb81 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java @@ -9,9 +9,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; @@ -84,7 +84,7 @@ void updatePutsNewResourceToCache() { @Test void deleteRemovesResourceFromCache() { - simpleDependentResource.cleanup(TestUtils.testCustomResource1(), null); + simpleDependentResource.delete(TestUtils.testCustomResource1(), null); verify(updatableCacheMock, times(1)).remove(any()); } @@ -111,6 +111,10 @@ public Optional fetchResource(HasMetadata primaryResourc return Optional.ofNullable(supplier.get()); } + @Override + protected void deleteResource(TestCustomResource primary, + Context context) {} + @Override public SampleExternalResource create( SampleExternalResource desired, TestCustomResource primary, @@ -127,9 +131,6 @@ public SampleExternalResource update( return SampleExternalResource.testResource1(); } - @Override - public void delete(TestCustomResource primary, Context context) {} - @Override protected SampleExternalResource desired(TestCustomResource primary, Context context) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index 802be9c3d9..e67d579adc 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -9,7 +9,6 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 2e6ee4aa0d..ccf218d3d0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -93,9 +93,8 @@ private ReconciliationDispatcher init(R customResourc CustomResourceFacade customResourceFacade, boolean useFinalizer) { configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; - final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; - when(configuration.getFinalizer()).thenReturn(finalizer); - when(configuration.useFinalizer()).thenCallRealMethod(); + + when(configuration.getFinalizerName()).thenReturn(DEFAULT_FINALIZER); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configuration.getResourceClass()).thenReturn((Class) customResource.getClass()); when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); @@ -103,7 +102,12 @@ private ReconciliationDispatcher init(R customResourc .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); Controller controller = new Controller<>(reconciler, configuration, - MockKubernetesClient.client(customResource.getClass())); + MockKubernetesClient.client(customResource.getClass())) { + @Override + public boolean useFinalizer() { + return useFinalizer; + } + }; controller.start(); return new ReconciliationDispatcher<>(controller, customResourceFacade); @@ -539,7 +543,8 @@ public ExecutionScope executionScopeWithCREvent(T res } private class TestReconciler - implements Reconciler, ErrorStatusHandler { + implements Reconciler, Cleaner, + ErrorStatusHandler { private BiFunction> reconcile; private BiFunction cleanup; private ErrorStatusHandler errorHandler; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index fd08a614af..2d943deb80 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -134,7 +134,7 @@ public TestCustomResource newMyResource(String app, String namespace) { public static class MyConfiguration extends DefaultControllerConfiguration { public MyConfiguration() { - super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.NO_FINALIZER, + super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.EMPTY_STRING, false, null, null, null, null, TestCustomResource.class, null, null); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index d06cf97fe8..1c467e0490 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -34,7 +34,7 @@ public void setup() { } @Test - public void skipsEventHandlingIfGenerationNotIncreased() { + void skipsEventHandlingIfGenerationNotIncreased() { TestCustomResource customResource = TestUtils.testCustomResource(); customResource.getMetadata().setFinalizers(List.of(FINALIZER)); customResource.getMetadata().setGeneration(2L); @@ -50,7 +50,7 @@ public void skipsEventHandlingIfGenerationNotIncreased() { } @Test - public void dontSkipEventHandlingIfMarkedForDeletion() { + void dontSkipEventHandlingIfMarkedForDeletion() { TestCustomResource customResource1 = TestUtils.testCustomResource(); source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); @@ -63,7 +63,7 @@ public void dontSkipEventHandlingIfMarkedForDeletion() { } @Test - public void normalExecutionIfGenerationChanges() { + void normalExecutionIfGenerationChanges() { TestCustomResource customResource1 = TestUtils.testCustomResource(); source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); @@ -75,7 +75,7 @@ public void normalExecutionIfGenerationChanges() { } @Test - public void handlesAllEventIfNotGenerationAware() { + void handlesAllEventIfNotGenerationAware() { source = new ControllerResourceEventSource<>(new TestController(false)); setup(); @@ -124,6 +124,11 @@ public TestController(boolean generationAware) { public EventSourceManager getEventSourceManager() { return eventSourceManager; } + + @Override + public boolean useFinalizer() { + return true; + } } private static class TestConfiguration extends diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java index 9595f3c13f..3413b620b3 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java @@ -15,7 +15,8 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @ControllerConfiguration(generationAwareEventProcessing = false) -public class TestCustomReconciler implements Reconciler { +public class TestCustomReconciler + implements Reconciler, Cleaner { private static final Logger log = LoggerFactory.getLogger(TestCustomReconciler.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java new file mode 100644 index 0000000000..5356204491 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java @@ -0,0 +1,49 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.cleanerforreconciler.CleanerForReconcilerCustomResource; +import io.javaoperatorsdk.operator.sample.cleanerforreconciler.CleanerForReconcilerTestReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class CleanerForReconcilerIT { + + public static final String TEST_RESOURCE_NAME = "cleaner-for-reconciler-test1"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder().withReconciler(new CleanerForReconcilerTestReconciler()).build(); + + + @Test + void addsFinalizerAndCallsCleanupIfCleanerImplemented() { + var testResource = createTestResource(); + operator.create(CleanerForReconcilerCustomResource.class, testResource); + + await().until(() -> !operator.get(CleanerForReconcilerCustomResource.class, TEST_RESOURCE_NAME) + .getMetadata().getFinalizers().isEmpty()); + + operator.delete(CleanerForReconcilerCustomResource.class, testResource); + + await().until( + () -> operator.get(CleanerForReconcilerCustomResource.class, TEST_RESOURCE_NAME) == null); + + CleanerForReconcilerTestReconciler reconciler = + (CleanerForReconcilerTestReconciler) operator.getFirstReconciler(); + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1); + assertThat(reconciler.getNumberOfCleanupExecutions()).isEqualTo(1); + } + + private CleanerForReconcilerCustomResource createTestResource() { + CleanerForReconcilerCustomResource cr = new CleanerForReconcilerCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(TEST_RESOURCE_NAME); + return cr; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index 0a3ed114a1..3688b55d3c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -42,6 +43,22 @@ void eventIsSkippedChangedOnMetadataOnlyUpdate() { assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(1); } + @Test + void cleanupExecuted() { + operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); + + TestCustomResource resource = TestUtils.testCustomResource(); + resource = operator.create(TestCustomResource.class, resource); + + awaitResourcesCreatedOrUpdated(); + awaitStatusUpdated(); + operator.delete(TestCustomResource.class, resource); + + await().atMost(Duration.ofSeconds(1)) + .until(() -> ((TestReconciler) operator.getFirstReconciler()) + .getNumberOfCleanupExecutions() == 1); + } + void awaitResourcesCreatedOrUpdated() { await("config map created") .atMost(5, TimeUnit.SECONDS) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java new file mode 100644 index 0000000000..94c7c7798d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.cleanermanageddependent.CleanerForManagedDependentCustomResource; +import io.javaoperatorsdk.operator.sample.cleanermanageddependent.CleanerForManagedDependentTestReconciler; +import io.javaoperatorsdk.operator.sample.cleanermanageddependent.ConfigMapDependentResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class DeleterForManagedDependentResourcesOnlyIT { + + public static final String TEST_RESOURCE_NAME = "cleaner-for-reconciler-test1"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder().withReconciler(new CleanerForManagedDependentTestReconciler()) + .build(); + + + @Test + void addsFinalizerAndCallsCleanupIfCleanerImplemented() { + var testResource = createTestResource(); + operator.create(CleanerForManagedDependentCustomResource.class, testResource); + + await().until( + () -> !operator.get(CleanerForManagedDependentCustomResource.class, TEST_RESOURCE_NAME) + .getMetadata().getFinalizers().isEmpty()); + + operator.delete(CleanerForManagedDependentCustomResource.class, testResource); + + await().until( + () -> operator.get(CleanerForManagedDependentCustomResource.class, + TEST_RESOURCE_NAME) == null); + + CleanerForManagedDependentTestReconciler reconciler = + (CleanerForManagedDependentTestReconciler) operator.getFirstReconciler(); + + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1); + assertThat(ConfigMapDependentResource.getNumberOfCleanupExecutions()).isEqualTo(1); + } + + private CleanerForManagedDependentCustomResource createTestResource() { + CleanerForManagedDependentCustomResource cr = new CleanerForManagedDependentCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(TEST_RESOURCE_NAME); + return cr; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index eea996cb3b..5aaeb0bab4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -41,7 +41,6 @@ public EventSourceTestCustomResource createTestCustomResource(String id) { new ObjectMetaBuilder() .withName("eventsource-" + id) .withNamespace(operator.getNamespace()) - .withFinalizers(EventSourceTestCustomReconciler.FINALIZER_NAME) .build()); resource.setSpec(new EventSourceTestCustomResourceSpec()); resource.getSpec().setValue(id); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index 02b7b7af9e..7a951aadc7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -65,7 +65,6 @@ public static RetryTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("retrysource-" + id) - .withFinalizers(RetryTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("retrysample"); resource.setSpec(new RetryTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index bd335c9ae7..61bc9f927c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -99,7 +99,6 @@ void awaitStatusUpdated(String name) { () -> { SubResourceTestCustomResource cr = operator.get(SubResourceTestCustomResource.class, name); - assertThat(cr.getMetadata().getFinalizers()).hasSize(1); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getState()) @@ -112,7 +111,6 @@ public SubResourceTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("subresource-" + id) - .withFinalizers(SubResourceTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("SubresourceSample"); resource.setSpec(new SubResourceTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index d86ad51d60..df885110d4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -54,11 +54,8 @@ void awaitStatusUpdated(String name) { () -> { DoubleUpdateTestCustomResource cr = operator.get(DoubleUpdateTestCustomResource.class, name); - assertThat(cr) .isNotNull(); - assertThat(cr.getMetadata().getFinalizers()) - .hasSize(1); assertThat(cr.getStatus()) .isNotNull(); assertThat(cr.getStatus().getState()) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index d123aa4899..c5bf845853 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -79,7 +79,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { configuration.getResourceTypeName()); assertEquals( ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class), - configuration.getFinalizer()); + configuration.getFinalizerName()); assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); } @@ -88,7 +88,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { void returnCustomerFinalizerNameIfSet() { final var reconciler = new TestCustomFinalizerReconciler(); final var configuration = configurationService.getConfigurationFor(reconciler); - assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizer()); + assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizerName()); } @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java new file mode 100644 index 0000000000..7a4b255d28 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.cleanerforreconciler; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("CleanerForReconcilerCustomResource") +@ShortNames("cfr") +public class CleanerForReconcilerCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java new file mode 100644 index 0000000000..fad1bb7798 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java @@ -0,0 +1,39 @@ +package io.javaoperatorsdk.operator.sample.cleanerforreconciler; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration +public class CleanerForReconcilerTestReconciler + implements Reconciler, + Cleaner, + TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + CleanerForReconcilerCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + public int getNumberOfCleanupExecutions() { + return numberOfCleanupExecutions.get(); + } + + @Override + public DeleteControl cleanup(CleanerForReconcilerCustomResource resource, + Context context) { + numberOfCleanupExecutions.addAndGet(1); + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java new file mode 100644 index 0000000000..9e18e657aa --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.cleanermanageddependent; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("CleanerForReconcilerCustomResource") +@ShortNames("cfr") +public class CleanerForManagedDependentCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java new file mode 100644 index 0000000000..6be29c5092 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.sample.cleanermanageddependent; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration(dependents = {@Dependent(type = ConfigMapDependentResource.class)}) +public class CleanerForManagedDependentTestReconciler + implements Reconciler, + TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + CleanerForManagedDependentCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java new file mode 100644 index 0000000000..332e2e7534 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java @@ -0,0 +1,44 @@ +package io.javaoperatorsdk.operator.sample.cleanermanageddependent; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +public class ConfigMapDependentResource extends + CRUDKubernetesDependentResource { + + private static final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(CleanerForManagedDependentCustomResource primary, + Context context) { + + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(primary.getMetadata().getName()); + configMap.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + HashMap data = new HashMap<>(); + data.put("key1", "val1"); + configMap.setData(data); + return configMap; + } + + @Override + public void delete(CleanerForManagedDependentCustomResource primary, + Context context) { + super.delete(primary, context); + numberOfCleanupExecutions.incrementAndGet(); + } + + public static int getNumberOfCleanupExecutions() { + return numberOfCleanupExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index 19e00e8ba2..4c1ffc4312 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -15,9 +15,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class CreateUpdateEventFilterTestReconciler implements Reconciler, EventSourceInitializer, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java index 58d2ed1ede..bba45a44ac 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java @@ -1,12 +1,12 @@ package io.javaoperatorsdk.operator.sample.customfilter; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class CustomFlagFilter implements ResourceEventFilter { @Override - public boolean acceptChange(ControllerConfiguration configuration, + public boolean acceptChange(Controller configuration, CustomFilteringTestResource oldResource, CustomFilteringTestResource newResource) { return newResource.getSpec().isFilter1(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java index f38a8c9553..ae6b5d684f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java @@ -1,12 +1,12 @@ package io.javaoperatorsdk.operator.sample.customfilter; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class CustomFlagFilter2 implements ResourceEventFilter { @Override - public boolean acceptChange(ControllerConfiguration configuration, + public boolean acceptChange(Controller configuration, CustomFilteringTestResource oldResource, CustomFilteringTestResource newResource) { return newResource.getSpec().isFilter2(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 17ea106104..412a784b7c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -8,9 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class ErrorStatusHandlerTestReconciler implements Reconciler, TestExecutionInfoProvider, ErrorStatusHandler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 97a99eed35..8e7dc1a6f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -14,13 +14,11 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - /** * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class InformerEventSourceTestCustomReconciler implements Reconciler, EventSourceInitializer { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java index f9a34371ce..f868148145 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java @@ -6,11 +6,8 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER, - reconciliationMaxInterval = @ReconciliationMaxInterval(interval = 50, - timeUnit = TimeUnit.MILLISECONDS)) +@ControllerConfiguration(reconciliationMaxInterval = @ReconciliationMaxInterval(interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) public class MaxIntervalTestReconciler implements Reconciler, TestExecutionInfoProvider { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java index de733ec6db..f4d9236077 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java @@ -8,9 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER, labelSelector = "!version") +@ControllerConfiguration(labelSelector = "!version") public class MultiVersionCRDTestReconciler1 implements Reconciler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java index 138647029e..b9521bd8d1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java @@ -8,11 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration( - finalizerName = NO_FINALIZER, - labelSelector = "version in (v2)") +@ControllerConfiguration(labelSelector = "version in (v2)") public class MultiVersionCRDTestReconciler2 implements Reconciler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java index 7a7964623f..02b9770acd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -3,11 +3,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class ObservedGenerationTestReconciler implements Reconciler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index 8ccd7c2160..a5273aac4c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -16,8 +15,6 @@ public class RetryTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { - public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(RetryTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(RetryTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @@ -34,9 +31,6 @@ public UpdateControl reconcile(RetryTestCustomResource Context context) { numberOfExecutions.addAndGet(1); - if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { - throw new IllegalStateException("Finalizer is not present."); - } log.info("Value: " + resource.getSpec().getValue()); if (numberOfExecutions.get() < numberOfExecutionFails.get() + 1) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 146c96c7db..78db3b16f7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -19,7 +19,8 @@ @ControllerConfiguration(generationAwareEventProcessing = false) public class TestReconciler - implements Reconciler, TestExecutionInfoProvider, + implements Reconciler, Cleaner, + TestExecutionInfoProvider, KubernetesClientAware { private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); @@ -28,21 +29,14 @@ public class TestReconciler ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); private KubernetesClient kubernetesClient; private boolean updateStatus; - public TestReconciler() { - this(true); - } - public TestReconciler(boolean updateStatus) { this.updateStatus = updateStatus; } - public boolean isUpdateStatus() { - return updateStatus; - } - public void setUpdateStatus(boolean updateStatus) { this.updateStatus = updateStatus; } @@ -60,6 +54,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { @Override public DeleteControl cleanup( TestCustomResource resource, Context context) { + numberOfCleanupExecutions.incrementAndGet(); Boolean delete = kubernetesClient .configMaps() @@ -139,4 +134,8 @@ private Map configMapData(TestCustomResource resource) { public int getNumberOfExecutions() { return numberOfExecutions.get(); } + + public int getNumberOfCleanupExecutions() { + return numberOfCleanupExecutions.get(); + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index e740032d6e..074c454560 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -7,23 +7,14 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class StandaloneDependentTestReconciler implements Reconciler, EventSourceInitializer, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index e520e94411..1e76681f25 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -20,8 +19,6 @@ public class SubResourceTestCustomReconciler public static final int RECONCILER_MIN_EXEC_TIME = 300; - public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(SubResourceTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @@ -31,9 +28,6 @@ public class SubResourceTestCustomReconciler public UpdateControl reconcile( SubResourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); - if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { - throw new IllegalStateException("Finalizer is not present."); - } log.info("Value: " + resource.getSpec().getValue()); ensureStatusExists(resource); diff --git a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java index 5ab0596319..0adc9aee09 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java +++ b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java @@ -1,15 +1,13 @@ package io; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; + import java.io.Serializable; @ControllerConfiguration -public class ReconcilerImplemented2Interfaces implements Serializable, Reconciler { +public class ReconcilerImplemented2Interfaces implements Serializable, + Reconciler, Cleaner { public static class MyCustomResource extends CustomResource { } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index b52d7931da..f306292ff8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -9,12 +9,10 @@ import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; -// todo handle this, should work with finalizer -@ControllerConfiguration(finalizerName = NO_FINALIZER, +@ControllerConfiguration( dependents = { @Dependent(type = SecretDependentResource.class), @Dependent(type = SchemaDependentResource.class) diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index 273cd086a1..b3155f9199 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -11,10 +11,9 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.MySQLDbConfig; import io.javaoperatorsdk.operator.sample.MySQLSchema; @@ -29,8 +28,10 @@ public class SchemaDependentResource extends PerResourcePollingDependentResource implements EventSourceProvider, DependentResourceConfigurator, - Creator, - Deleter { + Creator +// todo fix +// , Deleter +{ private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); @@ -74,7 +75,7 @@ private Connection getConnection() throws SQLException { return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword()); } - @Override + // @Override public void delete(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 60c4c4d2b0..7ee78deed7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -7,8 +7,8 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index b8e8b0566a..59f3bfcd98 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -39,6 +39,7 @@ class MySQLSchemaOperatorE2E { static final String MY_SQL_NS = "mysql"; private final static List infrastructure = new ArrayList<>(); + public static final String TEST_RESOURCE_NAME = "mydb1"; static { infrastructure.add( @@ -83,25 +84,12 @@ public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @Test void test() throws IOException { // Opening a port-forward if running locally - LocalPortForward portForward = null; - if (isLocal()) { - String podName = - client - .pods() - .inNamespace(MY_SQL_NS) - .withLabel("app", "mysql") - .list() - .getItems() - .get(0) - .getMetadata() - .getName(); - - portForward = client.pods().inNamespace(MY_SQL_NS).withName(podName).portForward(3306, 3306); - } + LocalPortForward portForward = createLocalPortForward(); MySQLSchema testSchema = new MySQLSchema(); testSchema.setMetadata( - new ObjectMetaBuilder().withName("mydb1").withNamespace(operator.getNamespace()).build()); + new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).withNamespace(operator.getNamespace()) + .build()); testSchema.setSpec(new SchemaSpec()); testSchema.getSpec().setEncoding("utf8"); @@ -130,4 +118,22 @@ void test() throws IOException { portForward.close(); } } + + private LocalPortForward createLocalPortForward() { + if (isLocal()) { + String podName = + client + .pods() + .inNamespace(MY_SQL_NS) + .withLabel("app", "mysql") + .list() + .getItems() + .get(0) + .getMetadata() + .getName(); + + return client.pods().inNamespace(MY_SQL_NS).withName(podName).portForward(3306, 3306); + } + return null; + } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 75632f3e69..1e0c7c08d1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -5,8 +5,8 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index c52553851d..8efaadc0a8 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -5,8 +5,8 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; public class ServiceDependentResource extends KubernetesDependentResource diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index dfad4533e3..30eb3c3848 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -13,14 +13,11 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - /** * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also * creates a Service over which the Pods can be accessed. */ @ControllerConfiguration( - finalizerName = NO_FINALIZER, dependents = { @Dependent(type = DeploymentDependentResource.class), @Dependent(type = ServiceDependentResource.class) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 58d17235ae..68e3fbe135 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -18,23 +18,16 @@ import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class WebappReconciler implements Reconciler, EventSourceInitializer { +@ControllerConfiguration +public class WebappReconciler + implements Reconciler, Cleaner, EventSourceInitializer { private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index bc7bb83894..570f2ffb53 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.sample; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -16,11 +18,8 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - /** Shows how to implement reconciler using the low level api directly. */ @ControllerConfiguration( - finalizerName = NO_FINALIZER, labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) public class WebPageReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index 90f747d158..7698c3a16d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -22,7 +22,8 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CrudKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -30,12 +31,11 @@ import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; /** * Shows how to implement reconciler using standalone dependent resources. */ -@ControllerConfiguration(finalizerName = NO_FINALIZER, +@ControllerConfiguration( labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) public class WebPageReconcilerDependentResources implements Reconciler, ErrorStatusHandler, EventSourceInitializer { @@ -100,7 +100,7 @@ private void createDependentResources(KubernetesClient client) { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.deploymentDR = - new CrudKubernetesDependentResource<>(Deployment.class) { + new CRUDKubernetesDependentResource<>(Deployment.class) { @Override protected Deployment desired(WebPage webPage, Context context) { @@ -132,7 +132,7 @@ protected Deployment desired(WebPage webPage, Context context) { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.serviceDR = - new CrudKubernetesDependentResource<>(Service.class) { + new CRUDKubernetesDependentResource<>(Service.class) { @Override protected Service desired(WebPage webPage, Context context) { @@ -163,7 +163,7 @@ public static String serviceName(WebPage webPage) { } private class ConfigMapDependentResource - extends CrudKubernetesDependentResource + extends CRUKubernetesDependentResource implements PrimaryToSecondaryMapper { public ConfigMapDependentResource() { diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index a899c50756..9a5e188cba 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -29,12 +29,6 @@ public CustomServiceReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } - @Override - public DeleteControl cleanup(CustomService resource, Context context) { - log.info("Cleaning up for: {}", resource.getMetadata().getName()); - return Reconciler.super.cleanup(resource, context); - } - @Override public UpdateControl reconcile( CustomService resource, Context context) { @@ -45,17 +39,20 @@ public UpdateControl reconcile( ServiceSpec serviceSpec = new ServiceSpec(); serviceSpec.setPorts(Collections.singletonList(servicePort)); + var service = new ServiceBuilder() + .withNewMetadata() + .withName(resource.getSpec().getName()) + .addToLabels("testLabel", resource.getSpec().getLabel()) + .endMetadata() + .withSpec(serviceSpec) + .build(); + service.addOwnerReference(resource); + kubernetesClient .services() .inNamespace(resource.getMetadata().getNamespace()) - .createOrReplace( - new ServiceBuilder() - .withNewMetadata() - .withName(resource.getSpec().getName()) - .addToLabels("testLabel", resource.getSpec().getLabel()) - .endMetadata() - .withSpec(serviceSpec) - .build()); + .createOrReplace(service); + return UpdateControl.updateResource(resource); } } From efd33e53babfede31ab0d9079f1dcfb4eaa7d4e5 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 21 Mar 2022 10:00:14 +0100 Subject: [PATCH 0085/1319] chore: move version to 3.0.0-SNAPSHOT to reflect API breakage (#1051) --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 11f70ef4ca..6bc6606f95 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 79ccc716df..1586f37028 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 9f8f4e5c01..7ca3fde2e5 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index a624af4e23..5c6c610eac 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index e282d0c852..49321f8ff3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index b2e6aefed9..6fc1d4557f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 666e471240..d44de9512b 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 82385f8c9f..c092ee1b01 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 93e32953c3..3c07557175 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT webpage diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 6dcdded08b..a09715d428 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index ffbe7c659b..11658f0cf5 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 21b8162183..c065b05452 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index 51860f7af8..4fde68a718 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From c1467e8fa4c3a3824e6bd5fc640df1fbe0eefb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Mar 2022 14:25:27 +0100 Subject: [PATCH 0086/1319] docs: use anchor headings (#1052) --- docs/_includes/anchor_headings.html | 172 ++++++++++++++++++++++++++++ docs/_layouts/docs.html | 2 +- 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 docs/_includes/anchor_headings.html diff --git a/docs/_includes/anchor_headings.html b/docs/_includes/anchor_headings.html new file mode 100644 index 0000000000..f8e22d6a0f --- /dev/null +++ b/docs/_includes/anchor_headings.html @@ -0,0 +1,172 @@ +{% capture headingsWorkspace %} + {% comment %} + Copyright (c) 2018 Vladimir "allejo" Jimenez + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + {% endcomment %} + {% comment %} + Version 1.0.11 + https://github.com/allejo/jekyll-anchor-headings + + "Be the pull request you wish to see in the world." ~Ben Balter + + Usage: + {% include anchor_headings.html html=content anchorBody="#" %} + + Parameters: + * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll + + Optional Parameters: + * beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content + * headerAttrs (string) : '' - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`; + the `%heading%` and `%html_id%` placeholders are available + * anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `` tag; you may NOT use `href`, `class` or `title`; + the `%heading%` and `%html_id%` placeholders are available + * anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available + * anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space + * anchorTitle (string) : '' - The `title` attribute that will be used for anchors + * h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored + * h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored + * bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content + * bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content + * generateId (true) : false - Set to true if a header without id should generate an id to use. + + Output: + The original HTML with the addition of anchors inside of all of the h1-h6 headings. + {% endcomment %} + + {% assign minHeader = include.h_min | default: 1 %} + {% assign maxHeader = include.h_max | default: 6 %} + {% assign beforeHeading = include.beforeHeading %} + {% assign headerAttrs = include.headerAttrs %} + {% assign nodes = include.html | split: ' + {% if headerLevel == 0 %} + + {% assign firstChunk = node | split: '>' | first %} + + + {% unless firstChunk contains '<' %} + {% capture node %}{% endcapture %} + {% assign _workspace = node | split: _closingTag %} + {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %} + {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} + {% assign escaped_header = header | strip_html | strip %} + + {% assign _classWorkspace = _workspace[0] | split: 'class="' %} + {% assign _classWorkspace = _classWorkspace[1] | split: '"' %} + {% assign _html_class = _classWorkspace[0] %} + + {% if _html_class contains "no_anchor" %} + {% assign skip_anchor = true %} + {% else %} + {% assign skip_anchor = false %} + {% endif %} + + {% assign _idWorkspace = _workspace[0] | split: 'id="' %} + {% if _idWorkspace[1] %} + {% assign _idWorkspace = _idWorkspace[1] | split: '"' %} + {% assign html_id = _idWorkspace[0] %} + {% elsif include.generateId %} + + {% assign html_id = escaped_header | slugify %} + {% if html_id == "" %} + {% assign html_id = false %} + {% endif %} + {% capture headerAttrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %} + {% endif %} + + + {% capture anchor %}{% endcapture %} + + {% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %} + {% if headerAttrs %} + {% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ headerAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %} + {% endif %} + + {% capture anchor %}href="#{{ html_id }}"{% endcapture %} + + {% if include.anchorClass %} + {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %} + {% endif %} + + {% if include.anchorTitle %} + {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %} + {% endif %} + + {% if include.anchorAttrs %} + {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %} + {% endif %} + + {% capture anchor %}{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}{% endcapture %} + + + {% if beforeHeading %} + {% capture anchor %}{{ anchor }} {% endcapture %} + {% else %} + {% capture anchor %} {{ anchor }}{% endcapture %} + {% endif %} + {% endif %} + + {% capture new_heading %} + + {% endcapture %} + + + {% assign chunkCount = _workspace | size %} + {% if chunkCount > 1 %} + {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %} + {% endif %} + + {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %} + {% endfor %} +{% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }} diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index 1283d701d6..b5cef5c127 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -18,7 +18,7 @@

- {{ content }} + {% include anchor_headings.html html=content anchorBody="#" %}
From a6c0784a0f146495360de89e16cb2983383e1c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Mar 2022 14:27:19 +0100 Subject: [PATCH 0087/1319] docs: use anchor headings (#1055) --- docs/_includes/anchor_headings.html | 172 ++++++++++++++++++++++++++++ docs/_layouts/docs.html | 2 +- 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 docs/_includes/anchor_headings.html diff --git a/docs/_includes/anchor_headings.html b/docs/_includes/anchor_headings.html new file mode 100644 index 0000000000..f8e22d6a0f --- /dev/null +++ b/docs/_includes/anchor_headings.html @@ -0,0 +1,172 @@ +{% capture headingsWorkspace %} + {% comment %} + Copyright (c) 2018 Vladimir "allejo" Jimenez + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + {% endcomment %} + {% comment %} + Version 1.0.11 + https://github.com/allejo/jekyll-anchor-headings + + "Be the pull request you wish to see in the world." ~Ben Balter + + Usage: + {% include anchor_headings.html html=content anchorBody="#" %} + + Parameters: + * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll + + Optional Parameters: + * beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content + * headerAttrs (string) : '' - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`; + the `%heading%` and `%html_id%` placeholders are available + * anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `` tag; you may NOT use `href`, `class` or `title`; + the `%heading%` and `%html_id%` placeholders are available + * anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available + * anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space + * anchorTitle (string) : '' - The `title` attribute that will be used for anchors + * h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored + * h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored + * bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content + * bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content + * generateId (true) : false - Set to true if a header without id should generate an id to use. + + Output: + The original HTML with the addition of anchors inside of all of the h1-h6 headings. + {% endcomment %} + + {% assign minHeader = include.h_min | default: 1 %} + {% assign maxHeader = include.h_max | default: 6 %} + {% assign beforeHeading = include.beforeHeading %} + {% assign headerAttrs = include.headerAttrs %} + {% assign nodes = include.html | split: ' + {% if headerLevel == 0 %} + + {% assign firstChunk = node | split: '>' | first %} + + + {% unless firstChunk contains '<' %} + {% capture node %}{% endcapture %} + {% assign _workspace = node | split: _closingTag %} + {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %} + {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} + {% assign escaped_header = header | strip_html | strip %} + + {% assign _classWorkspace = _workspace[0] | split: 'class="' %} + {% assign _classWorkspace = _classWorkspace[1] | split: '"' %} + {% assign _html_class = _classWorkspace[0] %} + + {% if _html_class contains "no_anchor" %} + {% assign skip_anchor = true %} + {% else %} + {% assign skip_anchor = false %} + {% endif %} + + {% assign _idWorkspace = _workspace[0] | split: 'id="' %} + {% if _idWorkspace[1] %} + {% assign _idWorkspace = _idWorkspace[1] | split: '"' %} + {% assign html_id = _idWorkspace[0] %} + {% elsif include.generateId %} + + {% assign html_id = escaped_header | slugify %} + {% if html_id == "" %} + {% assign html_id = false %} + {% endif %} + {% capture headerAttrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %} + {% endif %} + + + {% capture anchor %}{% endcapture %} + + {% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %} + {% if headerAttrs %} + {% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ headerAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %} + {% endif %} + + {% capture anchor %}href="#{{ html_id }}"{% endcapture %} + + {% if include.anchorClass %} + {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %} + {% endif %} + + {% if include.anchorTitle %} + {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %} + {% endif %} + + {% if include.anchorAttrs %} + {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %} + {% endif %} + + {% capture anchor %}{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}{% endcapture %} + + + {% if beforeHeading %} + {% capture anchor %}{{ anchor }} {% endcapture %} + {% else %} + {% capture anchor %} {{ anchor }}{% endcapture %} + {% endif %} + {% endif %} + + {% capture new_heading %} + + {% endcapture %} + + + {% assign chunkCount = _workspace | size %} + {% if chunkCount > 1 %} + {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %} + {% endif %} + + {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %} + {% endfor %} +{% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }} diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index 1283d701d6..b5cef5c127 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -18,7 +18,7 @@
- {{ content }} + {% include anchor_headings.html html=content anchorBody="#" %}
From c9c7016a62191b4a637cbf8e08cbfc677709e19c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 07:53:34 +0100 Subject: [PATCH 0088/1319] chore(deps): bump actions/cache from 2.1.7 to 3 (#1062) Bumps [actions/cache](https://github.com/actions/cache) from 2.1.7 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.7...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1611625618..8f830a7092 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -25,7 +25,7 @@ jobs: java-version: 17 cache: 'maven' - name: Cache SonarCloud packages - uses: actions/cache@v2.1.7 + uses: actions/cache@v3 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar From baf94e7b58c1d14982f57d681256d9dfd50000b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20CROCQUESEL?= <88554524+scrocquesel@users.noreply.github.com> Date: Tue, 22 Mar 2022 10:44:16 +0100 Subject: [PATCH 0089/1319] fix: measure total execution time in metrics (#1059) * fix: measure total time execution (reconcile/cleanup) * refactor: optimize controller metrics call --- .../operator/processing/Controller.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 7ee63d760f..f0aea0e1e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -2,6 +2,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -54,6 +55,8 @@ public class Controller

implements Reconciler

, Cleaner private final boolean contextInitializer; private final boolean hasDeleterDependents; private final boolean isCleaner; + private final Metrics metrics; + public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -61,6 +64,8 @@ public Controller(Reconciler

reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + this.metrics = Optional.ofNullable(ConfigurationServiceProvider.instance().getMetrics()) + .orElse(Metrics.NOOP); contextInitializer = reconciler instanceof ContextInitializer; eventSourceManager = new EventSourceManager<>(this); @@ -105,9 +110,8 @@ private void initContextIfNeeded(P resource, Context

context) { @Override public DeleteControl cleanup(P resource, Context

context) { - initContextIfNeeded(resource, context); try { - return metrics() + return metrics .timeControllerExecution( new ControllerExecution<>() { @Override @@ -127,6 +131,7 @@ public String successTypeName(DeleteControl deleteControl) { @Override public DeleteControl execute() { + initContextIfNeeded(resource, context); if (hasDeleterDependents) { dependents.stream() .filter(d -> d instanceof Deleter) @@ -147,8 +152,7 @@ public DeleteControl execute() { @Override public UpdateControl

reconcile(P resource, Context

context) throws Exception { - initContextIfNeeded(resource, context); - return metrics().timeControllerExecution( + return metrics.timeControllerExecution( new ControllerExecution<>() { @Override public String name() { @@ -174,18 +178,13 @@ public String successTypeName(UpdateControl

result) { @Override public UpdateControl

execute() throws Exception { + initContextIfNeeded(resource, context); dependents.forEach(dependent -> dependent.reconcile(resource, context)); return reconciler.reconcile(resource, context); } }); } - - private Metrics metrics() { - final var metrics = ConfigurationServiceProvider.instance().getMetrics(); - return metrics != null ? metrics : Metrics.NOOP; - } - @Override public List prepareEventSources(EventSourceContext

context) { List sources = new LinkedList<>(); From 7245f55a06955c45e0c379013a71a1dbdfdad312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 11:23:52 +0100 Subject: [PATCH 0090/1319] fix: Check CRD default to false (#1063) --- .../operator/api/config/ConfigurationService.java | 2 +- .../operator/api/config/Utils.java | 2 +- .../config/ConfigurationServiceProviderTest.java | 6 +++--- .../operator/api/config/UtilsTest.java | 4 ++-- .../operator/processing/ControllerTest.java | 15 --------------- 5 files changed, 7 insertions(+), 22 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index f13ae118f8..e2f3ac77c9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -77,7 +77,7 @@ default Config getClientConfiguration() { * @return {@code true} if CRDs should be checked (default), {@code false} otherwise */ default boolean checkCRDAndValidateLocalModel() { - return true; + return false; } int DEFAULT_RECONCILIATION_THREADS_NUMBER = 5; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index db882ae09e..d74d27f2d4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -64,7 +64,7 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, false); } public static boolean debugThreadPool() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java index f69dd0bc29..f8c1a0f6aa 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java @@ -35,16 +35,16 @@ void shouldProvideTheSetInstanceIfProvided() { @Test void shouldBePossibleToOverrideConfigOnce() { final var config = new AbstractConfigurationService(null); - assertTrue(config.checkCRDAndValidateLocalModel()); + assertFalse(config.checkCRDAndValidateLocalModel()); ConfigurationServiceProvider.set(config); var instance = ConfigurationServiceProvider.instance(); assertEquals(config, instance); - ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); + ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(true)); instance = ConfigurationServiceProvider.instance(); assertNotEquals(config, instance); - assertFalse(instance.checkCRDAndValidateLocalModel()); + assertTrue(instance.checkCRDAndValidateLocalModel()); assertThrows(IllegalStateException.class, () -> ConfigurationServiceProvider.overrideCurrent(o -> o.withCloseClientOnStop(false))); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 0a24d4f26d..75d263a4e1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -20,8 +20,8 @@ class UtilsTest { @Test - void shouldCheckCRDAndValidateLocalModelByDefault() { - assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + void shouldNotCheckCRDAndValidateLocalModelByDefault() { + assertFalse(Utils.shouldCheckCRDAndValidateLocalModel()); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 2f5b9bf4ba..1fb5b06562 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -12,7 +11,6 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @SuppressWarnings("unchecked") @@ -47,19 +45,6 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { } } - @Test - void crdShouldBeCheckedForCustomResourcesByDefault() { - ConfigurationServiceProvider.reset(); - final var client = MockKubernetesClient.client(TestCustomResource.class); - when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); - - final var controller = new Controller(reconciler, configuration, client); - // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we - // expect a MissingCRDException to be thrown - assertThrows(MissingCRDException.class, controller::start); - verify(client, times(1)).apiextensions(); - } - @Test void usesFinalizerIfThereIfReconcilerImplementsCleaner() { Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); From cb1fa110f3333bb9dda2b0d10c637cb28b3ea8cb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 22 Mar 2022 12:38:40 +0100 Subject: [PATCH 0091/1319] feat: add managed dependent webpage reconciler implementation (#1050) * feat: give explicit access to client from KubernetesClientAware * refactor: WebPageReconcilerDependentResources -> WebPageStandaloneDependentsReconciler * refactor: extract methods into Utils, extract dependent resources * fix: make Utils.simulateErrorIfRequested throw exception * feat: add managed dependent reconciler implementation * fix: unify artifactId --- .../managed/KubernetesClientAware.java | 2 + .../KubernetesDependentResource.java | 5 + sample-operators/webpage/pom.xml | 2 +- .../sample/ConfigMapDependentResource.java | 65 ++++++ .../sample/DeploymentDependentResource.java | 45 ++++ .../sample/ErrorSimulationException.java | 2 +- .../sample/ServiceDependentResource.java | 33 +++ .../operator/sample/Utils.java | 40 ++++ .../WebPageManagedDependentsReconciler.java | 48 ++++ .../operator/sample/WebPageOperator.java | 2 +- .../operator/sample/WebPageReconciler.java | 56 +++-- .../WebPageReconcilerDependentResources.java | 206 ------------------ ...WebPageStandaloneDependentsReconciler.java | 92 ++++++++ .../sample/WebPageOperatorAbstractTest.java | 6 +- .../WebPageOperatorDependentResourcesE2E.java | 2 +- 15 files changed, 362 insertions(+), 244 deletions(-) create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java delete mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java index 0e787753a5..2a28813c35 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java @@ -4,4 +4,6 @@ public interface KubernetesClientAware { void setKubernetesClient(KubernetesClient kubernetesClient); + + KubernetesClient getKubernetesClient(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 233deb020d..32606a5ebd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -156,6 +156,11 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + @Override protected R desired(P primary, Context

context) { return super.desired(primary, context); diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 3c07557175..08b08e3baa 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -10,7 +10,7 @@ 3.0.0-SNAPSHOT - webpage + sample-webpage-operator Operator SDK - Samples - WebPage Provisions an nginx Webserver based on a CRD with give html jar diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java new file mode 100644 index 0000000000..8cacd49765 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; + +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; + +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +class ConfigMapDependentResource extends CRUKubernetesDependentResource + implements PrimaryToSecondaryMapper { + + private static final Logger log = LoggerFactory.getLogger(ConfigMapDependentResource.class); + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, + Context context) { + var res = super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", + ns); + getKubernetesClient() + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + return res; + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java new file mode 100644 index 0000000000..0fd907f245 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; + +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +class DeploymentDependentResource extends CRUDKubernetesDependentResource { + + public DeploymentDependentResource() { + super(Deployment.class); + } + + @Override + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java index e2d3f3c1dd..94efc05baa 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.sample; -public class ErrorSimulationException extends RuntimeException { +public class ErrorSimulationException extends Exception { public ErrorSimulationException(String message) { super(message); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java new file mode 100644 index 0000000000..cb06ab594b --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; + +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +class ServiceDependentResource extends CRUDKubernetesDependentResource { + + public ServiceDependentResource() { + super(Service.class); + } + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, getClass(), "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java new file mode 100644 index 0000000000..f08c78f5db --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator.sample; + +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; + +public class Utils { + + private Utils() {} + + static WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); + status.setAreWeGood(true); + status.setErrorMessage(null); + return status; + } + + static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + static String serviceName(WebPage webPage) { + return webPage.getMetadata().getName(); + } + + static ErrorStatusUpdateControl handleError(WebPage resource, Exception e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return ErrorStatusUpdateControl.updateStatus(resource); + } + + static void simulateErrorIfRequested(WebPage webPage) throws ErrorSimulationException { + if (webPage.getSpec().getHtml().contains("error")) { + // special case just to showcase error if doing a demo + throw new ErrorSimulationException("Simulating error"); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java new file mode 100644 index 0000000000..f257893f8c --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java @@ -0,0 +1,48 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; +import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; + +/** + * Shows how to implement a reconciler with managed dependent resources. + */ +@ControllerConfiguration( + labelSelector = SELECTOR, + dependents = { + @Dependent(type = ConfigMapDependentResource.class), + @Dependent(type = DeploymentDependentResource.class), + @Dependent(type = ServiceDependentResource.class) + }) +public class WebPageManagedDependentsReconciler + implements Reconciler, ErrorStatusHandler { + + static final String SELECTOR = "managed"; + + @Override + public ErrorStatusUpdateControl updateErrorStatus(WebPage resource, + Context context, Exception e) { + return handleError(resource, e); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { + simulateErrorIfRequested(webPage); + + final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow() + .getMetadata().getName(); + webPage.setStatus(createStatus(name)); + return UpdateControl.updateStatus(webPage); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index 0277da8199..30958dbc84 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -30,7 +30,7 @@ public static void main(String[] args) throws IOException { if (WEBPAGE_RECONCILER_ENV_VALUE.equals(System.getenv(WEBPAGE_RECONCILER_ENV))) { operator.register(new WebPageReconciler(client)); } else { - operator.register(new WebPageReconcilerDependentResources(client)); + operator.register(new WebPageStandaloneDependentsReconciler(client)); } operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 570f2ffb53..3d74dcb00b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -8,16 +8,33 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; + /** Shows how to implement reconciler using the low level api directly. */ @ControllerConfiguration( labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) @@ -35,11 +52,9 @@ public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } - InformerEventSource configMapEventSource; - @Override public List prepareEventSources(EventSourceContext context) { - configMapEventSource = + var configMapEventSource = new InformerEventSource<>(InformerConfiguration.from(context, ConfigMap.class) .withLabelSelector(LOW_LEVEL_LABEL_KEY) .build(), context); @@ -55,11 +70,11 @@ public List prepareEventSources(EventSourceContext context } @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { log.info("Reconciling web page: {}", webPage); - if (webPage.getSpec().getHtml().contains("error")) { - throw new ErrorSimulationException("Simulating error"); - } + simulateErrorIfRequested(webPage); + String ns = webPage.getMetadata().getNamespace(); String configMapName = configMapName(webPage); String deploymentName = deploymentName(webPage); @@ -107,14 +122,6 @@ public UpdateControl reconcile(WebPage webPage, Context contex return UpdateControl.updateStatus(webPage); } - private WebPageStatus createStatus(String configMapName) { - WebPageStatus status = new WebPageStatus(); - status.setHtmlConfigMap(configMapName); - status.setAreWeGood(true); - status.setErrorMessage(null); - return status; - } - private boolean match(Deployment desiredDeployment, Deployment deployment) { if (deployment == null) { return false; @@ -196,22 +203,9 @@ public static Map lowLevelLabel() { return labels; } - private static String configMapName(WebPage nginx) { - return nginx.getMetadata().getName() + "-html"; - } - - private static String deploymentName(WebPage nginx) { - return nginx.getMetadata().getName(); - } - - private static String serviceName(WebPage nginx) { - return nginx.getMetadata().getName(); - } - @Override public ErrorStatusUpdateControl updateErrorStatus( WebPage resource, Context context, Exception e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return ErrorStatusUpdateControl.updateStatus(resource); + return handleError(resource, e); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java deleted file mode 100644 index 7698c3a16d..0000000000 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ /dev/null @@ -1,206 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; - -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; - -/** - * Shows how to implement reconciler using standalone dependent resources. - */ -@ControllerConfiguration( - labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) -public class WebPageReconcilerDependentResources - implements Reconciler, ErrorStatusHandler, EventSourceInitializer { - - public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; - private static final Logger log = - LoggerFactory.getLogger(WebPageReconcilerDependentResources.class); - private final KubernetesClient kubernetesClient; - - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; - - public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { - this.kubernetesClient = kubernetesClient; - createDependentResources(kubernetesClient); - } - - @Override - public List prepareEventSources(EventSourceContext context) { - return List.of( - configMapDR.initEventSource(context), - deploymentDR.initEventSource(context), - serviceDR.initEventSource(context)); - } - - @Override - public UpdateControl reconcile(WebPage webPage, Context context) { - if (webPage.getSpec().getHtml().contains("error")) { - // special case just to showcase error if doing a demo - throw new ErrorSimulationException("Simulating error"); - } - - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); - - webPage.setStatus( - createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); - return UpdateControl.updateStatus(webPage); - } - - private WebPageStatus createStatus(String configMapName) { - WebPageStatus status = new WebPageStatus(); - status.setHtmlConfigMap(configMapName); - status.setAreWeGood(true); - status.setErrorMessage(null); - return status; - } - - @Override - public ErrorStatusUpdateControl updateErrorStatus( - WebPage resource, Context retryInfo, Exception e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return ErrorStatusUpdateControl.updateStatus(resource); - } - - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - this.configMapDR.setKubernetesClient(client); - configMapDR.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - - this.deploymentDR = - new CRUDKubernetesDependentResource<>(Deployment.class) { - - @Override - protected Deployment desired(WebPage webPage, Context context) { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - } - }; - deploymentDR.setKubernetesClient(client); - deploymentDR.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - - this.serviceDR = - new CRUDKubernetesDependentResource<>(Service.class) { - - @Override - protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, getClass(), "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - } - }; - serviceDR.setKubernetesClient(client); - serviceDR.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - } - - public static String configMapName(WebPage nginx) { - return nginx.getMetadata().getName() + "-html"; - } - - public static String deploymentName(WebPage nginx) { - return nginx.getMetadata().getName(); - } - - public static String serviceName(WebPage webPage) { - return webPage.getMetadata().getName(); - } - - private class ConfigMapDependentResource - extends CRUKubernetesDependentResource - implements PrimaryToSecondaryMapper { - - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - - @Override - protected ConfigMap desired(WebPage webPage, Context context) { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(WebPageReconcilerDependentResources.configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - } - - @Override - public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, - Context context) { - var res = super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - return res; - } - - @Override - public ResourceID associatedSecondaryID(WebPage primary) { - return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); - } - } -} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java new file mode 100644 index 0000000000..180c6ade65 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java @@ -0,0 +1,92 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; + +/** + * Shows how to implement reconciler using standalone dependent resources. + */ +@ControllerConfiguration( + labelSelector = WebPageStandaloneDependentsReconciler.DEPENDENT_RESOURCE_LABEL_SELECTOR) +public class WebPageStandaloneDependentsReconciler + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { + + public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; + private static final Logger log = + LoggerFactory.getLogger(WebPageStandaloneDependentsReconciler.class); + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + + public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) { + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + return List.of( + configMapDR.initEventSource(context), + deploymentDR.initEventSource(context), + serviceDR.initEventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { + simulateErrorIfRequested(webPage); + + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } + + @Override + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context retryInfo, Exception e) { + return handleError(resource, e); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.configMapDR.setKubernetesClient(client); + configMapDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.deploymentDR = new DeploymentDependentResource(); + deploymentDR.setKubernetesClient(client); + deploymentDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.serviceDR = new ServiceDependentResource(); + serviceDR.setKubernetesClient(client); + serviceDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + } +} diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java index 092ad6f25a..65a3084989 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java @@ -20,7 +20,8 @@ import io.fabric8.kubernetes.client.LocalPortForward; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; -import static io.javaoperatorsdk.operator.sample.WebPageReconcilerDependentResources.serviceName; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -50,8 +51,7 @@ void testAddingWebPage() { .until( () -> { var actual = operator().get(WebPage.class, TEST_PAGE); - var deployment = operator().get(Deployment.class, - WebPageReconcilerDependentResources.deploymentName(webPage)); + var deployment = operator().get(Deployment.class, deploymentName(webPage)); return Boolean.TRUE.equals(actual.getStatus().getAreWeGood()) && Objects.equals(deployment.getSpec().getReplicas(), diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java index 3bfdd3f45b..df024b890a 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java @@ -18,7 +18,7 @@ public WebPageOperatorDependentResourcesE2E() throws FileNotFoundException {} isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withReconciler(new WebPageReconcilerDependentResources(client)) + .withReconciler(new WebPageStandaloneDependentsReconciler(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) From 76650a5201816912523a9c4eca51dc74069a2b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 12:40:48 +0100 Subject: [PATCH 0092/1319] docs: dependent resources (#1026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: dependent resources * docs: dr motivation * docs: add flow chart to motivation, detail design * fix: remove now fixed fix-me :) [skip-ci] * fix: missing word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: re-order sentences Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: clarify Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: remove extra word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: header * fix: simplify dependent logic [skip-ci] * fix: reword [skip ci] Co-authored-by: Attila Mészáros Co-authored-by: Chris Laprun Co-authored-by: Chris Laprun Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> --- docs/_data/sidebar.yml | 2 + docs/documentation/dependent-resources.md | 117 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 docs/documentation/dependent-resources.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 3f3f1734b7..fe27a8e6aa 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -9,6 +9,8 @@ url: /docs/glossary - title: Features url: /docs/features + - title: Dependent Resource Feature + url: /docs/dependent-resources - title: Patterns and Best Practices url: /docs/patterns-best-practices - title: FAQ diff --git a/docs/documentation/dependent-resources.md b/docs/documentation/dependent-resources.md new file mode 100644 index 0000000000..225c5a4377 --- /dev/null +++ b/docs/documentation/dependent-resources.md @@ -0,0 +1,117 @@ +--- +title: Dependent Resources Feature +description: Dependent Resources Feature +layout: docs +permalink: /docs/dependent-resources +--- + +# Dependent Resources + +DISCLAIMER: The Dependent Resource support is relatively new and, while we strove to cover what we +anticipate will be the most common use cases, the implementation is not simple and might still +evolve. As a result, some APIs could be still a subject of change in the future. However, +non-backwards compatible changes are expected to be trivial to adapt to. + +## Motivations and Goals + +Most operators need to deal with secondary resources when trying to realize the desired state +described by the primary resource it is in charge of. For example, the Kubernetes-native +`Deployment` controller needs to manage `ReplicaSet` instances as part of a `Deployment`'s +reconciliation process. In this instance, `ReplicatSet` is considered a secondary resource for +the `Deployment` controller. + +Controllers that deal with secondary resources typically need to perform the following steps, for +each secondary resource: + +```mermaid +flowchart TD + +compute[Compute desired secondary resource based on primary state] --> A +A{Secondary resource exists?} +A -- Yes --> match +A -- No --> Create --> Done + +match{Matches desired state as defined by primary?} +match -- Yes --> Done +match -- No --> Update --> Done + +``` + +While these steps are not difficult in and of themselves, there are some subtleties that can lead to +bugs or sub-optimal code if not done right. As this process is pretty much similar for each +dependent resource, it makes sense for the SDK to offer some level of support to remove the +boilerplate code of these repetitive actions. It should be possible to handle common cases (such as +dealing with Kubernetes-native secondary resources) in a semi-declarative way with only a minimal +amount of code, JOSDK taking care of wiring everything accordingly. + +Moreover, in order for your reconciler to get informed of events on these secondary resources, you +need to configure and create event sources and maintain them. JOSDK already makes it rather easy to +deal with these, but dependent resources makes it even simpler. + +Finally, there are also opportunities for the SDK to transparently add features that are even +trickier to get right, such as immediate caching of updated or created resources (so that your +reconciler doesn't need to wait for a cluster roundtrip to continue its work) and associated event +filtering (so that something your reconciler just changed doesn't re-trigger a reconciliation, for +example). + +## Design + +### `DependentResource` vs. `AbstractDependentResource` + +The new `DependentResource` interface lies at the core of the design and strives to encapsulate the +logic that is required to reconcile the state of the associated secondary resource based on the +state of the primary one. For most cases, this logic will follow the flow expressed above and JOSDK +provides a very convenient implementation of this logic in the form of the +`AbstractDependentResource` class. If your logic doesn't fit this pattern, though, you can still +provide your own `reconcile` method implementation. While the benefits of using dependent resources +are less obvious in that case, this allows you to separate the logic necessary to deal with each +secondary resource in its own class that can then be tested in isolation via unit tests. You can +also use the declarative support with your own implementations as we shall see later on. + +`AbstractDependentResource` is designed so that classes extending it specify which functionality +they support by implementing trait interfaces. This design has been selected to express the fact +that not all secondary resources are completely under the control of the primary reconciler: some +dependent resources are only ever created or updated for example and we needed a way to let JOSDK +know when that is the case. We therefore provide trait interfaces: `Creator`, +`Updater` and `Deleter` to express that the `DependentResource` implementation will provide custom +functionality to create, update and delete its associated secondary resources, respectively. If +these traits are not implemented then parts of the logic described above is never triggered: if your +implementation doesn't implement `Creator`, for example, +`AbstractDependentResource` will never try to create the associated secondary resource, even if it +doesn't exist. It is possible to not implement any of these traits and therefore create read-only dependent resources that will trigger your +reconciler whenever a user interacts with them but that are never modified by your reconciler +itself. + +### Batteries included: convenient `DependentResource` implementations! + +JOSDK also offers several other convenient implementations building on top of +`AbstractDependentResource` that you can use as starting points for your own implementations. + +One such implementation is the `KubernetesDependentResource` class that makes it really easy to work +with Kubernetes-native resources. In this case, you usually only need to provide an +implementation for the `desired` method to tell JOSDK what the desired state of your secondary +resource should be based on the specified primary resource state. JOSDK takes care of everything +else using default implementations that you can override in case you need more precise control of +what's going on. + +We also provide implementations that makes it very easy to cache +(`AbstractCachingDependentResource`) or make it easy to poll for changes in external +resources (`PollingDependentResource`, `PerResourcePollingDependentResource`). All the provided +implementations can be found in the `io/javaoperatorsdk/operator/processing/dependent` package of +the `operator-framework-core` module. + +## Managed Dependent Resources + +As mentioned previously, one goal of this implementation is to make it possible to +semi-declaratively create and wire dependent resources. You can annotate your reconciler with +`@Dependent` annotations that specify which `DependentResource` implementation it depends upon. +JOSDK will take the appropriate steps to wire everything together and call your +`DependentResource` implementations `reconcile` method before your primary resource is reconciled. +This makes sense in most use cases where the logic associated with the primary resource is usually +limited to status handling based on the state of the secondary resources. This behavior and +automated handling is referred to as "managed" because the `DependentResource` +implementations are managed by JOSDK. + +## Standalone Dependent Resources + +## Other Dependent Resources features \ No newline at end of file From d4df638dbc5e92947b0cbb42b84b52057e95d46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 12:54:01 +0100 Subject: [PATCH 0093/1319] fix: set default CRD check to false (#1057) --- .../operator/api/config/ConfigurationService.java | 2 +- .../java/io/javaoperatorsdk/operator/api/config/Utils.java | 2 +- .../io/javaoperatorsdk/operator/api/config/UtilsTest.java | 2 +- .../javaoperatorsdk/operator/processing/ControllerTest.java | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 49ccc1da07..dc19d00c56 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -75,7 +75,7 @@ default Config getClientConfiguration() { * @return {@code true} if CRDs should be checked (default), {@code false} otherwise */ default boolean checkCRDAndValidateLocalModel() { - return true; + return false; } int DEFAULT_RECONCILIATION_THREADS_NUMBER = 5; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index 06bbcaa7a8..10a213ab11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -61,7 +61,7 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, false); } public static boolean debugThreadPool() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index a17eb1ba59..1e8468ed1d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -8,7 +8,7 @@ class UtilsTest { @Test void shouldCheckCRDAndValidateLocalModelByDefault() { - assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + assertFalse(Utils.shouldCheckCRDAndValidateLocalModel()); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index d61b50d583..4c749dcf71 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -54,10 +54,10 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { } @Test - void crdShouldBeCheckedForCustomResourcesByDefault() { + void crdCanBeCheckedForCustomResources() { final var client = mock(KubernetesClient.class); final var configurationService = mock(ConfigurationService.class); - when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); + when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(true); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); From 8078fec2b5036228ef76be264b037f36b70c5cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 13:00:09 +0100 Subject: [PATCH 0094/1319] docs: mermaid support (#1053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: dependent resources * docs: dr motivation * docs: add flow chart to motivation, detail design * fix: remove now fixed fix-me :) [skip-ci] * fix: missing word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: re-order sentences Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: clarify Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: remove extra word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: header * fix: simplify dependent logic [skip-ci] * fix: reword [skip ci] Co-authored-by: Attila Mészáros * chore: move version to 3.0.0-SNAPSHOT to reflect API breakage (#1051) * docs: mermaid support * fix: mermaid integration * docs: dependent resources * docs: dr motivation * docs: add flow chart to motivation, detail design * fix: remove now fixed fix-me :) [skip-ci] * fix: missing word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: re-order sentences Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: clarify Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: remove extra word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: header * fix: simplify dependent logic [skip-ci] * fix: reword [skip ci] Co-authored-by: Attila Mészáros * fix: diagram * fix: flag not needed Co-authored-by: Chris Laprun Co-authored-by: Chris Laprun Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> --- docs/_layouts/docs.html | 2 ++ docs/assets/js/mermaid.min.js | 3 +++ docs/assets/js/mermaid.min.js.map | 1 + docs/documentation/dependent-resources.md | 5 +++-- 4 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 docs/assets/js/mermaid.min.js create mode 100644 docs/assets/js/mermaid.min.js.map diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index b5cef5c127..16db62e0c0 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -6,6 +6,7 @@ {% if page.title %}{{ page.title }}{% else %}{{ site.title | escape }}{% endif %} {% include links.html %} {% include analytics.html %} + {% include navbar.html %} @@ -20,6 +21,7 @@

{% include anchor_headings.html html=content anchorBody="#" %}
+ diff --git a/docs/assets/js/mermaid.min.js b/docs/assets/js/mermaid.min.js new file mode 100644 index 0000000000..8f8ef35651 --- /dev/null +++ b/docs/assets/js/mermaid.min.js @@ -0,0 +1,3 @@ +/*! For license information please see mermaid.min.js.LICENSE.txt */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.mermaid=e():t.mermaid=e()}("undefined"!=typeof self?self:this,(function(){return(()=>{var t={1362:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,6],n=[1,7],r=[1,8],i=[1,9],a=[1,12],o=[1,11],s=[1,15,24],c=[1,19],u=[1,31],l=[1,34],h=[1,32],f=[1,33],d=[1,35],p=[1,36],y=[1,37],g=[1,38],m=[1,41],v=[1,42],b=[1,43],_=[1,44],x=[15,24],w=[1,56],k=[1,57],T=[1,58],E=[1,59],C=[1,60],S=[1,61],A=[15,24,31,38,39,47,50,51,52,53,54,55,60,62],M=[15,24,29,31,38,39,43,47,50,51,52,53,54,55,60,62,77,78,79,80],N=[7,8,9,10,15,18,22,24],D=[47,77,78,79,80],O=[47,54,55,77,78,79,80],B=[47,50,51,52,53,77,78,79,80],L=[15,24,31],I=[1,93],R={trace:function(){},yy:{},symbols_:{error:2,start:3,mermaidDoc:4,direction:5,directive:6,direction_tb:7,direction_bt:8,direction_rl:9,direction_lr:10,graphConfig:11,openDirective:12,typeDirective:13,closeDirective:14,NEWLINE:15,":":16,argDirective:17,open_directive:18,type_directive:19,arg_directive:20,close_directive:21,CLASS_DIAGRAM:22,statements:23,EOF:24,statement:25,className:26,alphaNumToken:27,classLiteralName:28,GENERICTYPE:29,relationStatement:30,LABEL:31,classStatement:32,methodStatement:33,annotationStatement:34,clickStatement:35,cssClassStatement:36,CLASS:37,STYLE_SEPARATOR:38,STRUCT_START:39,members:40,STRUCT_STOP:41,ANNOTATION_START:42,ANNOTATION_END:43,MEMBER:44,SEPARATOR:45,relation:46,STR:47,relationType:48,lineType:49,AGGREGATION:50,EXTENSION:51,COMPOSITION:52,DEPENDENCY:53,LINE:54,DOTTED_LINE:55,CALLBACK:56,LINK:57,LINK_TARGET:58,CLICK:59,CALLBACK_NAME:60,CALLBACK_ARGS:61,HREF:62,CSSCLASS:63,commentToken:64,textToken:65,graphCodeTokens:66,textNoTagsToken:67,TAGSTART:68,TAGEND:69,"==":70,"--":71,PCT:72,DEFAULT:73,SPACE:74,MINUS:75,keywords:76,UNICODE_TEXT:77,NUM:78,ALPHA:79,BQUOTE_STR:80,$accept:0,$end:1},terminals_:{2:"error",7:"direction_tb",8:"direction_bt",9:"direction_rl",10:"direction_lr",15:"NEWLINE",16:":",18:"open_directive",19:"type_directive",20:"arg_directive",21:"close_directive",22:"CLASS_DIAGRAM",24:"EOF",29:"GENERICTYPE",31:"LABEL",37:"CLASS",38:"STYLE_SEPARATOR",39:"STRUCT_START",41:"STRUCT_STOP",42:"ANNOTATION_START",43:"ANNOTATION_END",44:"MEMBER",45:"SEPARATOR",47:"STR",50:"AGGREGATION",51:"EXTENSION",52:"COMPOSITION",53:"DEPENDENCY",54:"LINE",55:"DOTTED_LINE",56:"CALLBACK",57:"LINK",58:"LINK_TARGET",59:"CLICK",60:"CALLBACK_NAME",61:"CALLBACK_ARGS",62:"HREF",63:"CSSCLASS",66:"graphCodeTokens",68:"TAGSTART",69:"TAGEND",70:"==",71:"--",72:"PCT",73:"DEFAULT",74:"SPACE",75:"MINUS",76:"keywords",77:"UNICODE_TEXT",78:"NUM",79:"ALPHA",80:"BQUOTE_STR"},productions_:[0,[3,1],[3,1],[3,2],[5,1],[5,1],[5,1],[5,1],[4,1],[6,4],[6,6],[12,1],[13,1],[17,1],[14,1],[11,4],[23,1],[23,2],[23,3],[26,1],[26,1],[26,2],[26,2],[26,2],[25,1],[25,2],[25,1],[25,1],[25,1],[25,1],[25,1],[25,1],[25,1],[32,2],[32,4],[32,5],[32,7],[34,4],[40,1],[40,2],[33,1],[33,2],[33,1],[33,1],[30,3],[30,4],[30,4],[30,5],[46,3],[46,2],[46,2],[46,1],[48,1],[48,1],[48,1],[48,1],[49,1],[49,1],[35,3],[35,4],[35,3],[35,4],[35,4],[35,5],[35,3],[35,4],[35,4],[35,5],[35,3],[35,4],[35,4],[35,5],[36,3],[64,1],[64,1],[65,1],[65,1],[65,1],[65,1],[65,1],[65,1],[65,1],[67,1],[67,1],[67,1],[67,1],[27,1],[27,1],[27,1],[28,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:r.setDirection("TB");break;case 5:r.setDirection("BT");break;case 6:r.setDirection("RL");break;case 7:r.setDirection("LR");break;case 11:r.parseDirective("%%{","open_directive");break;case 12:r.parseDirective(a[s],"type_directive");break;case 13:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 14:r.parseDirective("}%%","close_directive","class");break;case 19:case 20:this.$=a[s];break;case 21:this.$=a[s-1]+a[s];break;case 22:case 23:this.$=a[s-1]+"~"+a[s];break;case 24:r.addRelation(a[s]);break;case 25:a[s-1].title=r.cleanupLabel(a[s]),r.addRelation(a[s-1]);break;case 33:r.addClass(a[s]);break;case 34:r.addClass(a[s-2]),r.setCssClass(a[s-2],a[s]);break;case 35:r.addClass(a[s-3]),r.addMembers(a[s-3],a[s-1]);break;case 36:r.addClass(a[s-5]),r.setCssClass(a[s-5],a[s-3]),r.addMembers(a[s-5],a[s-1]);break;case 37:r.addAnnotation(a[s],a[s-2]);break;case 38:this.$=[a[s]];break;case 39:a[s].push(a[s-1]),this.$=a[s];break;case 40:case 42:case 43:break;case 41:r.addMember(a[s-1],r.cleanupLabel(a[s]));break;case 44:this.$={id1:a[s-2],id2:a[s],relation:a[s-1],relationTitle1:"none",relationTitle2:"none"};break;case 45:this.$={id1:a[s-3],id2:a[s],relation:a[s-1],relationTitle1:a[s-2],relationTitle2:"none"};break;case 46:this.$={id1:a[s-3],id2:a[s],relation:a[s-2],relationTitle1:"none",relationTitle2:a[s-1]};break;case 47:this.$={id1:a[s-4],id2:a[s],relation:a[s-2],relationTitle1:a[s-3],relationTitle2:a[s-1]};break;case 48:this.$={type1:a[s-2],type2:a[s],lineType:a[s-1]};break;case 49:this.$={type1:"none",type2:a[s],lineType:a[s-1]};break;case 50:this.$={type1:a[s-1],type2:"none",lineType:a[s]};break;case 51:this.$={type1:"none",type2:"none",lineType:a[s]};break;case 52:this.$=r.relationType.AGGREGATION;break;case 53:this.$=r.relationType.EXTENSION;break;case 54:this.$=r.relationType.COMPOSITION;break;case 55:this.$=r.relationType.DEPENDENCY;break;case 56:this.$=r.lineType.LINE;break;case 57:this.$=r.lineType.DOTTED_LINE;break;case 58:case 64:this.$=a[s-2],r.setClickEvent(a[s-1],a[s]);break;case 59:case 65:this.$=a[s-3],r.setClickEvent(a[s-2],a[s-1]),r.setTooltip(a[s-2],a[s]);break;case 60:case 68:this.$=a[s-2],r.setLink(a[s-1],a[s]);break;case 61:case 69:this.$=a[s-3],r.setLink(a[s-2],a[s-1],a[s]);break;case 62:case 70:this.$=a[s-3],r.setLink(a[s-2],a[s-1]),r.setTooltip(a[s-2],a[s]);break;case 63:case 71:this.$=a[s-4],r.setLink(a[s-3],a[s-2],a[s]),r.setTooltip(a[s-3],a[s-1]);break;case 66:this.$=a[s-3],r.setClickEvent(a[s-2],a[s-1],a[s]);break;case 67:this.$=a[s-4],r.setClickEvent(a[s-3],a[s-2],a[s-1]),r.setTooltip(a[s-3],a[s]);break;case 72:r.setCssClass(a[s-1],a[s])}},table:[{3:1,4:2,5:3,6:4,7:e,8:n,9:r,10:i,11:5,12:10,18:a,22:o},{1:[3]},{1:[2,1]},{1:[2,2]},{3:13,4:2,5:3,6:4,7:e,8:n,9:r,10:i,11:5,12:10,18:a,22:o},{1:[2,8]},t(s,[2,4]),t(s,[2,5]),t(s,[2,6]),t(s,[2,7]),{13:14,19:[1,15]},{15:[1,16]},{19:[2,11]},{1:[2,3]},{14:17,16:[1,18],21:c},t([16,21],[2,12]),{5:29,6:28,7:e,8:n,9:r,10:i,12:10,18:a,23:20,25:21,26:30,27:39,28:40,30:22,32:23,33:24,34:25,35:26,36:27,37:u,42:l,44:h,45:f,56:d,57:p,59:y,63:g,77:m,78:v,79:b,80:_},{15:[1,45]},{17:46,20:[1,47]},{15:[2,14]},{24:[1,48]},{15:[1,49],24:[2,16]},t(x,[2,24],{31:[1,50]}),t(x,[2,26]),t(x,[2,27]),t(x,[2,28]),t(x,[2,29]),t(x,[2,30]),t(x,[2,31]),t(x,[2,32]),t(x,[2,40],{46:51,48:54,49:55,31:[1,53],47:[1,52],50:w,51:k,52:T,53:E,54:C,55:S}),{26:62,27:39,28:40,77:m,78:v,79:b,80:_},t(x,[2,42]),t(x,[2,43]),{27:63,77:m,78:v,79:b},{26:64,27:39,28:40,77:m,78:v,79:b,80:_},{26:65,27:39,28:40,77:m,78:v,79:b,80:_},{26:66,27:39,28:40,77:m,78:v,79:b,80:_},{47:[1,67]},t(A,[2,19],{27:39,28:40,26:68,29:[1,69],77:m,78:v,79:b,80:_}),t(A,[2,20],{29:[1,70]}),t(M,[2,86]),t(M,[2,87]),t(M,[2,88]),t([15,24,29,31,38,39,47,50,51,52,53,54,55,60,62],[2,89]),t(N,[2,9]),{14:71,21:c},{21:[2,13]},{1:[2,15]},{5:29,6:28,7:e,8:n,9:r,10:i,12:10,18:a,23:72,24:[2,17],25:21,26:30,27:39,28:40,30:22,32:23,33:24,34:25,35:26,36:27,37:u,42:l,44:h,45:f,56:d,57:p,59:y,63:g,77:m,78:v,79:b,80:_},t(x,[2,25]),{26:73,27:39,28:40,47:[1,74],77:m,78:v,79:b,80:_},{46:75,48:54,49:55,50:w,51:k,52:T,53:E,54:C,55:S},t(x,[2,41]),{49:76,54:C,55:S},t(D,[2,51],{48:77,50:w,51:k,52:T,53:E}),t(O,[2,52]),t(O,[2,53]),t(O,[2,54]),t(O,[2,55]),t(B,[2,56]),t(B,[2,57]),t(x,[2,33],{38:[1,78],39:[1,79]}),{43:[1,80]},{47:[1,81]},{47:[1,82]},{60:[1,83],62:[1,84]},{27:85,77:m,78:v,79:b},t(A,[2,21]),t(A,[2,22]),t(A,[2,23]),{15:[1,86]},{24:[2,18]},t(L,[2,44]),{26:87,27:39,28:40,77:m,78:v,79:b,80:_},{26:88,27:39,28:40,47:[1,89],77:m,78:v,79:b,80:_},t(D,[2,50],{48:90,50:w,51:k,52:T,53:E}),t(D,[2,49]),{27:91,77:m,78:v,79:b},{40:92,44:I},{26:94,27:39,28:40,77:m,78:v,79:b,80:_},t(x,[2,58],{47:[1,95]}),t(x,[2,60],{47:[1,97],58:[1,96]}),t(x,[2,64],{47:[1,98],61:[1,99]}),t(x,[2,68],{47:[1,101],58:[1,100]}),t(x,[2,72]),t(N,[2,10]),t(L,[2,46]),t(L,[2,45]),{26:102,27:39,28:40,77:m,78:v,79:b,80:_},t(D,[2,48]),t(x,[2,34],{39:[1,103]}),{41:[1,104]},{40:105,41:[2,38],44:I},t(x,[2,37]),t(x,[2,59]),t(x,[2,61]),t(x,[2,62],{58:[1,106]}),t(x,[2,65]),t(x,[2,66],{47:[1,107]}),t(x,[2,69]),t(x,[2,70],{58:[1,108]}),t(L,[2,47]),{40:109,44:I},t(x,[2,35]),{41:[2,39]},t(x,[2,63]),t(x,[2,67]),t(x,[2,71]),{41:[1,110]},t(x,[2,36])],defaultActions:{2:[2,1],3:[2,2],5:[2,8],12:[2,11],13:[2,3],19:[2,14],47:[2,13],48:[2,15],72:[2,18],105:[2,39]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},F={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),18;case 1:return 7;case 2:return 8;case 3:return 9;case 4:return 10;case 5:return this.begin("type_directive"),19;case 6:return this.popState(),this.begin("arg_directive"),16;case 7:return this.popState(),this.popState(),21;case 8:return 20;case 9:case 10:case 12:case 19:break;case 11:return 15;case 13:case 14:return 22;case 15:return this.begin("struct"),39;case 16:return"EOF_IN_STRUCT";case 17:return"OPEN_IN_STRUCT";case 18:return this.popState(),41;case 20:return"MEMBER";case 21:return 37;case 22:return 63;case 23:return 56;case 24:return 57;case 25:return 59;case 26:return 42;case 27:return 43;case 28:this.begin("generic");break;case 29:case 32:case 35:case 38:case 41:case 44:this.popState();break;case 30:return"GENERICTYPE";case 31:this.begin("string");break;case 33:return"STR";case 34:this.begin("bqstring");break;case 36:return"BQUOTE_STR";case 37:this.begin("href");break;case 39:return 62;case 40:this.begin("callback_name");break;case 42:this.popState(),this.begin("callback_args");break;case 43:return 60;case 45:return 61;case 46:case 47:case 48:case 49:return 58;case 50:case 51:return 51;case 52:case 53:return 53;case 54:return 52;case 55:return 50;case 56:return 54;case 57:return 55;case 58:return 31;case 59:return 38;case 60:return 75;case 61:return"DOT";case 62:return"PLUS";case 63:return 72;case 64:case 65:return"EQUALS";case 66:return 79;case 67:return"PUNCTUATION";case 68:return 78;case 69:return 77;case 70:return 74;case 71:return 24}},rules:[/^(?:%%\{)/,/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:((?:(?!\}%%)[^:.])*))/,/^(?::)/,/^(?:\}%%)/,/^(?:((?:(?!\}%%).|\n)*))/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:classDiagram-v2\b)/,/^(?:classDiagram\b)/,/^(?:[{])/,/^(?:$)/,/^(?:[{])/,/^(?:[}])/,/^(?:[\n])/,/^(?:[^{}\n]*)/,/^(?:class\b)/,/^(?:cssClass\b)/,/^(?:callback\b)/,/^(?:link\b)/,/^(?:click\b)/,/^(?:<<)/,/^(?:>>)/,/^(?:[~])/,/^(?:[~])/,/^(?:[^~]*)/,/^(?:["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:[`])/,/^(?:[`])/,/^(?:[^`]+)/,/^(?:href[\s]+["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:\s*<\|)/,/^(?:\s*\|>)/,/^(?:\s*>)/,/^(?:\s*<)/,/^(?:\s*\*)/,/^(?:\s*o\b)/,/^(?:--)/,/^(?:\.\.)/,/^(?::{1}[^:\n;]+)/,/^(?::{3})/,/^(?:-)/,/^(?:\.)/,/^(?:\+)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:\w+)/,/^(?:[!"#$%&'*+,-.`?\\/])/,/^(?:[0-9]+)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\s)/,/^(?:$)/],conditions:{arg_directive:{rules:[7,8],inclusive:!1},type_directive:{rules:[6,7],inclusive:!1},open_directive:{rules:[5],inclusive:!1},callback_args:{rules:[44,45],inclusive:!1},callback_name:{rules:[41,42,43],inclusive:!1},href:{rules:[38,39],inclusive:!1},struct:{rules:[16,17,18,19,20],inclusive:!1},generic:{rules:[29,30],inclusive:!1},bqstring:{rules:[35,36],inclusive:!1},string:{rules:[32,33],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,9,10,11,12,13,14,15,21,22,23,24,25,26,27,28,31,34,37,40,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71],inclusive:!0}}};function P(){this.yy={}}return R.lexer=F,P.prototype=R,R.Parser=P,new P}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8218).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},5890:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,5],r=[6,9,11,23,41],i=[1,17],a=[1,20],o=[1,25],s=[1,26],c=[1,27],u=[1,28],l=[1,37],h=[23,38,39],f=[4,6,9,11,23,41],d=[34,35,36,37],p=[22,29],y=[1,55],g={trace:function(){},yy:{},symbols_:{error:2,start:3,ER_DIAGRAM:4,document:5,EOF:6,directive:7,line:8,SPACE:9,statement:10,NEWLINE:11,openDirective:12,typeDirective:13,closeDirective:14,":":15,argDirective:16,entityName:17,relSpec:18,role:19,BLOCK_START:20,attributes:21,BLOCK_STOP:22,ALPHANUM:23,attribute:24,attributeType:25,attributeName:26,attributeKeyType:27,attributeComment:28,ATTRIBUTE_WORD:29,ATTRIBUTE_KEY:30,COMMENT:31,cardinality:32,relType:33,ZERO_OR_ONE:34,ZERO_OR_MORE:35,ONE_OR_MORE:36,ONLY_ONE:37,NON_IDENTIFYING:38,IDENTIFYING:39,WORD:40,open_directive:41,type_directive:42,arg_directive:43,close_directive:44,$accept:0,$end:1},terminals_:{2:"error",4:"ER_DIAGRAM",6:"EOF",9:"SPACE",11:"NEWLINE",15:":",20:"BLOCK_START",22:"BLOCK_STOP",23:"ALPHANUM",29:"ATTRIBUTE_WORD",30:"ATTRIBUTE_KEY",31:"COMMENT",34:"ZERO_OR_ONE",35:"ZERO_OR_MORE",36:"ONE_OR_MORE",37:"ONLY_ONE",38:"NON_IDENTIFYING",39:"IDENTIFYING",40:"WORD",41:"open_directive",42:"type_directive",43:"arg_directive",44:"close_directive"},productions_:[0,[3,3],[3,2],[5,0],[5,2],[8,2],[8,1],[8,1],[8,1],[7,4],[7,6],[10,1],[10,5],[10,4],[10,3],[10,1],[17,1],[21,1],[21,2],[24,2],[24,3],[24,3],[24,4],[25,1],[26,1],[27,1],[28,1],[18,3],[32,1],[32,1],[32,1],[32,1],[33,1],[33,1],[19,1],[19,1],[12,1],[13,1],[16,1],[14,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 1:break;case 3:case 7:case 8:this.$=[];break;case 4:a[s-1].push(a[s]),this.$=a[s-1];break;case 5:case 6:case 16:case 23:case 24:case 25:case 35:this.$=a[s];break;case 12:r.addEntity(a[s-4]),r.addEntity(a[s-2]),r.addRelationship(a[s-4],a[s],a[s-2],a[s-3]);break;case 13:r.addEntity(a[s-3]),r.addAttributes(a[s-3],a[s-1]);break;case 14:r.addEntity(a[s-2]);break;case 15:r.addEntity(a[s]);break;case 17:this.$=[a[s]];break;case 18:a[s].push(a[s-1]),this.$=a[s];break;case 19:this.$={attributeType:a[s-1],attributeName:a[s]};break;case 20:this.$={attributeType:a[s-2],attributeName:a[s-1],attributeKeyType:a[s]};break;case 21:this.$={attributeType:a[s-2],attributeName:a[s-1],attributeComment:a[s]};break;case 22:this.$={attributeType:a[s-3],attributeName:a[s-2],attributeKeyType:a[s-1],attributeComment:a[s]};break;case 26:case 34:this.$=a[s].replace(/"/g,"");break;case 27:this.$={cardA:a[s],relType:a[s-1],cardB:a[s-2]};break;case 28:this.$=r.Cardinality.ZERO_OR_ONE;break;case 29:this.$=r.Cardinality.ZERO_OR_MORE;break;case 30:this.$=r.Cardinality.ONE_OR_MORE;break;case 31:this.$=r.Cardinality.ONLY_ONE;break;case 32:this.$=r.Identification.NON_IDENTIFYING;break;case 33:this.$=r.Identification.IDENTIFYING;break;case 36:r.parseDirective("%%{","open_directive");break;case 37:r.parseDirective(a[s],"type_directive");break;case 38:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 39:r.parseDirective("}%%","close_directive","er")}},table:[{3:1,4:e,7:3,12:4,41:n},{1:[3]},t(r,[2,3],{5:6}),{3:7,4:e,7:3,12:4,41:n},{13:8,42:[1,9]},{42:[2,36]},{6:[1,10],7:15,8:11,9:[1,12],10:13,11:[1,14],12:4,17:16,23:i,41:n},{1:[2,2]},{14:18,15:[1,19],44:a},t([15,44],[2,37]),t(r,[2,8],{1:[2,1]}),t(r,[2,4]),{7:15,10:21,12:4,17:16,23:i,41:n},t(r,[2,6]),t(r,[2,7]),t(r,[2,11]),t(r,[2,15],{18:22,32:24,20:[1,23],34:o,35:s,36:c,37:u}),t([6,9,11,15,20,23,34,35,36,37,41],[2,16]),{11:[1,29]},{16:30,43:[1,31]},{11:[2,39]},t(r,[2,5]),{17:32,23:i},{21:33,22:[1,34],24:35,25:36,29:l},{33:38,38:[1,39],39:[1,40]},t(h,[2,28]),t(h,[2,29]),t(h,[2,30]),t(h,[2,31]),t(f,[2,9]),{14:41,44:a},{44:[2,38]},{15:[1,42]},{22:[1,43]},t(r,[2,14]),{21:44,22:[2,17],24:35,25:36,29:l},{26:45,29:[1,46]},{29:[2,23]},{32:47,34:o,35:s,36:c,37:u},t(d,[2,32]),t(d,[2,33]),{11:[1,48]},{19:49,23:[1,51],40:[1,50]},t(r,[2,13]),{22:[2,18]},t(p,[2,19],{27:52,28:53,30:[1,54],31:y}),t([22,29,30,31],[2,24]),{23:[2,27]},t(f,[2,10]),t(r,[2,12]),t(r,[2,34]),t(r,[2,35]),t(p,[2,20],{28:56,31:y}),t(p,[2,21]),t([22,29,31],[2,25]),t(p,[2,26]),t(p,[2,22])],defaultActions:{5:[2,36],7:[2,2],20:[2,39],31:[2,38],37:[2,23],44:[2,18],47:[2,27]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},m={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),41;case 1:return this.begin("type_directive"),42;case 2:return this.popState(),this.begin("arg_directive"),15;case 3:return this.popState(),this.popState(),44;case 4:return 43;case 5:case 6:case 8:case 13:case 17:break;case 7:return 11;case 9:return 9;case 10:return 40;case 11:return 4;case 12:return this.begin("block"),20;case 14:return 30;case 15:return 29;case 16:return 31;case 18:return this.popState(),22;case 19:case 32:return e.yytext[0];case 20:case 24:return 34;case 21:case 25:return 35;case 22:case 26:return 36;case 23:return 37;case 27:case 29:case 30:return 38;case 28:return 39;case 31:return 23;case 33:return 6}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:[\s]+)/i,/^(?:"[^"]*")/i,/^(?:erDiagram\b)/i,/^(?:\{)/i,/^(?:\s+)/i,/^(?:(?:PK)|(?:FK))/i,/^(?:[A-Za-z][A-Za-z0-9\-_]*)/i,/^(?:"[^"]*")/i,/^(?:[\n]+)/i,/^(?:\})/i,/^(?:.)/i,/^(?:\|o\b)/i,/^(?:\}o\b)/i,/^(?:\}\|)/i,/^(?:\|\|)/i,/^(?:o\|)/i,/^(?:o\{)/i,/^(?:\|\{)/i,/^(?:\.\.)/i,/^(?:--)/i,/^(?:\.-)/i,/^(?:-\.)/i,/^(?:[A-Za-z][A-Za-z0-9\-_]*)/i,/^(?:.)/i,/^(?:$)/i],conditions:{open_directive:{rules:[1],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},block:{rules:[13,14,15,16,17,18,19],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,20,21,22,23,24,25,26,27,28,29,30,31,32,33],inclusive:!0}}};function v(){this.yy={}}return g.lexer=m,v.prototype=g,g.Parser=v,new v}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8009).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},3602:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,9],n=[1,7],r=[1,6],i=[1,8],a=[1,20,21,22,23,38,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],o=[2,10],s=[1,20],c=[1,21],u=[1,22],l=[1,23],h=[1,30],f=[1,59],d=[1,45],p=[1,49],y=[1,33],g=[1,34],m=[1,35],v=[1,36],b=[1,37],_=[1,53],x=[1,60],w=[1,48],k=[1,50],T=[1,52],E=[1,56],C=[1,57],S=[1,38],A=[1,39],M=[1,40],N=[1,41],D=[1,58],O=[1,47],B=[1,51],L=[1,54],I=[1,55],R=[1,46],F=[1,63],P=[1,68],j=[1,20,21,22,23,38,42,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],Y=[1,72],z=[1,71],U=[1,73],q=[20,21,23,74,75],H=[1,94],$=[1,99],W=[1,102],V=[1,103],G=[1,96],X=[1,101],Z=[1,104],Q=[1,97],K=[1,109],J=[1,108],tt=[1,98],et=[1,100],nt=[1,105],rt=[1,106],it=[1,107],at=[1,110],ot=[20,21,22,23,74,75],st=[20,21,22,23,48,74,75],ct=[20,21,22,23,40,47,48,50,52,54,56,58,59,60,62,64,66,67,69,74,75,84,88,98,99,102,104,105,115,116,117,118,119,120],ut=[20,21,23],lt=[20,21,23,47,59,60,74,75,84,88,98,99,102,104,105,115,116,117,118,119,120],ht=[1,12,20,21,22,23,24,38,42,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],ft=[47,59,60,84,88,98,99,102,104,105,115,116,117,118,119,120],dt=[1,143],pt=[1,151],yt=[1,152],gt=[1,153],mt=[1,154],vt=[1,138],bt=[1,139],_t=[1,135],xt=[1,146],wt=[1,147],kt=[1,148],Tt=[1,149],Et=[1,150],Ct=[1,155],St=[1,156],At=[1,141],Mt=[1,144],Nt=[1,140],Dt=[1,137],Ot=[20,21,22,23,38,42,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],Bt=[1,159],Lt=[20,21,22,23,26,47,59,60,84,98,99,102,104,105,115,116,117,118,119,120],It=[20,21,22,23,24,26,38,40,41,42,47,51,53,55,57,59,60,61,63,65,66,68,70,74,75,79,80,81,82,83,84,85,88,98,99,102,104,105,106,107,115,116,117,118,119,120],Rt=[12,21,22,24],Ft=[22,99],Pt=[1,242],jt=[1,237],Yt=[1,238],zt=[1,246],Ut=[1,243],qt=[1,240],Ht=[1,239],$t=[1,241],Wt=[1,244],Vt=[1,245],Gt=[1,247],Xt=[1,265],Zt=[20,21,23,99],Qt=[20,21,22,23,59,60,79,95,98,99,102,103,104,105,106],Kt={trace:function(){},yy:{},symbols_:{error:2,start:3,mermaidDoc:4,directive:5,openDirective:6,typeDirective:7,closeDirective:8,separator:9,":":10,argDirective:11,open_directive:12,type_directive:13,arg_directive:14,close_directive:15,graphConfig:16,document:17,line:18,statement:19,SEMI:20,NEWLINE:21,SPACE:22,EOF:23,GRAPH:24,NODIR:25,DIR:26,FirstStmtSeperator:27,ending:28,endToken:29,spaceList:30,spaceListNewline:31,verticeStatement:32,styleStatement:33,linkStyleStatement:34,classDefStatement:35,classStatement:36,clickStatement:37,subgraph:38,text:39,SQS:40,SQE:41,end:42,direction:43,link:44,node:45,vertex:46,AMP:47,STYLE_SEPARATOR:48,idString:49,PS:50,PE:51,"(-":52,"-)":53,STADIUMSTART:54,STADIUMEND:55,SUBROUTINESTART:56,SUBROUTINEEND:57,VERTEX_WITH_PROPS_START:58,ALPHA:59,COLON:60,PIPE:61,CYLINDERSTART:62,CYLINDEREND:63,DIAMOND_START:64,DIAMOND_STOP:65,TAGEND:66,TRAPSTART:67,TRAPEND:68,INVTRAPSTART:69,INVTRAPEND:70,linkStatement:71,arrowText:72,TESTSTR:73,START_LINK:74,LINK:75,textToken:76,STR:77,keywords:78,STYLE:79,LINKSTYLE:80,CLASSDEF:81,CLASS:82,CLICK:83,DOWN:84,UP:85,textNoTags:86,textNoTagsToken:87,DEFAULT:88,stylesOpt:89,alphaNum:90,CALLBACKNAME:91,CALLBACKARGS:92,HREF:93,LINK_TARGET:94,HEX:95,numList:96,INTERPOLATE:97,NUM:98,COMMA:99,style:100,styleComponent:101,MINUS:102,UNIT:103,BRKT:104,DOT:105,PCT:106,TAGSTART:107,alphaNumToken:108,idStringToken:109,alphaNumStatement:110,direction_tb:111,direction_bt:112,direction_rl:113,direction_lr:114,PUNCTUATION:115,UNICODE_TEXT:116,PLUS:117,EQUALS:118,MULT:119,UNDERSCORE:120,graphCodeTokens:121,ARROW_CROSS:122,ARROW_POINT:123,ARROW_CIRCLE:124,ARROW_OPEN:125,QUOTE:126,$accept:0,$end:1},terminals_:{2:"error",10:":",12:"open_directive",13:"type_directive",14:"arg_directive",15:"close_directive",20:"SEMI",21:"NEWLINE",22:"SPACE",23:"EOF",24:"GRAPH",25:"NODIR",26:"DIR",38:"subgraph",40:"SQS",41:"SQE",42:"end",47:"AMP",48:"STYLE_SEPARATOR",50:"PS",51:"PE",52:"(-",53:"-)",54:"STADIUMSTART",55:"STADIUMEND",56:"SUBROUTINESTART",57:"SUBROUTINEEND",58:"VERTEX_WITH_PROPS_START",59:"ALPHA",60:"COLON",61:"PIPE",62:"CYLINDERSTART",63:"CYLINDEREND",64:"DIAMOND_START",65:"DIAMOND_STOP",66:"TAGEND",67:"TRAPSTART",68:"TRAPEND",69:"INVTRAPSTART",70:"INVTRAPEND",73:"TESTSTR",74:"START_LINK",75:"LINK",77:"STR",79:"STYLE",80:"LINKSTYLE",81:"CLASSDEF",82:"CLASS",83:"CLICK",84:"DOWN",85:"UP",88:"DEFAULT",91:"CALLBACKNAME",92:"CALLBACKARGS",93:"HREF",94:"LINK_TARGET",95:"HEX",97:"INTERPOLATE",98:"NUM",99:"COMMA",102:"MINUS",103:"UNIT",104:"BRKT",105:"DOT",106:"PCT",107:"TAGSTART",111:"direction_tb",112:"direction_bt",113:"direction_rl",114:"direction_lr",115:"PUNCTUATION",116:"UNICODE_TEXT",117:"PLUS",118:"EQUALS",119:"MULT",120:"UNDERSCORE",122:"ARROW_CROSS",123:"ARROW_POINT",124:"ARROW_CIRCLE",125:"ARROW_OPEN",126:"QUOTE"},productions_:[0,[3,1],[3,2],[5,4],[5,6],[6,1],[7,1],[11,1],[8,1],[4,2],[17,0],[17,2],[18,1],[18,1],[18,1],[18,1],[18,1],[16,2],[16,2],[16,2],[16,3],[28,2],[28,1],[29,1],[29,1],[29,1],[27,1],[27,1],[27,2],[31,2],[31,2],[31,1],[31,1],[30,2],[30,1],[19,2],[19,2],[19,2],[19,2],[19,2],[19,2],[19,9],[19,6],[19,4],[19,1],[9,1],[9,1],[9,1],[32,3],[32,4],[32,2],[32,1],[45,1],[45,5],[45,3],[46,4],[46,6],[46,4],[46,4],[46,4],[46,8],[46,4],[46,4],[46,4],[46,6],[46,4],[46,4],[46,4],[46,4],[46,4],[46,1],[44,2],[44,3],[44,3],[44,1],[44,3],[71,1],[72,3],[39,1],[39,2],[39,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[86,1],[86,2],[35,5],[35,5],[36,5],[37,2],[37,4],[37,3],[37,5],[37,2],[37,4],[37,4],[37,6],[37,2],[37,4],[37,2],[37,4],[37,4],[37,6],[33,5],[33,5],[34,5],[34,5],[34,9],[34,9],[34,7],[34,7],[96,1],[96,3],[89,1],[89,3],[100,1],[100,2],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[76,1],[76,1],[76,1],[76,1],[76,1],[76,1],[87,1],[87,1],[87,1],[87,1],[49,1],[49,2],[90,1],[90,2],[110,1],[110,1],[110,1],[110,1],[43,1],[43,1],[43,1],[43,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 5:r.parseDirective("%%{","open_directive");break;case 6:r.parseDirective(a[s],"type_directive");break;case 7:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 8:r.parseDirective("}%%","close_directive","flowchart");break;case 10:case 36:case 37:case 38:case 39:case 40:this.$=[];break;case 11:a[s]!==[]&&a[s-1].push(a[s]),this.$=a[s-1];break;case 12:case 78:case 80:case 92:case 148:case 150:case 151:case 74:case 146:this.$=a[s];break;case 19:r.setDirection("TB"),this.$="TB";break;case 20:r.setDirection(a[s-1]),this.$=a[s-1];break;case 35:this.$=a[s-1].nodes;break;case 41:this.$=r.addSubGraph(a[s-6],a[s-1],a[s-4]);break;case 42:this.$=r.addSubGraph(a[s-3],a[s-1],a[s-3]);break;case 43:this.$=r.addSubGraph(void 0,a[s-1],void 0);break;case 48:r.addLink(a[s-2].stmt,a[s],a[s-1]),this.$={stmt:a[s],nodes:a[s].concat(a[s-2].nodes)};break;case 49:r.addLink(a[s-3].stmt,a[s-1],a[s-2]),this.$={stmt:a[s-1],nodes:a[s-1].concat(a[s-3].nodes)};break;case 50:this.$={stmt:a[s-1],nodes:a[s-1]};break;case 51:this.$={stmt:a[s],nodes:a[s]};break;case 52:case 119:case 121:this.$=[a[s]];break;case 53:this.$=a[s-4].concat(a[s]);break;case 54:this.$=[a[s-2]],r.setClass(a[s-2],a[s]);break;case 55:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"square");break;case 56:this.$=a[s-5],r.addVertex(a[s-5],a[s-2],"circle");break;case 57:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"ellipse");break;case 58:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"stadium");break;case 59:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"subroutine");break;case 60:this.$=a[s-7],r.addVertex(a[s-7],a[s-1],"rect",void 0,void 0,void 0,Object.fromEntries([[a[s-5],a[s-3]]]));break;case 61:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"cylinder");break;case 62:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"round");break;case 63:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"diamond");break;case 64:this.$=a[s-5],r.addVertex(a[s-5],a[s-2],"hexagon");break;case 65:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"odd");break;case 66:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"trapezoid");break;case 67:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"inv_trapezoid");break;case 68:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"lean_right");break;case 69:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"lean_left");break;case 70:this.$=a[s],r.addVertex(a[s]);break;case 71:a[s-1].text=a[s],this.$=a[s-1];break;case 72:case 73:a[s-2].text=a[s-1],this.$=a[s-2];break;case 75:var c=r.destructLink(a[s],a[s-2]);this.$={type:c.type,stroke:c.stroke,length:c.length,text:a[s-1]};break;case 76:c=r.destructLink(a[s]),this.$={type:c.type,stroke:c.stroke,length:c.length};break;case 77:this.$=a[s-1];break;case 79:case 93:case 149:case 147:this.$=a[s-1]+""+a[s];break;case 94:case 95:this.$=a[s-4],r.addClass(a[s-2],a[s]);break;case 96:this.$=a[s-4],r.setClass(a[s-2],a[s]);break;case 97:case 105:this.$=a[s-1],r.setClickEvent(a[s-1],a[s]);break;case 98:case 106:this.$=a[s-3],r.setClickEvent(a[s-3],a[s-2]),r.setTooltip(a[s-3],a[s]);break;case 99:this.$=a[s-2],r.setClickEvent(a[s-2],a[s-1],a[s]);break;case 100:this.$=a[s-4],r.setClickEvent(a[s-4],a[s-3],a[s-2]),r.setTooltip(a[s-4],a[s]);break;case 101:case 107:this.$=a[s-1],r.setLink(a[s-1],a[s]);break;case 102:case 108:this.$=a[s-3],r.setLink(a[s-3],a[s-2]),r.setTooltip(a[s-3],a[s]);break;case 103:case 109:this.$=a[s-3],r.setLink(a[s-3],a[s-2],a[s]);break;case 104:case 110:this.$=a[s-5],r.setLink(a[s-5],a[s-4],a[s]),r.setTooltip(a[s-5],a[s-2]);break;case 111:this.$=a[s-4],r.addVertex(a[s-2],void 0,void 0,a[s]);break;case 112:case 114:this.$=a[s-4],r.updateLink(a[s-2],a[s]);break;case 113:this.$=a[s-4],r.updateLink([a[s-2]],a[s]);break;case 115:this.$=a[s-8],r.updateLinkInterpolate([a[s-6]],a[s-2]),r.updateLink([a[s-6]],a[s]);break;case 116:this.$=a[s-8],r.updateLinkInterpolate(a[s-6],a[s-2]),r.updateLink(a[s-6],a[s]);break;case 117:this.$=a[s-6],r.updateLinkInterpolate([a[s-4]],a[s]);break;case 118:this.$=a[s-6],r.updateLinkInterpolate(a[s-4],a[s]);break;case 120:case 122:a[s-2].push(a[s]),this.$=a[s-2];break;case 124:this.$=a[s-1]+a[s];break;case 152:this.$="v";break;case 153:this.$="-";break;case 154:this.$={stmt:"dir",value:"TB"};break;case 155:this.$={stmt:"dir",value:"BT"};break;case 156:this.$={stmt:"dir",value:"RL"};break;case 157:this.$={stmt:"dir",value:"LR"}}},table:[{3:1,4:2,5:3,6:5,12:e,16:4,21:n,22:r,24:i},{1:[3]},{1:[2,1]},{3:10,4:2,5:3,6:5,12:e,16:4,21:n,22:r,24:i},t(a,o,{17:11}),{7:12,13:[1,13]},{16:14,21:n,22:r,24:i},{16:15,21:n,22:r,24:i},{25:[1,16],26:[1,17]},{13:[2,5]},{1:[2,2]},{1:[2,9],18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},{8:61,10:[1,62],15:F},t([10,15],[2,6]),t(a,[2,17]),t(a,[2,18]),t(a,[2,19]),{20:[1,65],21:[1,66],22:P,27:64,30:67},t(j,[2,11]),t(j,[2,12]),t(j,[2,13]),t(j,[2,14]),t(j,[2,15]),t(j,[2,16]),{9:69,20:Y,21:z,23:U,44:70,71:74,74:[1,75],75:[1,76]},{9:77,20:Y,21:z,23:U},{9:78,20:Y,21:z,23:U},{9:79,20:Y,21:z,23:U},{9:80,20:Y,21:z,23:U},{9:81,20:Y,21:z,23:U},{9:83,20:Y,21:z,22:[1,82],23:U},t(j,[2,44]),t(q,[2,51],{30:84,22:P}),{22:[1,85]},{22:[1,86]},{22:[1,87]},{22:[1,88]},{26:H,47:$,59:W,60:V,77:[1,92],84:G,90:91,91:[1,89],93:[1,90],98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(j,[2,154]),t(j,[2,155]),t(j,[2,156]),t(j,[2,157]),t(ot,[2,52],{48:[1,111]}),t(st,[2,70],{109:123,40:[1,112],47:f,50:[1,113],52:[1,114],54:[1,115],56:[1,116],58:[1,117],59:d,60:p,62:[1,118],64:[1,119],66:[1,120],67:[1,121],69:[1,122],84:_,88:x,98:w,99:k,102:T,104:E,105:C,115:D,116:O,117:B,118:L,119:I,120:R}),t(ct,[2,146]),t(ct,[2,171]),t(ct,[2,172]),t(ct,[2,173]),t(ct,[2,174]),t(ct,[2,175]),t(ct,[2,176]),t(ct,[2,177]),t(ct,[2,178]),t(ct,[2,179]),t(ct,[2,180]),t(ct,[2,181]),t(ct,[2,182]),t(ct,[2,183]),t(ct,[2,184]),t(ct,[2,185]),t(ct,[2,186]),{9:124,20:Y,21:z,23:U},{11:125,14:[1,126]},t(ut,[2,8]),t(a,[2,20]),t(a,[2,26]),t(a,[2,27]),{21:[1,127]},t(lt,[2,34],{30:128,22:P}),t(j,[2,35]),{45:129,46:42,47:f,49:43,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,115:D,116:O,117:B,118:L,119:I,120:R},t(ht,[2,45]),t(ht,[2,46]),t(ht,[2,47]),t(ft,[2,74],{72:130,61:[1,132],73:[1,131]}),{22:dt,24:pt,26:yt,38:gt,39:133,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t([47,59,60,61,73,84,88,98,99,102,104,105,115,116,117,118,119,120],[2,76]),t(j,[2,36]),t(j,[2,37]),t(j,[2,38]),t(j,[2,39]),t(j,[2,40]),{22:dt,24:pt,26:yt,38:gt,39:157,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(Ot,o,{17:158}),t(q,[2,50],{47:Bt}),{26:H,47:$,59:W,60:V,84:G,90:160,95:[1,161],98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},{88:[1,162],96:163,98:[1,164]},{26:H,47:$,59:W,60:V,84:G,88:[1,165],90:166,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},{26:H,47:$,59:W,60:V,84:G,90:167,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ut,[2,97],{22:[1,168],92:[1,169]}),t(ut,[2,101],{22:[1,170]}),t(ut,[2,105],{108:95,110:172,22:[1,171],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),t(ut,[2,107],{22:[1,173]}),t(Lt,[2,148]),t(Lt,[2,150]),t(Lt,[2,151]),t(Lt,[2,152]),t(Lt,[2,153]),t(It,[2,158]),t(It,[2,159]),t(It,[2,160]),t(It,[2,161]),t(It,[2,162]),t(It,[2,163]),t(It,[2,164]),t(It,[2,165]),t(It,[2,166]),t(It,[2,167]),t(It,[2,168]),t(It,[2,169]),t(It,[2,170]),{47:f,49:174,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,115:D,116:O,117:B,118:L,119:I,120:R},{22:dt,24:pt,26:yt,38:gt,39:175,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:177,42:mt,47:$,50:[1,176],59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:178,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:179,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:180,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{59:[1,181]},{22:dt,24:pt,26:yt,38:gt,39:182,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:183,42:mt,47:$,59:W,60:V,64:[1,184],66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:185,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:186,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:187,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ct,[2,147]),t(Rt,[2,3]),{8:188,15:F},{15:[2,7]},t(a,[2,28]),t(lt,[2,33]),t(q,[2,48],{30:189,22:P}),t(ft,[2,71],{22:[1,190]}),{22:[1,191]},{22:dt,24:pt,26:yt,38:gt,39:192,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,66:vt,74:bt,75:[1,193],76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(It,[2,78]),t(It,[2,80]),t(It,[2,136]),t(It,[2,137]),t(It,[2,138]),t(It,[2,139]),t(It,[2,140]),t(It,[2,141]),t(It,[2,142]),t(It,[2,143]),t(It,[2,144]),t(It,[2,145]),t(It,[2,81]),t(It,[2,82]),t(It,[2,83]),t(It,[2,84]),t(It,[2,85]),t(It,[2,86]),t(It,[2,87]),t(It,[2,88]),t(It,[2,89]),t(It,[2,90]),t(It,[2,91]),{9:196,20:Y,21:z,22:dt,23:U,24:pt,26:yt,38:gt,40:[1,195],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,42:[1,197],43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},{22:P,30:198},{22:[1,199],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,108:95,110:172,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:[1,200]},{22:[1,201]},{22:[1,202],99:[1,203]},t(Ft,[2,119]),{22:[1,204]},{22:[1,205],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,108:95,110:172,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:[1,206],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,108:95,110:172,115:tt,116:et,117:nt,118:rt,119:it,120:at},{77:[1,207]},t(ut,[2,99],{22:[1,208]}),{77:[1,209],94:[1,210]},{77:[1,211]},t(Lt,[2,149]),{77:[1,212],94:[1,213]},t(ot,[2,54],{109:123,47:f,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,115:D,116:O,117:B,118:L,119:I,120:R}),{22:dt,24:pt,26:yt,38:gt,41:[1,214],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:215,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,51:[1,216],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,53:[1,217],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,55:[1,218],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,57:[1,219],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{60:[1,220]},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,63:[1,221],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,65:[1,222],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:223,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,41:[1,224],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,66:vt,68:[1,225],70:[1,226],74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,66:vt,68:[1,228],70:[1,227],74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{9:229,20:Y,21:z,23:U},t(q,[2,49],{47:Bt}),t(ft,[2,73]),t(ft,[2,72]),{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,61:[1,230],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ft,[2,75]),t(It,[2,79]),{22:dt,24:pt,26:yt,38:gt,39:231,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(Ot,o,{17:232}),t(j,[2,43]),{46:233,47:f,49:43,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,115:D,116:O,117:B,118:L,119:I,120:R},{22:Pt,59:jt,60:Yt,79:zt,89:234,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:248,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:249,95:Ut,97:[1,250],98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:251,95:Ut,97:[1,252],98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{98:[1,253]},{22:Pt,59:jt,60:Yt,79:zt,89:254,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:255,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{26:H,47:$,59:W,60:V,84:G,90:256,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ut,[2,98]),{77:[1,257]},t(ut,[2,102],{22:[1,258]}),t(ut,[2,103]),t(ut,[2,106]),t(ut,[2,108],{22:[1,259]}),t(ut,[2,109]),t(st,[2,55]),{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,51:[1,260],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(st,[2,62]),t(st,[2,57]),t(st,[2,58]),t(st,[2,59]),{59:[1,261]},t(st,[2,61]),t(st,[2,63]),{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,65:[1,262],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(st,[2,65]),t(st,[2,66]),t(st,[2,68]),t(st,[2,67]),t(st,[2,69]),t(Rt,[2,4]),t([22,47,59,60,84,88,98,99,102,104,105,115,116,117,118,119,120],[2,77]),{22:dt,24:pt,26:yt,38:gt,41:[1,263],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,42:[1,264],43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},t(ot,[2,53]),t(ut,[2,111],{99:Xt}),t(Zt,[2,121],{101:266,22:Pt,59:jt,60:Yt,79:zt,95:Ut,98:qt,102:Ht,103:$t,104:Wt,105:Vt,106:Gt}),t(Qt,[2,123]),t(Qt,[2,125]),t(Qt,[2,126]),t(Qt,[2,127]),t(Qt,[2,128]),t(Qt,[2,129]),t(Qt,[2,130]),t(Qt,[2,131]),t(Qt,[2,132]),t(Qt,[2,133]),t(Qt,[2,134]),t(Qt,[2,135]),t(ut,[2,112],{99:Xt}),t(ut,[2,113],{99:Xt}),{22:[1,267]},t(ut,[2,114],{99:Xt}),{22:[1,268]},t(Ft,[2,120]),t(ut,[2,94],{99:Xt}),t(ut,[2,95],{99:Xt}),t(ut,[2,96],{108:95,110:172,26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),t(ut,[2,100]),{94:[1,269]},{94:[1,270]},{51:[1,271]},{61:[1,272]},{65:[1,273]},{9:274,20:Y,21:z,23:U},t(j,[2,42]),{22:Pt,59:jt,60:Yt,79:zt,95:Ut,98:qt,100:275,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},t(Qt,[2,124]),{26:H,47:$,59:W,60:V,84:G,90:276,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},{26:H,47:$,59:W,60:V,84:G,90:277,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ut,[2,104]),t(ut,[2,110]),t(st,[2,56]),{22:dt,24:pt,26:yt,38:gt,39:278,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(st,[2,64]),t(Ot,o,{17:279}),t(Zt,[2,122],{101:266,22:Pt,59:jt,60:Yt,79:zt,95:Ut,98:qt,102:Ht,103:$t,104:Wt,105:Vt,106:Gt}),t(ut,[2,117],{108:95,110:172,22:[1,280],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),t(ut,[2,118],{108:95,110:172,22:[1,281],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),{22:dt,24:pt,26:yt,38:gt,41:[1,282],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,42:[1,283],43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},{22:Pt,59:jt,60:Yt,79:zt,89:284,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:285,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},t(st,[2,60]),t(j,[2,41]),t(ut,[2,115],{99:Xt}),t(ut,[2,116],{99:Xt})],defaultActions:{2:[2,1],9:[2,5],10:[2,2],126:[2,7]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},Jt={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),12;case 1:return this.begin("type_directive"),13;case 2:return this.popState(),this.begin("arg_directive"),10;case 3:return this.popState(),this.popState(),15;case 4:return 14;case 5:case 6:break;case 7:this.begin("string");break;case 8:case 17:case 20:case 23:case 26:this.popState();break;case 9:return"STR";case 10:return 79;case 11:return 88;case 12:return 80;case 13:return 97;case 14:return 81;case 15:return 82;case 16:this.begin("href");break;case 18:return 93;case 19:this.begin("callbackname");break;case 21:this.popState(),this.begin("callbackargs");break;case 22:return 91;case 24:return 92;case 25:this.begin("click");break;case 27:return 83;case 28:case 29:return t.lex.firstGraph()&&this.begin("dir"),24;case 30:return 38;case 31:return 42;case 32:case 33:case 34:case 35:return 94;case 36:return this.popState(),25;case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:return this.popState(),26;case 47:return 111;case 48:return 112;case 49:return 113;case 50:return 114;case 51:return 98;case 52:return 104;case 53:return 48;case 54:return 60;case 55:return 47;case 56:return 20;case 57:return 99;case 58:return 119;case 59:case 60:case 61:return 75;case 62:case 63:case 64:return 74;case 65:return 52;case 66:return 53;case 67:return 54;case 68:return 55;case 69:return 56;case 70:return 57;case 71:return 58;case 72:return 62;case 73:return 63;case 74:return 102;case 75:return 105;case 76:return 120;case 77:return 117;case 78:return 106;case 79:case 80:return 118;case 81:return 107;case 82:return 66;case 83:return 85;case 84:return"SEP";case 85:return 84;case 86:return 59;case 87:return 68;case 88:return 67;case 89:return 70;case 90:return 69;case 91:return 115;case 92:return 116;case 93:return 61;case 94:return 50;case 95:return 51;case 96:return 40;case 97:return 41;case 98:return 64;case 99:return 65;case 100:return 126;case 101:return 21;case 102:return 22;case 103:return 23}},rules:[/^(?:%%\{)/,/^(?:((?:(?!\}%%)[^:.])*))/,/^(?::)/,/^(?:\}%%)/,/^(?:((?:(?!\}%%).|\n)*))/,/^(?:%%(?!\{)[^\n]*)/,/^(?:[^\}]%%[^\n]*)/,/^(?:["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:style\b)/,/^(?:default\b)/,/^(?:linkStyle\b)/,/^(?:interpolate\b)/,/^(?:classDef\b)/,/^(?:class\b)/,/^(?:href[\s]+["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:click[\s]+)/,/^(?:[\s\n])/,/^(?:[^\s\n]*)/,/^(?:graph\b)/,/^(?:flowchart\b)/,/^(?:subgraph\b)/,/^(?:end\b\s*)/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:(\r?\n)*\s*\n)/,/^(?:\s*LR\b)/,/^(?:\s*RL\b)/,/^(?:\s*TB\b)/,/^(?:\s*BT\b)/,/^(?:\s*TD\b)/,/^(?:\s*BR\b)/,/^(?:\s*<)/,/^(?:\s*>)/,/^(?:\s*\^)/,/^(?:\s*v\b)/,/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:[0-9]+)/,/^(?:#)/,/^(?::::)/,/^(?::)/,/^(?:&)/,/^(?:;)/,/^(?:,)/,/^(?:\*)/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?:\s*[xo<]?--\s*)/,/^(?:\s*[xo<]?==\s*)/,/^(?:\s*[xo<]?-\.\s*)/,/^(?:\(-)/,/^(?:-\))/,/^(?:\(\[)/,/^(?:\]\))/,/^(?:\[\[)/,/^(?:\]\])/,/^(?:\[\|)/,/^(?:\[\()/,/^(?:\)\])/,/^(?:-)/,/^(?:\.)/,/^(?:[\_])/,/^(?:\+)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:<)/,/^(?:>)/,/^(?:\^)/,/^(?:\\\|)/,/^(?:v\b)/,/^(?:[A-Za-z]+)/,/^(?:\\\])/,/^(?:\[\/)/,/^(?:\/\])/,/^(?:\[\\)/,/^(?:[!"#$%&'*+,-.`?\\_/])/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\|)/,/^(?:\()/,/^(?:\))/,/^(?:\[)/,/^(?:\])/,/^(?:\{)/,/^(?:\})/,/^(?:")/,/^(?:(\r?\n)+)/,/^(?:\s)/,/^(?:$)/],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},callbackargs:{rules:[23,24],inclusive:!1},callbackname:{rules:[20,21,22],inclusive:!1},href:{rules:[17,18],inclusive:!1},click:{rules:[26,27],inclusive:!1},vertex:{rules:[],inclusive:!1},dir:{rules:[36,37,38,39,40,41,42,43,44,45,46],inclusive:!1},string:{rules:[8,9],inclusive:!1},INITIAL:{rules:[0,5,6,7,10,11,12,13,14,15,16,19,25,28,29,30,31,32,33,34,35,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103],inclusive:!0}}};function te(){this.yy={}}return Kt.lexer=Jt,te.prototype=Kt,Kt.Parser=te,new te}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(5354).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},9959:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,3],n=[1,5],r=[7,9,11,12,13,14,15,16,17,18,19,20,22,29,34],i=[1,15],a=[1,16],o=[1,17],s=[1,18],c=[1,19],u=[1,20],l=[1,21],h=[1,22],f=[1,23],d=[1,25],p=[1,27],y=[1,30],g=[5,7,9,11,12,13,14,15,16,17,18,19,20,22,29,34],m={trace:function(){},yy:{},symbols_:{error:2,start:3,directive:4,gantt:5,document:6,EOF:7,line:8,SPACE:9,statement:10,NL:11,dateFormat:12,inclusiveEndDates:13,topAxis:14,axisFormat:15,excludes:16,includes:17,todayMarker:18,title:19,section:20,clickStatement:21,taskTxt:22,taskData:23,openDirective:24,typeDirective:25,closeDirective:26,":":27,argDirective:28,click:29,callbackname:30,callbackargs:31,href:32,clickStatementDebug:33,open_directive:34,type_directive:35,arg_directive:36,close_directive:37,$accept:0,$end:1},terminals_:{2:"error",5:"gantt",7:"EOF",9:"SPACE",11:"NL",12:"dateFormat",13:"inclusiveEndDates",14:"topAxis",15:"axisFormat",16:"excludes",17:"includes",18:"todayMarker",19:"title",20:"section",22:"taskTxt",23:"taskData",27:":",29:"click",30:"callbackname",31:"callbackargs",32:"href",34:"open_directive",35:"type_directive",36:"arg_directive",37:"close_directive"},productions_:[0,[3,2],[3,3],[6,0],[6,2],[8,2],[8,1],[8,1],[8,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,2],[10,1],[4,4],[4,6],[21,2],[21,3],[21,3],[21,4],[21,3],[21,4],[21,2],[33,2],[33,3],[33,3],[33,4],[33,3],[33,4],[33,2],[24,1],[25,1],[28,1],[26,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 2:return a[s-1];case 3:case 7:case 8:this.$=[];break;case 4:a[s-1].push(a[s]),this.$=a[s-1];break;case 5:case 6:this.$=a[s];break;case 9:r.setDateFormat(a[s].substr(11)),this.$=a[s].substr(11);break;case 10:r.enableInclusiveEndDates(),this.$=a[s].substr(18);break;case 11:r.TopAxis(),this.$=a[s].substr(8);break;case 12:r.setAxisFormat(a[s].substr(11)),this.$=a[s].substr(11);break;case 13:r.setExcludes(a[s].substr(9)),this.$=a[s].substr(9);break;case 14:r.setIncludes(a[s].substr(9)),this.$=a[s].substr(9);break;case 15:r.setTodayMarker(a[s].substr(12)),this.$=a[s].substr(12);break;case 16:r.setTitle(a[s].substr(6)),this.$=a[s].substr(6);break;case 17:r.addSection(a[s].substr(8)),this.$=a[s].substr(8);break;case 19:r.addTask(a[s-1],a[s]),this.$="task";break;case 23:this.$=a[s-1],r.setClickEvent(a[s-1],a[s],null);break;case 24:this.$=a[s-2],r.setClickEvent(a[s-2],a[s-1],a[s]);break;case 25:this.$=a[s-2],r.setClickEvent(a[s-2],a[s-1],null),r.setLink(a[s-2],a[s]);break;case 26:this.$=a[s-3],r.setClickEvent(a[s-3],a[s-2],a[s-1]),r.setLink(a[s-3],a[s]);break;case 27:this.$=a[s-2],r.setClickEvent(a[s-2],a[s],null),r.setLink(a[s-2],a[s-1]);break;case 28:this.$=a[s-3],r.setClickEvent(a[s-3],a[s-1],a[s]),r.setLink(a[s-3],a[s-2]);break;case 29:this.$=a[s-1],r.setLink(a[s-1],a[s]);break;case 30:case 36:this.$=a[s-1]+" "+a[s];break;case 31:case 32:case 34:this.$=a[s-2]+" "+a[s-1]+" "+a[s];break;case 33:case 35:this.$=a[s-3]+" "+a[s-2]+" "+a[s-1]+" "+a[s];break;case 37:r.parseDirective("%%{","open_directive");break;case 38:r.parseDirective(a[s],"type_directive");break;case 39:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 40:r.parseDirective("}%%","close_directive","gantt")}},table:[{3:1,4:2,5:e,24:4,34:n},{1:[3]},{3:6,4:2,5:e,24:4,34:n},t(r,[2,3],{6:7}),{25:8,35:[1,9]},{35:[2,37]},{1:[2,1]},{4:26,7:[1,10],8:11,9:[1,12],10:13,11:[1,14],12:i,13:a,14:o,15:s,16:c,17:u,18:l,19:h,20:f,21:24,22:d,24:4,29:p,34:n},{26:28,27:[1,29],37:y},t([27,37],[2,38]),t(r,[2,8],{1:[2,2]}),t(r,[2,4]),{4:26,10:31,12:i,13:a,14:o,15:s,16:c,17:u,18:l,19:h,20:f,21:24,22:d,24:4,29:p,34:n},t(r,[2,6]),t(r,[2,7]),t(r,[2,9]),t(r,[2,10]),t(r,[2,11]),t(r,[2,12]),t(r,[2,13]),t(r,[2,14]),t(r,[2,15]),t(r,[2,16]),t(r,[2,17]),t(r,[2,18]),{23:[1,32]},t(r,[2,20]),{30:[1,33],32:[1,34]},{11:[1,35]},{28:36,36:[1,37]},{11:[2,40]},t(r,[2,5]),t(r,[2,19]),t(r,[2,23],{31:[1,38],32:[1,39]}),t(r,[2,29],{30:[1,40]}),t(g,[2,21]),{26:41,37:y},{37:[2,39]},t(r,[2,24],{32:[1,42]}),t(r,[2,25]),t(r,[2,27],{31:[1,43]}),{11:[1,44]},t(r,[2,26]),t(r,[2,28]),t(g,[2,22])],defaultActions:{5:[2,37],6:[2,1],30:[2,40],37:[2,39]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},v={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),34;case 1:return this.begin("type_directive"),35;case 2:return this.popState(),this.begin("arg_directive"),27;case 3:return this.popState(),this.popState(),37;case 4:return 36;case 5:case 6:case 7:case 9:case 10:case 11:break;case 8:return 11;case 12:this.begin("href");break;case 13:case 16:case 19:case 22:this.popState();break;case 14:return 32;case 15:this.begin("callbackname");break;case 17:this.popState(),this.begin("callbackargs");break;case 18:return 30;case 20:return 31;case 21:this.begin("click");break;case 23:return 29;case 24:return 5;case 25:return 12;case 26:return 13;case 27:return 14;case 28:return 15;case 29:return 17;case 30:return 16;case 31:return 18;case 32:return"date";case 33:return 19;case 34:return 20;case 35:return 22;case 36:return 23;case 37:return 27;case 38:return 7;case 39:return"INVALID"}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%%(?!\{)*[^\n]*)/i,/^(?:[^\}]%%*[^\n]*)/i,/^(?:%%*[^\n]*[\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:href[\s]+["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:call[\s]+)/i,/^(?:\([\s]*\))/i,/^(?:\()/i,/^(?:[^(]*)/i,/^(?:\))/i,/^(?:[^)]*)/i,/^(?:click[\s]+)/i,/^(?:[\s\n])/i,/^(?:[^\s\n]*)/i,/^(?:gantt\b)/i,/^(?:dateFormat\s[^#\n;]+)/i,/^(?:inclusiveEndDates\b)/i,/^(?:topAxis\b)/i,/^(?:axisFormat\s[^#\n;]+)/i,/^(?:includes\s[^#\n;]+)/i,/^(?:excludes\s[^#\n;]+)/i,/^(?:todayMarker\s[^\n;]+)/i,/^(?:\d\d\d\d-\d\d-\d\d\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:section\s[^#:\n;]+)/i,/^(?:[^#:\n;]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},callbackargs:{rules:[19,20],inclusive:!1},callbackname:{rules:[16,17,18],inclusive:!1},href:{rules:[13,14],inclusive:!1},click:{rules:[22,23],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,15,21,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],inclusive:!0}}};function b(){this.yy={}}return m.lexer=v,b.prototype=m,m.Parser=b,new b}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(6878).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},2553:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[2,3],n=[1,7],r=[7,12,15,17,19,20,21],i=[7,11,12,15,17,19,20,21],a=[2,20],o=[1,32],s={trace:function(){},yy:{},symbols_:{error:2,start:3,GG:4,":":5,document:6,EOF:7,DIR:8,options:9,body:10,OPT:11,NL:12,line:13,statement:14,COMMIT:15,commit_arg:16,BRANCH:17,ID:18,CHECKOUT:19,MERGE:20,RESET:21,reset_arg:22,STR:23,HEAD:24,reset_parents:25,CARET:26,$accept:0,$end:1},terminals_:{2:"error",4:"GG",5:":",7:"EOF",8:"DIR",11:"OPT",12:"NL",15:"COMMIT",17:"BRANCH",18:"ID",19:"CHECKOUT",20:"MERGE",21:"RESET",23:"STR",24:"HEAD",26:"CARET"},productions_:[0,[3,4],[3,5],[6,0],[6,2],[9,2],[9,1],[10,0],[10,2],[13,2],[13,1],[14,2],[14,2],[14,2],[14,2],[14,2],[16,0],[16,1],[22,2],[22,2],[25,0],[25,2]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 1:return a[s-1];case 2:return r.setDirection(a[s-3]),a[s-1];case 4:r.setOptions(a[s-1]),this.$=a[s];break;case 5:a[s-1]+=a[s],this.$=a[s-1];break;case 7:this.$=[];break;case 8:a[s-1].push(a[s]),this.$=a[s-1];break;case 9:this.$=a[s-1];break;case 11:r.commit(a[s]);break;case 12:r.branch(a[s]);break;case 13:r.checkout(a[s]);break;case 14:r.merge(a[s]);break;case 15:r.reset(a[s]);break;case 16:this.$="";break;case 17:this.$=a[s];break;case 18:this.$=a[s-1]+":"+a[s];break;case 19:this.$=a[s-1]+":"+r.count,r.count=0;break;case 20:r.count=0;break;case 21:r.count+=1}},table:[{3:1,4:[1,2]},{1:[3]},{5:[1,3],8:[1,4]},{6:5,7:e,9:6,12:n},{5:[1,8]},{7:[1,9]},t(r,[2,7],{10:10,11:[1,11]}),t(i,[2,6]),{6:12,7:e,9:6,12:n},{1:[2,1]},{7:[2,4],12:[1,15],13:13,14:14,15:[1,16],17:[1,17],19:[1,18],20:[1,19],21:[1,20]},t(i,[2,5]),{7:[1,21]},t(r,[2,8]),{12:[1,22]},t(r,[2,10]),{12:[2,16],16:23,23:[1,24]},{18:[1,25]},{18:[1,26]},{18:[1,27]},{18:[1,30],22:28,24:[1,29]},{1:[2,2]},t(r,[2,9]),{12:[2,11]},{12:[2,17]},{12:[2,12]},{12:[2,13]},{12:[2,14]},{12:[2,15]},{12:a,25:31,26:o},{12:a,25:33,26:o},{12:[2,18]},{12:a,25:34,26:o},{12:[2,19]},{12:[2,21]}],defaultActions:{9:[2,1],21:[2,2],23:[2,11],24:[2,17],25:[2,12],26:[2,13],27:[2,14],28:[2,15],31:[2,18],33:[2,19],34:[2,21]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},c={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return 12;case 1:case 2:case 3:break;case 4:return 4;case 5:return 15;case 6:return 17;case 7:return 20;case 8:return 21;case 9:return 19;case 10:case 11:return 8;case 12:return 5;case 13:return 26;case 14:this.begin("options");break;case 15:case 18:this.popState();break;case 16:return 11;case 17:this.begin("string");break;case 19:return 23;case 20:return 18;case 21:return 7}},rules:[/^(?:(\r?\n)+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:gitGraph\b)/i,/^(?:commit\b)/i,/^(?:branch\b)/i,/^(?:merge\b)/i,/^(?:reset\b)/i,/^(?:checkout\b)/i,/^(?:LR\b)/i,/^(?:BT\b)/i,/^(?::)/i,/^(?:\^)/i,/^(?:options\r?\n)/i,/^(?:end\r?\n)/i,/^(?:[^\n]+\r?\n)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[a-zA-Z][-_\.a-zA-Z0-9]*[-_a-zA-Z0-9])/i,/^(?:$)/i],conditions:{options:{rules:[15,16],inclusive:!1},string:{rules:[18,19],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,17,20,21],inclusive:!0}}};function u(){this.yy={}}return s.lexer=c,u.prototype=s,s.Parser=u,new u}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8183).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},6765:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[6,9,10],n={trace:function(){},yy:{},symbols_:{error:2,start:3,info:4,document:5,EOF:6,line:7,statement:8,NL:9,showInfo:10,$accept:0,$end:1},terminals_:{2:"error",4:"info",6:"EOF",9:"NL",10:"showInfo"},productions_:[0,[3,3],[5,0],[5,2],[7,1],[7,1],[8,1]],performAction:function(t,e,n,r,i,a,o){switch(a.length,i){case 1:return r;case 4:break;case 6:r.setInfo(!0)}},table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:6,9:[1,7],10:[1,8]},{1:[2,1]},t(e,[2,3]),t(e,[2,4]),t(e,[2,5]),t(e,[2,6])],defaultActions:{4:[2,1]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},r={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return 4;case 1:return 9;case 2:return"space";case 3:return 10;case 4:return 6;case 5:return"TXT"}},rules:[/^(?:info\b)/i,/^(?:[\s\n\r]+)/i,/^(?:[\s]+)/i,/^(?:showInfo\b)/i,/^(?:$)/i,/^(?:.)/i],conditions:{INITIAL:{rules:[0,1,2,3,4,5],inclusive:!0}}};function i(){this.yy={}}return n.lexer=r,i.prototype=n,n.Parser=i,new i}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(1428).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},7062:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,4],n=[1,5],r=[1,6],i=[1,7],a=[1,9],o=[1,11,13,20,21,22,23],s=[2,5],c=[1,6,11,13,20,21,22,23],u=[20,21,22],l=[2,8],h=[1,18],f=[1,19],d=[1,24],p=[6,20,21,22,23],y={trace:function(){},yy:{},symbols_:{error:2,start:3,eol:4,directive:5,PIE:6,document:7,showData:8,line:9,statement:10,txt:11,value:12,title:13,title_value:14,openDirective:15,typeDirective:16,closeDirective:17,":":18,argDirective:19,NEWLINE:20,";":21,EOF:22,open_directive:23,type_directive:24,arg_directive:25,close_directive:26,$accept:0,$end:1},terminals_:{2:"error",6:"PIE",8:"showData",11:"txt",12:"value",13:"title",14:"title_value",18:":",20:"NEWLINE",21:";",22:"EOF",23:"open_directive",24:"type_directive",25:"arg_directive",26:"close_directive"},productions_:[0,[3,2],[3,2],[3,2],[3,3],[7,0],[7,2],[9,2],[10,0],[10,2],[10,2],[10,1],[5,3],[5,5],[4,1],[4,1],[4,1],[15,1],[16,1],[19,1],[17,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:r.setShowData(!0);break;case 7:this.$=a[s-1];break;case 9:r.addSection(a[s-1],r.cleanupValue(a[s]));break;case 10:this.$=a[s].trim(),r.setTitle(this.$);break;case 17:r.parseDirective("%%{","open_directive");break;case 18:r.parseDirective(a[s],"type_directive");break;case 19:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 20:r.parseDirective("}%%","close_directive","pie")}},table:[{3:1,4:2,5:3,6:e,15:8,20:n,21:r,22:i,23:a},{1:[3]},{3:10,4:2,5:3,6:e,15:8,20:n,21:r,22:i,23:a},{3:11,4:2,5:3,6:e,15:8,20:n,21:r,22:i,23:a},t(o,s,{7:12,8:[1,13]}),t(c,[2,14]),t(c,[2,15]),t(c,[2,16]),{16:14,24:[1,15]},{24:[2,17]},{1:[2,1]},{1:[2,2]},t(u,l,{15:8,9:16,10:17,5:20,1:[2,3],11:h,13:f,23:a}),t(o,s,{7:21}),{17:22,18:[1,23],26:d},t([18,26],[2,18]),t(o,[2,6]),{4:25,20:n,21:r,22:i},{12:[1,26]},{14:[1,27]},t(u,[2,11]),t(u,l,{15:8,9:16,10:17,5:20,1:[2,4],11:h,13:f,23:a}),t(p,[2,12]),{19:28,25:[1,29]},t(p,[2,20]),t(o,[2,7]),t(u,[2,9]),t(u,[2,10]),{17:30,26:d},{26:[2,19]},t(p,[2,13])],defaultActions:{9:[2,17],10:[2,1],11:[2,2],29:[2,19]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},g={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),23;case 1:return this.begin("type_directive"),24;case 2:return this.popState(),this.begin("arg_directive"),18;case 3:return this.popState(),this.popState(),26;case 4:return 25;case 5:case 6:case 8:case 9:break;case 7:return 20;case 10:return this.begin("title"),13;case 11:return this.popState(),"title_value";case 12:this.begin("string");break;case 13:this.popState();break;case 14:return"txt";case 15:return 6;case 16:return 8;case 17:return"value";case 18:return 22}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:[\s]+)/i,/^(?:title\b)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:pie\b)/i,/^(?:showData\b)/i,/^(?::[\s]*[\d]+(?:\.[\d]+)?)/i,/^(?:$)/i],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},title:{rules:[11],inclusive:!1},string:{rules:[13,14],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,12,15,16,17,18],inclusive:!0}}};function m(){this.yy={}}return y.lexer=g,m.prototype=y,y.Parser=m,new m}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(4551).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},3176:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,3],n=[1,5],r=[1,17],i=[2,10],a=[1,21],o=[1,22],s=[1,23],c=[1,24],u=[1,25],l=[1,26],h=[1,19],f=[1,27],d=[1,28],p=[1,31],y=[66,67],g=[5,8,14,35,36,37,38,39,40,48,55,57,66,67],m=[5,6,8,14,35,36,37,38,39,40,48,66,67],v=[1,51],b=[1,52],_=[1,53],x=[1,54],w=[1,55],k=[1,56],T=[1,57],E=[57,58],C=[1,69],S=[1,65],A=[1,66],M=[1,67],N=[1,68],D=[1,70],O=[1,74],B=[1,75],L=[1,72],I=[1,73],R=[5,8,14,35,36,37,38,39,40,48,66,67],F={trace:function(){},yy:{},symbols_:{error:2,start:3,directive:4,NEWLINE:5,RD:6,diagram:7,EOF:8,openDirective:9,typeDirective:10,closeDirective:11,":":12,argDirective:13,open_directive:14,type_directive:15,arg_directive:16,close_directive:17,requirementDef:18,elementDef:19,relationshipDef:20,requirementType:21,requirementName:22,STRUCT_START:23,requirementBody:24,ID:25,COLONSEP:26,id:27,TEXT:28,text:29,RISK:30,riskLevel:31,VERIFYMTHD:32,verifyType:33,STRUCT_STOP:34,REQUIREMENT:35,FUNCTIONAL_REQUIREMENT:36,INTERFACE_REQUIREMENT:37,PERFORMANCE_REQUIREMENT:38,PHYSICAL_REQUIREMENT:39,DESIGN_CONSTRAINT:40,LOW_RISK:41,MED_RISK:42,HIGH_RISK:43,VERIFY_ANALYSIS:44,VERIFY_DEMONSTRATION:45,VERIFY_INSPECTION:46,VERIFY_TEST:47,ELEMENT:48,elementName:49,elementBody:50,TYPE:51,type:52,DOCREF:53,ref:54,END_ARROW_L:55,relationship:56,LINE:57,END_ARROW_R:58,CONTAINS:59,COPIES:60,DERIVES:61,SATISFIES:62,VERIFIES:63,REFINES:64,TRACES:65,unqString:66,qString:67,$accept:0,$end:1},terminals_:{2:"error",5:"NEWLINE",6:"RD",8:"EOF",12:":",14:"open_directive",15:"type_directive",16:"arg_directive",17:"close_directive",23:"STRUCT_START",25:"ID",26:"COLONSEP",28:"TEXT",30:"RISK",32:"VERIFYMTHD",34:"STRUCT_STOP",35:"REQUIREMENT",36:"FUNCTIONAL_REQUIREMENT",37:"INTERFACE_REQUIREMENT",38:"PERFORMANCE_REQUIREMENT",39:"PHYSICAL_REQUIREMENT",40:"DESIGN_CONSTRAINT",41:"LOW_RISK",42:"MED_RISK",43:"HIGH_RISK",44:"VERIFY_ANALYSIS",45:"VERIFY_DEMONSTRATION",46:"VERIFY_INSPECTION",47:"VERIFY_TEST",48:"ELEMENT",51:"TYPE",53:"DOCREF",55:"END_ARROW_L",57:"LINE",58:"END_ARROW_R",59:"CONTAINS",60:"COPIES",61:"DERIVES",62:"SATISFIES",63:"VERIFIES",64:"REFINES",65:"TRACES",66:"unqString",67:"qString"},productions_:[0,[3,3],[3,2],[3,4],[4,3],[4,5],[9,1],[10,1],[13,1],[11,1],[7,0],[7,2],[7,2],[7,2],[7,2],[7,2],[18,5],[24,5],[24,5],[24,5],[24,5],[24,2],[24,1],[21,1],[21,1],[21,1],[21,1],[21,1],[21,1],[31,1],[31,1],[31,1],[33,1],[33,1],[33,1],[33,1],[19,5],[50,5],[50,5],[50,2],[50,1],[20,5],[20,5],[56,1],[56,1],[56,1],[56,1],[56,1],[56,1],[56,1],[22,1],[22,1],[27,1],[27,1],[29,1],[29,1],[49,1],[49,1],[52,1],[52,1],[54,1],[54,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 6:r.parseDirective("%%{","open_directive");break;case 7:r.parseDirective(a[s],"type_directive");break;case 8:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 9:r.parseDirective("}%%","close_directive","pie");break;case 10:this.$=[];break;case 16:r.addRequirement(a[s-3],a[s-4]);break;case 17:r.setNewReqId(a[s-2]);break;case 18:r.setNewReqText(a[s-2]);break;case 19:r.setNewReqRisk(a[s-2]);break;case 20:r.setNewReqVerifyMethod(a[s-2]);break;case 23:this.$=r.RequirementType.REQUIREMENT;break;case 24:this.$=r.RequirementType.FUNCTIONAL_REQUIREMENT;break;case 25:this.$=r.RequirementType.INTERFACE_REQUIREMENT;break;case 26:this.$=r.RequirementType.PERFORMANCE_REQUIREMENT;break;case 27:this.$=r.RequirementType.PHYSICAL_REQUIREMENT;break;case 28:this.$=r.RequirementType.DESIGN_CONSTRAINT;break;case 29:this.$=r.RiskLevel.LOW_RISK;break;case 30:this.$=r.RiskLevel.MED_RISK;break;case 31:this.$=r.RiskLevel.HIGH_RISK;break;case 32:this.$=r.VerifyType.VERIFY_ANALYSIS;break;case 33:this.$=r.VerifyType.VERIFY_DEMONSTRATION;break;case 34:this.$=r.VerifyType.VERIFY_INSPECTION;break;case 35:this.$=r.VerifyType.VERIFY_TEST;break;case 36:r.addElement(a[s-3]);break;case 37:r.setNewElementType(a[s-2]);break;case 38:r.setNewElementDocRef(a[s-2]);break;case 41:r.addRelationship(a[s-2],a[s],a[s-4]);break;case 42:r.addRelationship(a[s-2],a[s-4],a[s]);break;case 43:this.$=r.Relationships.CONTAINS;break;case 44:this.$=r.Relationships.COPIES;break;case 45:this.$=r.Relationships.DERIVES;break;case 46:this.$=r.Relationships.SATISFIES;break;case 47:this.$=r.Relationships.VERIFIES;break;case 48:this.$=r.Relationships.REFINES;break;case 49:this.$=r.Relationships.TRACES}},table:[{3:1,4:2,6:e,9:4,14:n},{1:[3]},{3:7,4:2,5:[1,6],6:e,9:4,14:n},{5:[1,8]},{10:9,15:[1,10]},{15:[2,6]},{3:11,4:2,6:e,9:4,14:n},{1:[2,2]},{4:16,5:r,7:12,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{11:29,12:[1,30],17:p},t([12,17],[2,7]),{1:[2,1]},{8:[1,32]},{4:16,5:r,7:33,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:34,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:35,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:36,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:37,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{22:38,66:[1,39],67:[1,40]},{49:41,66:[1,42],67:[1,43]},{55:[1,44],57:[1,45]},t(y,[2,23]),t(y,[2,24]),t(y,[2,25]),t(y,[2,26]),t(y,[2,27]),t(y,[2,28]),t(g,[2,52]),t(g,[2,53]),t(m,[2,4]),{13:46,16:[1,47]},t(m,[2,9]),{1:[2,3]},{8:[2,11]},{8:[2,12]},{8:[2,13]},{8:[2,14]},{8:[2,15]},{23:[1,48]},{23:[2,50]},{23:[2,51]},{23:[1,49]},{23:[2,56]},{23:[2,57]},{56:50,59:v,60:b,61:_,62:x,63:w,64:k,65:T},{56:58,59:v,60:b,61:_,62:x,63:w,64:k,65:T},{11:59,17:p},{17:[2,8]},{5:[1,60]},{5:[1,61]},{57:[1,62]},t(E,[2,43]),t(E,[2,44]),t(E,[2,45]),t(E,[2,46]),t(E,[2,47]),t(E,[2,48]),t(E,[2,49]),{58:[1,63]},t(m,[2,5]),{5:C,24:64,25:S,28:A,30:M,32:N,34:D},{5:O,34:B,50:71,51:L,53:I},{27:76,66:f,67:d},{27:77,66:f,67:d},t(R,[2,16]),{26:[1,78]},{26:[1,79]},{26:[1,80]},{26:[1,81]},{5:C,24:82,25:S,28:A,30:M,32:N,34:D},t(R,[2,22]),t(R,[2,36]),{26:[1,83]},{26:[1,84]},{5:O,34:B,50:85,51:L,53:I},t(R,[2,40]),t(R,[2,41]),t(R,[2,42]),{27:86,66:f,67:d},{29:87,66:[1,88],67:[1,89]},{31:90,41:[1,91],42:[1,92],43:[1,93]},{33:94,44:[1,95],45:[1,96],46:[1,97],47:[1,98]},t(R,[2,21]),{52:99,66:[1,100],67:[1,101]},{54:102,66:[1,103],67:[1,104]},t(R,[2,39]),{5:[1,105]},{5:[1,106]},{5:[2,54]},{5:[2,55]},{5:[1,107]},{5:[2,29]},{5:[2,30]},{5:[2,31]},{5:[1,108]},{5:[2,32]},{5:[2,33]},{5:[2,34]},{5:[2,35]},{5:[1,109]},{5:[2,58]},{5:[2,59]},{5:[1,110]},{5:[2,60]},{5:[2,61]},{5:C,24:111,25:S,28:A,30:M,32:N,34:D},{5:C,24:112,25:S,28:A,30:M,32:N,34:D},{5:C,24:113,25:S,28:A,30:M,32:N,34:D},{5:C,24:114,25:S,28:A,30:M,32:N,34:D},{5:O,34:B,50:115,51:L,53:I},{5:O,34:B,50:116,51:L,53:I},t(R,[2,17]),t(R,[2,18]),t(R,[2,19]),t(R,[2,20]),t(R,[2,37]),t(R,[2,38])],defaultActions:{5:[2,6],7:[2,2],11:[2,1],32:[2,3],33:[2,11],34:[2,12],35:[2,13],36:[2,14],37:[2,15],39:[2,50],40:[2,51],42:[2,56],43:[2,57],47:[2,8],88:[2,54],89:[2,55],91:[2,29],92:[2,30],93:[2,31],95:[2,32],96:[2,33],97:[2,34],98:[2,35],100:[2,58],101:[2,59],103:[2,60],104:[2,61]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},P={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),14;case 1:return this.begin("type_directive"),15;case 2:return this.popState(),this.begin("arg_directive"),12;case 3:return this.popState(),this.popState(),17;case 4:return 16;case 5:return 5;case 6:case 7:case 8:break;case 9:return 8;case 10:return 6;case 11:return 23;case 12:return 34;case 13:return 26;case 14:return 25;case 15:return 28;case 16:return 30;case 17:return 32;case 18:return 35;case 19:return 36;case 20:return 37;case 21:return 38;case 22:return 39;case 23:return 40;case 24:return 41;case 25:return 42;case 26:return 43;case 27:return 44;case 28:return 45;case 29:return 46;case 30:return 47;case 31:return 48;case 32:return 59;case 33:return 60;case 34:return 61;case 35:return 62;case 36:return 63;case 37:return 64;case 38:return 65;case 39:return 51;case 40:return 53;case 41:return 55;case 42:return 58;case 43:return 57;case 44:this.begin("string");break;case 45:this.popState();break;case 46:return"qString";case 47:return e.yytext=e.yytext.trim(),66}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:(\r?\n)+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:$)/i,/^(?:requirementDiagram\b)/i,/^(?:\{)/i,/^(?:\})/i,/^(?::)/i,/^(?:id\b)/i,/^(?:text\b)/i,/^(?:risk\b)/i,/^(?:verifyMethod\b)/i,/^(?:requirement\b)/i,/^(?:functionalRequirement\b)/i,/^(?:interfaceRequirement\b)/i,/^(?:performanceRequirement\b)/i,/^(?:physicalRequirement\b)/i,/^(?:designConstraint\b)/i,/^(?:low\b)/i,/^(?:medium\b)/i,/^(?:high\b)/i,/^(?:analysis\b)/i,/^(?:demonstration\b)/i,/^(?:inspection\b)/i,/^(?:test\b)/i,/^(?:element\b)/i,/^(?:contains\b)/i,/^(?:copies\b)/i,/^(?:derives\b)/i,/^(?:satisfies\b)/i,/^(?:verifies\b)/i,/^(?:refines\b)/i,/^(?:traces\b)/i,/^(?:type\b)/i,/^(?:docref\b)/i,/^(?:<-)/i,/^(?:->)/i,/^(?:-)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[\w][^\r\n\{\<\>\-\=]*)/i],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},unqString:{rules:[],inclusive:!1},token:{rules:[],inclusive:!1},string:{rules:[45,46],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,47],inclusive:!0}}};function j(){this.yy={}}return F.lexer=P,j.prototype=F,F.Parser=j,new j}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8800).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},6876:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,3],r=[1,5],i=[1,7],a=[2,5],o=[1,15],s=[1,17],c=[1,18],u=[1,19],l=[1,21],h=[1,22],f=[1,23],d=[1,29],p=[1,30],y=[1,31],g=[1,32],m=[1,33],v=[1,34],b=[1,37],_=[1,38],x=[1,39],w=[1,40],k=[1,41],T=[1,42],E=[1,45],C=[1,4,5,16,20,22,23,24,30,32,33,34,35,36,38,40,41,42,46,47,48,49,57,67],S=[1,58],A=[4,5,16,20,22,23,24,30,32,33,34,35,36,38,42,46,47,48,49,57,67],M=[4,5,16,20,22,23,24,30,32,33,34,35,36,38,41,42,46,47,48,49,57,67],N=[4,5,16,20,22,23,24,30,32,33,34,35,36,38,40,42,46,47,48,49,57,67],D=[55,56,57],O=[1,4,5,7,16,20,22,23,24,30,32,33,34,35,36,38,40,41,42,46,47,48,49,57,67],B={trace:function(){},yy:{},symbols_:{error:2,start:3,SPACE:4,NEWLINE:5,directive:6,SD:7,document:8,line:9,statement:10,openDirective:11,typeDirective:12,closeDirective:13,":":14,argDirective:15,participant:16,actor:17,AS:18,restOfLine:19,participant_actor:20,signal:21,autonumber:22,activate:23,deactivate:24,note_statement:25,links_statement:26,link_statement:27,properties_statement:28,details_statement:29,title:30,text2:31,loop:32,end:33,rect:34,opt:35,alt:36,else_sections:37,par:38,par_sections:39,and:40,else:41,note:42,placement:43,over:44,actor_pair:45,links:46,link:47,properties:48,details:49,spaceList:50,",":51,left_of:52,right_of:53,signaltype:54,"+":55,"-":56,ACTOR:57,SOLID_OPEN_ARROW:58,DOTTED_OPEN_ARROW:59,SOLID_ARROW:60,DOTTED_ARROW:61,SOLID_CROSS:62,DOTTED_CROSS:63,SOLID_POINT:64,DOTTED_POINT:65,TXT:66,open_directive:67,type_directive:68,arg_directive:69,close_directive:70,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NEWLINE",7:"SD",14:":",16:"participant",18:"AS",19:"restOfLine",20:"participant_actor",22:"autonumber",23:"activate",24:"deactivate",30:"title",32:"loop",33:"end",34:"rect",35:"opt",36:"alt",38:"par",40:"and",41:"else",42:"note",44:"over",46:"links",47:"link",48:"properties",49:"details",51:",",52:"left_of",53:"right_of",55:"+",56:"-",57:"ACTOR",58:"SOLID_OPEN_ARROW",59:"DOTTED_OPEN_ARROW",60:"SOLID_ARROW",61:"DOTTED_ARROW",62:"SOLID_CROSS",63:"DOTTED_CROSS",64:"SOLID_POINT",65:"DOTTED_POINT",66:"TXT",67:"open_directive",68:"type_directive",69:"arg_directive",70:"close_directive"},productions_:[0,[3,2],[3,2],[3,2],[3,2],[8,0],[8,2],[9,2],[9,1],[9,1],[6,4],[6,6],[10,5],[10,3],[10,5],[10,3],[10,2],[10,1],[10,3],[10,3],[10,2],[10,2],[10,2],[10,2],[10,2],[10,3],[10,4],[10,4],[10,4],[10,4],[10,4],[10,1],[39,1],[39,4],[37,1],[37,4],[25,4],[25,4],[26,3],[27,3],[28,3],[29,3],[50,2],[50,1],[45,3],[45,1],[43,1],[43,1],[21,5],[21,5],[21,4],[17,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[31,1],[11,1],[12,1],[15,1],[13,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:return r.apply(a[s]),a[s];case 5:case 9:this.$=[];break;case 6:a[s-1].push(a[s]),this.$=a[s-1];break;case 7:case 8:case 45:this.$=a[s];break;case 12:a[s-3].type="addParticipant",a[s-3].description=r.parseMessage(a[s-1]),this.$=a[s-3];break;case 13:a[s-1].type="addParticipant",this.$=a[s-1];break;case 14:a[s-3].type="addActor",a[s-3].description=r.parseMessage(a[s-1]),this.$=a[s-3];break;case 15:a[s-1].type="addActor",this.$=a[s-1];break;case 17:r.enableSequenceNumbers();break;case 18:this.$={type:"activeStart",signalType:r.LINETYPE.ACTIVE_START,actor:a[s-1]};break;case 19:this.$={type:"activeEnd",signalType:r.LINETYPE.ACTIVE_END,actor:a[s-1]};break;case 25:this.$=[{type:"setTitle",text:a[s-1]}];break;case 26:a[s-1].unshift({type:"loopStart",loopText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.LOOP_START}),a[s-1].push({type:"loopEnd",loopText:a[s-2],signalType:r.LINETYPE.LOOP_END}),this.$=a[s-1];break;case 27:a[s-1].unshift({type:"rectStart",color:r.parseMessage(a[s-2]),signalType:r.LINETYPE.RECT_START}),a[s-1].push({type:"rectEnd",color:r.parseMessage(a[s-2]),signalType:r.LINETYPE.RECT_END}),this.$=a[s-1];break;case 28:a[s-1].unshift({type:"optStart",optText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.OPT_START}),a[s-1].push({type:"optEnd",optText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.OPT_END}),this.$=a[s-1];break;case 29:a[s-1].unshift({type:"altStart",altText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.ALT_START}),a[s-1].push({type:"altEnd",signalType:r.LINETYPE.ALT_END}),this.$=a[s-1];break;case 30:a[s-1].unshift({type:"parStart",parText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.PAR_START}),a[s-1].push({type:"parEnd",signalType:r.LINETYPE.PAR_END}),this.$=a[s-1];break;case 33:this.$=a[s-3].concat([{type:"and",parText:r.parseMessage(a[s-1]),signalType:r.LINETYPE.PAR_AND},a[s]]);break;case 35:this.$=a[s-3].concat([{type:"else",altText:r.parseMessage(a[s-1]),signalType:r.LINETYPE.ALT_ELSE},a[s]]);break;case 36:this.$=[a[s-1],{type:"addNote",placement:a[s-2],actor:a[s-1].actor,text:a[s]}];break;case 37:a[s-2]=[].concat(a[s-1],a[s-1]).slice(0,2),a[s-2][0]=a[s-2][0].actor,a[s-2][1]=a[s-2][1].actor,this.$=[a[s-1],{type:"addNote",placement:r.PLACEMENT.OVER,actor:a[s-2].slice(0,2),text:a[s]}];break;case 38:this.$=[a[s-1],{type:"addLinks",actor:a[s-1].actor,text:a[s]}];break;case 39:this.$=[a[s-1],{type:"addALink",actor:a[s-1].actor,text:a[s]}];break;case 40:this.$=[a[s-1],{type:"addProperties",actor:a[s-1].actor,text:a[s]}];break;case 41:this.$=[a[s-1],{type:"addDetails",actor:a[s-1].actor,text:a[s]}];break;case 44:this.$=[a[s-2],a[s]];break;case 46:this.$=r.PLACEMENT.LEFTOF;break;case 47:this.$=r.PLACEMENT.RIGHTOF;break;case 48:this.$=[a[s-4],a[s-1],{type:"addMessage",from:a[s-4].actor,to:a[s-1].actor,signalType:a[s-3],msg:a[s]},{type:"activeStart",signalType:r.LINETYPE.ACTIVE_START,actor:a[s-1]}];break;case 49:this.$=[a[s-4],a[s-1],{type:"addMessage",from:a[s-4].actor,to:a[s-1].actor,signalType:a[s-3],msg:a[s]},{type:"activeEnd",signalType:r.LINETYPE.ACTIVE_END,actor:a[s-4]}];break;case 50:this.$=[a[s-3],a[s-1],{type:"addMessage",from:a[s-3].actor,to:a[s-1].actor,signalType:a[s-2],msg:a[s]}];break;case 51:this.$={type:"addParticipant",actor:a[s]};break;case 52:this.$=r.LINETYPE.SOLID_OPEN;break;case 53:this.$=r.LINETYPE.DOTTED_OPEN;break;case 54:this.$=r.LINETYPE.SOLID;break;case 55:this.$=r.LINETYPE.DOTTED;break;case 56:this.$=r.LINETYPE.SOLID_CROSS;break;case 57:this.$=r.LINETYPE.DOTTED_CROSS;break;case 58:this.$=r.LINETYPE.SOLID_POINT;break;case 59:this.$=r.LINETYPE.DOTTED_POINT;break;case 60:this.$=r.parseMessage(a[s].trim().substring(1));break;case 61:r.parseDirective("%%{","open_directive");break;case 62:r.parseDirective(a[s],"type_directive");break;case 63:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 64:r.parseDirective("}%%","close_directive","sequence")}},table:[{3:1,4:e,5:n,6:4,7:r,11:6,67:i},{1:[3]},{3:8,4:e,5:n,6:4,7:r,11:6,67:i},{3:9,4:e,5:n,6:4,7:r,11:6,67:i},{3:10,4:e,5:n,6:4,7:r,11:6,67:i},t([1,4,5,16,20,22,23,24,30,32,34,35,36,38,42,46,47,48,49,57,67],a,{8:11}),{12:12,68:[1,13]},{68:[2,61]},{1:[2,1]},{1:[2,2]},{1:[2,3]},{1:[2,4],4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{13:43,14:[1,44],70:E},t([14,70],[2,62]),t(C,[2,6]),{6:35,10:46,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},t(C,[2,8]),t(C,[2,9]),{17:47,57:T},{17:48,57:T},{5:[1,49]},t(C,[2,17]),{17:50,57:T},{17:51,57:T},{5:[1,52]},{5:[1,53]},{5:[1,54]},{5:[1,55]},{5:[1,56]},{31:57,66:S},{19:[1,59]},{19:[1,60]},{19:[1,61]},{19:[1,62]},{19:[1,63]},t(C,[2,31]),{54:64,58:[1,65],59:[1,66],60:[1,67],61:[1,68],62:[1,69],63:[1,70],64:[1,71],65:[1,72]},{43:73,44:[1,74],52:[1,75],53:[1,76]},{17:77,57:T},{17:78,57:T},{17:79,57:T},{17:80,57:T},t([5,18,51,58,59,60,61,62,63,64,65,66],[2,51]),{5:[1,81]},{15:82,69:[1,83]},{5:[2,64]},t(C,[2,7]),{5:[1,85],18:[1,84]},{5:[1,87],18:[1,86]},t(C,[2,16]),{5:[1,88]},{5:[1,89]},t(C,[2,20]),t(C,[2,21]),t(C,[2,22]),t(C,[2,23]),t(C,[2,24]),{5:[1,90]},{5:[2,60]},t(A,a,{8:91}),t(A,a,{8:92}),t(A,a,{8:93}),t(M,a,{37:94,8:95}),t(N,a,{39:96,8:97}),{17:100,55:[1,98],56:[1,99],57:T},t(D,[2,52]),t(D,[2,53]),t(D,[2,54]),t(D,[2,55]),t(D,[2,56]),t(D,[2,57]),t(D,[2,58]),t(D,[2,59]),{17:101,57:T},{17:103,45:102,57:T},{57:[2,46]},{57:[2,47]},{31:104,66:S},{31:105,66:S},{31:106,66:S},{31:107,66:S},t(O,[2,10]),{13:108,70:E},{70:[2,63]},{19:[1,109]},t(C,[2,13]),{19:[1,110]},t(C,[2,15]),t(C,[2,18]),t(C,[2,19]),t(C,[2,25]),{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[1,111],34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[1,112],34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[1,113],34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{33:[1,114]},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[2,34],34:y,35:g,36:m,38:v,41:[1,115],42:b,46:_,47:x,48:w,49:k,57:T,67:i},{33:[1,116]},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[2,32],34:y,35:g,36:m,38:v,40:[1,117],42:b,46:_,47:x,48:w,49:k,57:T,67:i},{17:118,57:T},{17:119,57:T},{31:120,66:S},{31:121,66:S},{31:122,66:S},{51:[1,123],66:[2,45]},{5:[2,38]},{5:[2,39]},{5:[2,40]},{5:[2,41]},{5:[1,124]},{5:[1,125]},{5:[1,126]},t(C,[2,26]),t(C,[2,27]),t(C,[2,28]),t(C,[2,29]),{19:[1,127]},t(C,[2,30]),{19:[1,128]},{31:129,66:S},{31:130,66:S},{5:[2,50]},{5:[2,36]},{5:[2,37]},{17:131,57:T},t(O,[2,11]),t(C,[2,12]),t(C,[2,14]),t(M,a,{8:95,37:132}),t(N,a,{8:97,39:133}),{5:[2,48]},{5:[2,49]},{66:[2,44]},{33:[2,35]},{33:[2,33]}],defaultActions:{7:[2,61],8:[2,1],9:[2,2],10:[2,3],45:[2,64],58:[2,60],75:[2,46],76:[2,47],83:[2,63],104:[2,38],105:[2,39],106:[2,40],107:[2,41],120:[2,50],121:[2,36],122:[2,37],129:[2,48],130:[2,49],131:[2,44],132:[2,35],133:[2,33]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},L={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),67;case 1:return this.begin("type_directive"),68;case 2:return this.popState(),this.begin("arg_directive"),14;case 3:return this.popState(),this.popState(),70;case 4:return 69;case 5:case 39:case 52:return 5;case 6:case 7:case 8:case 9:case 10:break;case 11:return this.begin("ID"),16;case 12:return this.begin("ID"),20;case 13:return e.yytext=e.yytext.trim(),this.begin("ALIAS"),57;case 14:return this.popState(),this.popState(),this.begin("LINE"),18;case 15:return this.popState(),this.popState(),5;case 16:return this.begin("LINE"),32;case 17:return this.begin("LINE"),34;case 18:return this.begin("LINE"),35;case 19:return this.begin("LINE"),36;case 20:return this.begin("LINE"),41;case 21:return this.begin("LINE"),38;case 22:return this.begin("LINE"),40;case 23:return this.popState(),19;case 24:return 33;case 25:return 52;case 26:return 53;case 27:return 46;case 28:return 47;case 29:return 48;case 30:return 49;case 31:return 44;case 32:return 42;case 33:return this.begin("ID"),23;case 34:return this.begin("ID"),24;case 35:return 30;case 36:return 7;case 37:return 22;case 38:return 51;case 40:return e.yytext=e.yytext.trim(),57;case 41:return 60;case 42:return 61;case 43:return 58;case 44:return 59;case 45:return 62;case 46:return 63;case 47:return 64;case 48:return 65;case 49:return 66;case 50:return 55;case 51:return 56;case 53:return"INVALID"}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:participant\b)/i,/^(?:actor\b)/i,/^(?:[^\->:\n,;]+?(?=((?!\n)\s)+as(?!\n)\s|[#\n;]|$))/i,/^(?:as\b)/i,/^(?:(?:))/i,/^(?:loop\b)/i,/^(?:rect\b)/i,/^(?:opt\b)/i,/^(?:alt\b)/i,/^(?:else\b)/i,/^(?:par\b)/i,/^(?:and\b)/i,/^(?:(?:[:]?(?:no)?wrap)?[^#\n;]*)/i,/^(?:end\b)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:links\b)/i,/^(?:link\b)/i,/^(?:properties\b)/i,/^(?:details\b)/i,/^(?:over\b)/i,/^(?:note\b)/i,/^(?:activate\b)/i,/^(?:deactivate\b)/i,/^(?:title\b)/i,/^(?:sequenceDiagram\b)/i,/^(?:autonumber\b)/i,/^(?:,)/i,/^(?:;)/i,/^(?:[^\+\->:\n,;]+((?!(-x|--x|-\)|--\)))[\-]*[^\+\->:\n,;]+)*)/i,/^(?:->>)/i,/^(?:-->>)/i,/^(?:->)/i,/^(?:-->)/i,/^(?:-[x])/i,/^(?:--[x])/i,/^(?:-[\)])/i,/^(?:--[\)])/i,/^(?::(?:(?:no)?wrap)?[^#\n;]+)/i,/^(?:\+)/i,/^(?:-)/i,/^(?:$)/i,/^(?:.)/i],conditions:{open_directive:{rules:[1,8],inclusive:!1},type_directive:{rules:[2,3,8],inclusive:!1},arg_directive:{rules:[3,4,8],inclusive:!1},ID:{rules:[7,8,13],inclusive:!1},ALIAS:{rules:[7,8,14,15],inclusive:!1},LINE:{rules:[7,8,23],inclusive:!1},INITIAL:{rules:[0,5,6,8,9,10,11,12,16,17,18,19,20,21,22,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53],inclusive:!0}}};function I(){this.yy={}}return B.lexer=L,I.prototype=B,B.Parser=I,new I}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(1993).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},3584:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,3],r=[1,5],i=[1,7],a=[2,5],o=[1,15],s=[1,17],c=[1,19],u=[1,20],l=[1,21],h=[1,22],f=[1,30],d=[1,23],p=[1,24],y=[1,25],g=[1,26],m=[1,27],v=[1,32],b=[1,33],_=[1,34],x=[1,35],w=[1,31],k=[1,38],T=[1,4,5,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],E=[1,4,5,12,13,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],C=[1,4,5,7,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],S=[4,5,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],A={trace:function(){},yy:{},symbols_:{error:2,start:3,SPACE:4,NL:5,directive:6,SD:7,document:8,line:9,statement:10,idStatement:11,DESCR:12,"--\x3e":13,HIDE_EMPTY:14,scale:15,WIDTH:16,COMPOSIT_STATE:17,STRUCT_START:18,STRUCT_STOP:19,STATE_DESCR:20,AS:21,ID:22,FORK:23,JOIN:24,CHOICE:25,CONCURRENT:26,note:27,notePosition:28,NOTE_TEXT:29,direction:30,openDirective:31,typeDirective:32,closeDirective:33,":":34,argDirective:35,direction_tb:36,direction_bt:37,direction_rl:38,direction_lr:39,eol:40,";":41,EDGE_STATE:42,left_of:43,right_of:44,open_directive:45,type_directive:46,arg_directive:47,close_directive:48,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NL",7:"SD",12:"DESCR",13:"--\x3e",14:"HIDE_EMPTY",15:"scale",16:"WIDTH",17:"COMPOSIT_STATE",18:"STRUCT_START",19:"STRUCT_STOP",20:"STATE_DESCR",21:"AS",22:"ID",23:"FORK",24:"JOIN",25:"CHOICE",26:"CONCURRENT",27:"note",29:"NOTE_TEXT",34:":",36:"direction_tb",37:"direction_bt",38:"direction_rl",39:"direction_lr",41:";",42:"EDGE_STATE",43:"left_of",44:"right_of",45:"open_directive",46:"type_directive",47:"arg_directive",48:"close_directive"},productions_:[0,[3,2],[3,2],[3,2],[3,2],[8,0],[8,2],[9,2],[9,1],[9,1],[10,1],[10,2],[10,3],[10,4],[10,1],[10,2],[10,1],[10,4],[10,3],[10,6],[10,1],[10,1],[10,1],[10,1],[10,4],[10,4],[10,1],[10,1],[6,3],[6,5],[30,1],[30,1],[30,1],[30,1],[40,1],[40,1],[11,1],[11,1],[28,1],[28,1],[31,1],[32,1],[35,1],[33,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:return r.setRootDoc(a[s]),a[s];case 5:this.$=[];break;case 6:"nl"!=a[s]&&(a[s-1].push(a[s]),this.$=a[s-1]);break;case 7:case 8:case 36:case 37:this.$=a[s];break;case 9:this.$="nl";break;case 10:this.$={stmt:"state",id:a[s],type:"default",description:""};break;case 11:this.$={stmt:"state",id:a[s-1],type:"default",description:r.trimColon(a[s])};break;case 12:this.$={stmt:"relation",state1:{stmt:"state",id:a[s-2],type:"default",description:""},state2:{stmt:"state",id:a[s],type:"default",description:""}};break;case 13:this.$={stmt:"relation",state1:{stmt:"state",id:a[s-3],type:"default",description:""},state2:{stmt:"state",id:a[s-1],type:"default",description:""},description:a[s].substr(1).trim()};break;case 17:this.$={stmt:"state",id:a[s-3],type:"default",description:"",doc:a[s-1]};break;case 18:var c=a[s],u=a[s-2].trim();if(a[s].match(":")){var l=a[s].split(":");c=l[0],u=[u,l[1]]}this.$={stmt:"state",id:c,type:"default",description:u};break;case 19:this.$={stmt:"state",id:a[s-3],type:"default",description:a[s-5],doc:a[s-1]};break;case 20:this.$={stmt:"state",id:a[s],type:"fork"};break;case 21:this.$={stmt:"state",id:a[s],type:"join"};break;case 22:this.$={stmt:"state",id:a[s],type:"choice"};break;case 23:this.$={stmt:"state",id:r.getDividerId(),type:"divider"};break;case 24:this.$={stmt:"state",id:a[s-1].trim(),note:{position:a[s-2].trim(),text:a[s].trim()}};break;case 30:r.setDirection("TB"),this.$={stmt:"dir",value:"TB"};break;case 31:r.setDirection("BT"),this.$={stmt:"dir",value:"BT"};break;case 32:r.setDirection("RL"),this.$={stmt:"dir",value:"RL"};break;case 33:r.setDirection("LR"),this.$={stmt:"dir",value:"LR"};break;case 40:r.parseDirective("%%{","open_directive");break;case 41:r.parseDirective(a[s],"type_directive");break;case 42:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 43:r.parseDirective("}%%","close_directive","state")}},table:[{3:1,4:e,5:n,6:4,7:r,31:6,45:i},{1:[3]},{3:8,4:e,5:n,6:4,7:r,31:6,45:i},{3:9,4:e,5:n,6:4,7:r,31:6,45:i},{3:10,4:e,5:n,6:4,7:r,31:6,45:i},t([1,4,5,14,15,17,20,22,23,24,25,26,27,36,37,38,39,42,45],a,{8:11}),{32:12,46:[1,13]},{46:[2,40]},{1:[2,1]},{1:[2,2]},{1:[2,3]},{1:[2,4],4:o,5:s,6:28,9:14,10:16,11:18,14:c,15:u,17:l,20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},{33:36,34:[1,37],48:k},t([34,48],[2,41]),t(T,[2,6]),{6:28,10:39,11:18,14:c,15:u,17:l,20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},t(T,[2,8]),t(T,[2,9]),t(T,[2,10],{12:[1,40],13:[1,41]}),t(T,[2,14]),{16:[1,42]},t(T,[2,16],{18:[1,43]}),{21:[1,44]},t(T,[2,20]),t(T,[2,21]),t(T,[2,22]),t(T,[2,23]),{28:45,29:[1,46],43:[1,47],44:[1,48]},t(T,[2,26]),t(T,[2,27]),t(E,[2,36]),t(E,[2,37]),t(T,[2,30]),t(T,[2,31]),t(T,[2,32]),t(T,[2,33]),t(C,[2,28]),{35:49,47:[1,50]},t(C,[2,43]),t(T,[2,7]),t(T,[2,11]),{11:51,22:f,42:w},t(T,[2,15]),t(S,a,{8:52}),{22:[1,53]},{22:[1,54]},{21:[1,55]},{22:[2,38]},{22:[2,39]},{33:56,48:k},{48:[2,42]},t(T,[2,12],{12:[1,57]}),{4:o,5:s,6:28,9:14,10:16,11:18,14:c,15:u,17:l,19:[1,58],20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},t(T,[2,18],{18:[1,59]}),{29:[1,60]},{22:[1,61]},t(C,[2,29]),t(T,[2,13]),t(T,[2,17]),t(S,a,{8:62}),t(T,[2,24]),t(T,[2,25]),{4:o,5:s,6:28,9:14,10:16,11:18,14:c,15:u,17:l,19:[1,63],20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},t(T,[2,19])],defaultActions:{7:[2,40],8:[2,1],9:[2,2],10:[2,3],47:[2,38],48:[2,39],50:[2,42]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},M={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:case 26:return 36;case 1:case 27:return 37;case 2:case 28:return 38;case 3:case 29:return 39;case 4:return this.begin("open_directive"),45;case 5:return this.begin("type_directive"),46;case 6:return this.popState(),this.begin("arg_directive"),34;case 7:return this.popState(),this.popState(),48;case 8:return 47;case 9:case 10:case 12:case 13:case 14:case 15:case 39:case 45:break;case 11:case 59:return 5;case 16:return this.pushState("SCALE"),15;case 17:return 16;case 18:case 33:case 36:this.popState();break;case 19:this.pushState("STATE");break;case 20:case 23:return this.popState(),e.yytext=e.yytext.slice(0,-8).trim(),23;case 21:case 24:return this.popState(),e.yytext=e.yytext.slice(0,-8).trim(),24;case 22:case 25:return this.popState(),e.yytext=e.yytext.slice(0,-10).trim(),25;case 30:this.begin("STATE_STRING");break;case 31:return this.popState(),this.pushState("STATE_ID"),"AS";case 32:case 47:return this.popState(),"ID";case 34:return"STATE_DESCR";case 35:return 17;case 37:return this.popState(),this.pushState("struct"),18;case 38:return this.popState(),19;case 40:return this.begin("NOTE"),27;case 41:return this.popState(),this.pushState("NOTE_ID"),43;case 42:return this.popState(),this.pushState("NOTE_ID"),44;case 43:this.popState(),this.pushState("FLOATING_NOTE");break;case 44:return this.popState(),this.pushState("FLOATING_NOTE_ID"),"AS";case 46:return"NOTE_TEXT";case 48:return this.popState(),this.pushState("NOTE_TEXT"),22;case 49:return this.popState(),e.yytext=e.yytext.substr(2).trim(),29;case 50:return this.popState(),e.yytext=e.yytext.slice(0,-8).trim(),29;case 51:case 52:return 7;case 53:return 14;case 54:return 42;case 55:return 22;case 56:return e.yytext=e.yytext.trim(),12;case 57:return 13;case 58:return 26;case 60:return"INVALID"}},rules:[/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:state\s+)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*\[\[fork\]\])/i,/^(?:.*\[\[join\]\])/i,/^(?:.*\[\[choice\]\])/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:["])/i,/^(?:\s*as\s+)/i,/^(?:[^\n\{]*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[^\n\s\{]+)/i,/^(?:\n)/i,/^(?:\{)/i,/^(?:\})/i,/^(?:[\n])/i,/^(?:note\s+)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:")/i,/^(?:\s*as\s*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[^\n]*)/i,/^(?:\s*[^:\n\s\-]+)/i,/^(?:\s*:[^:\n;]+)/i,/^(?:[\s\S]*?end note\b)/i,/^(?:stateDiagram\s+)/i,/^(?:stateDiagram-v2\s+)/i,/^(?:hide empty description\b)/i,/^(?:\[\*\])/i,/^(?:[^:\n\s\-\{]+)/i,/^(?:\s*:[^:\n;]+)/i,/^(?:-->)/i,/^(?:--)/i,/^(?:$)/i,/^(?:.)/i],conditions:{LINE:{rules:[13,14],inclusive:!1},close_directive:{rules:[13,14],inclusive:!1},arg_directive:{rules:[7,8,13,14],inclusive:!1},type_directive:{rules:[6,7,13,14],inclusive:!1},open_directive:{rules:[5,13,14],inclusive:!1},struct:{rules:[13,14,19,26,27,28,29,38,39,40,54,55,56,57,58],inclusive:!1},FLOATING_NOTE_ID:{rules:[47],inclusive:!1},FLOATING_NOTE:{rules:[44,45,46],inclusive:!1},NOTE_TEXT:{rules:[49,50],inclusive:!1},NOTE_ID:{rules:[48],inclusive:!1},NOTE:{rules:[41,42,43],inclusive:!1},SCALE:{rules:[17,18],inclusive:!1},ALIAS:{rules:[],inclusive:!1},STATE_ID:{rules:[32],inclusive:!1},STATE_STRING:{rules:[33,34],inclusive:!1},FORK_STATE:{rules:[],inclusive:!1},STATE:{rules:[13,14,20,21,22,23,24,25,30,31,35,36,37],inclusive:!1},ID:{rules:[13,14],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,9,10,11,12,14,15,16,19,37,40,51,52,53,54,55,56,57,59,60],inclusive:!0}}};function N(){this.yy={}}return A.lexer=M,N.prototype=A,A.Parser=N,new N}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(3069).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},9763:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,5],r=[6,9,11,17,18,19,21],i=[1,15],a=[1,16],o=[1,17],s=[1,21],c=[4,6,9,11,17,18,19,21],u={trace:function(){},yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,directive:7,line:8,SPACE:9,statement:10,NEWLINE:11,openDirective:12,typeDirective:13,closeDirective:14,":":15,argDirective:16,title:17,section:18,taskName:19,taskData:20,open_directive:21,type_directive:22,arg_directive:23,close_directive:24,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",9:"SPACE",11:"NEWLINE",15:":",17:"title",18:"section",19:"taskName",20:"taskData",21:"open_directive",22:"type_directive",23:"arg_directive",24:"close_directive"},productions_:[0,[3,3],[3,2],[5,0],[5,2],[8,2],[8,1],[8,1],[8,1],[7,4],[7,6],[10,1],[10,1],[10,2],[10,1],[12,1],[13,1],[16,1],[14,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 1:return a[s-1];case 3:case 7:case 8:this.$=[];break;case 4:a[s-1].push(a[s]),this.$=a[s-1];break;case 5:case 6:this.$=a[s];break;case 11:r.setTitle(a[s].substr(6)),this.$=a[s].substr(6);break;case 12:r.addSection(a[s].substr(8)),this.$=a[s].substr(8);break;case 13:r.addTask(a[s-1],a[s]),this.$="task";break;case 15:r.parseDirective("%%{","open_directive");break;case 16:r.parseDirective(a[s],"type_directive");break;case 17:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 18:r.parseDirective("}%%","close_directive","journey")}},table:[{3:1,4:e,7:3,12:4,21:n},{1:[3]},t(r,[2,3],{5:6}),{3:7,4:e,7:3,12:4,21:n},{13:8,22:[1,9]},{22:[2,15]},{6:[1,10],7:18,8:11,9:[1,12],10:13,11:[1,14],12:4,17:i,18:a,19:o,21:n},{1:[2,2]},{14:19,15:[1,20],24:s},t([15,24],[2,16]),t(r,[2,8],{1:[2,1]}),t(r,[2,4]),{7:18,10:22,12:4,17:i,18:a,19:o,21:n},t(r,[2,6]),t(r,[2,7]),t(r,[2,11]),t(r,[2,12]),{20:[1,23]},t(r,[2,14]),{11:[1,24]},{16:25,23:[1,26]},{11:[2,18]},t(r,[2,5]),t(r,[2,13]),t(c,[2,9]),{14:27,24:s},{24:[2,17]},{11:[1,28]},t(c,[2,10])],defaultActions:{5:[2,15],7:[2,2],21:[2,18],26:[2,17]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},l={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),21;case 1:return this.begin("type_directive"),22;case 2:return this.popState(),this.begin("arg_directive"),15;case 3:return this.popState(),this.popState(),24;case 4:return 23;case 5:case 6:case 8:case 9:break;case 7:return 11;case 10:return 4;case 11:return 17;case 12:return 18;case 13:return 19;case 14:return 20;case 15:return 15;case 16:return 6;case 17:return"INVALID"}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:journey\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:section\s[^#:\n;]+)/i,/^(?:[^#:\n;]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{open_directive:{rules:[1],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,13,14,15,16,17],inclusive:!0}}};function h(){this.yy={}}return u.lexer=l,h.prototype=u,u.Parser=h,new h}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(9143).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},9609:t=>{"use strict";var e=/^(%20|\s)*(javascript|data)/im,n=/[^\x20-\x7E]/gim,r=/^([^:]+):/gm,i=[".","/"];t.exports={sanitizeUrl:function(t){if(!t)return"about:blank";var a,o,s=t.replace(n,"").trim();return function(t){return i.indexOf(t[0])>-1}(s)?s:(o=s.match(r))?(a=o[0],e.test(a)?"about:blank":s):"about:blank"}}},3841:t=>{t.exports=function(t,e){return t.intersect(e)}},7458:(t,e,n)=>{"use strict";n.d(e,{default:()=>fC});var r=n(1941),i=n.n(r),a={debug:1,info:2,warn:3,error:4,fatal:5},o={debug:function(){},info:function(){},warn:function(){},error:function(){},fatal:function(){}},s=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"fatal";isNaN(t)&&(t=t.toLowerCase(),void 0!==a[t]&&(t=a[t])),o.trace=function(){},o.debug=function(){},o.info=function(){},o.warn=function(){},o.error=function(){},o.fatal=function(){},t<=a.fatal&&(o.fatal=console.error?console.error.bind(console,c("FATAL"),"color: orange"):console.log.bind(console,"",c("FATAL"))),t<=a.error&&(o.error=console.error?console.error.bind(console,c("ERROR"),"color: orange"):console.log.bind(console,"",c("ERROR"))),t<=a.warn&&(o.warn=console.warn?console.warn.bind(console,c("WARN"),"color: orange"):console.log.bind(console,"",c("WARN"))),t<=a.info&&(o.info=console.info?console.info.bind(console,c("INFO"),"color: lightblue"):console.log.bind(console,"",c("INFO"))),t<=a.debug&&(o.debug=console.debug?console.debug.bind(console,c("DEBUG"),"color: lightgreen"):console.log.bind(console,"",c("DEBUG")))},c=function(t){var e=i()().format("ss.SSS");return"%c".concat(e," : ").concat(t," : ")};function u(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n=e)&&(n=e);else{let r=-1;for(let i of t)null!=(i=e(i,++r,t))&&(n=i)&&(n=i)}return n}function l(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n>e||void 0===n&&e>=e)&&(n=e);else{let r=-1;for(let i of t)null!=(i=e(i,++r,t))&&(n>i||void 0===n&&i>=i)&&(n=i)}return n}function h(t){return t}var f=1e-6;function d(t){return"translate("+t+",0)"}function p(t){return"translate(0,"+t+")"}function y(t){return e=>+t(e)}function g(t,e){return e=Math.max(0,t.bandwidth()-2*e)/2,t.round()&&(e=Math.round(e)),n=>+t(n)+e}function m(){return!this.__axis}function v(t,e){var n=[],r=null,i=null,a=6,o=6,s=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,u=1===t||4===t?-1:1,l=4===t||2===t?"x":"y",v=1===t||3===t?d:p;function b(d){var p=null==r?e.ticks?e.ticks.apply(e,n):e.domain():r,b=null==i?e.tickFormat?e.tickFormat.apply(e,n):h:i,_=Math.max(a,0)+s,x=e.range(),w=+x[0]+c,k=+x[x.length-1]+c,T=(e.bandwidth?g:y)(e.copy(),c),E=d.selection?d.selection():d,C=E.selectAll(".domain").data([null]),S=E.selectAll(".tick").data(p,e).order(),A=S.exit(),M=S.enter().append("g").attr("class","tick"),N=S.select("line"),D=S.select("text");C=C.merge(C.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),S=S.merge(M),N=N.merge(M.append("line").attr("stroke","currentColor").attr(l+"2",u*a)),D=D.merge(M.append("text").attr("fill","currentColor").attr(l,u*_).attr("dy",1===t?"0em":3===t?"0.71em":"0.32em")),d!==E&&(C=C.transition(d),S=S.transition(d),N=N.transition(d),D=D.transition(d),A=A.transition(d).attr("opacity",f).attr("transform",(function(t){return isFinite(t=T(t))?v(t+c):this.getAttribute("transform")})),M.attr("opacity",f).attr("transform",(function(t){var e=this.parentNode.__axis;return v((e&&isFinite(e=e(t))?e:T(t))+c)}))),A.remove(),C.attr("d",4===t||2===t?o?"M"+u*o+","+w+"H"+c+"V"+k+"H"+u*o:"M"+c+","+w+"V"+k:o?"M"+w+","+u*o+"V"+c+"H"+k+"V"+u*o:"M"+w+","+c+"H"+k),S.attr("opacity",1).attr("transform",(function(t){return v(T(t)+c)})),N.attr(l+"2",u*a),D.attr(l,u*_).text(b),E.filter(m).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",2===t?"start":4===t?"end":"middle"),E.each((function(){this.__axis=T}))}return b.scale=function(t){return arguments.length?(e=t,b):e},b.ticks=function(){return n=Array.from(arguments),b},b.tickArguments=function(t){return arguments.length?(n=null==t?[]:Array.from(t),b):n.slice()},b.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),b):r&&r.slice()},b.tickFormat=function(t){return arguments.length?(i=t,b):i},b.tickSize=function(t){return arguments.length?(a=o=+t,b):a},b.tickSizeInner=function(t){return arguments.length?(a=+t,b):a},b.tickSizeOuter=function(t){return arguments.length?(o=+t,b):o},b.tickPadding=function(t){return arguments.length?(s=+t,b):s},b.offset=function(t){return arguments.length?(c=+t,b):c},b}function b(){}function _(t){return null==t?b:function(){return this.querySelector(t)}}function x(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function w(){return[]}function k(t){return null==t?w:function(){return this.querySelectorAll(t)}}function T(t){return function(){return this.matches(t)}}function E(t){return function(e){return e.matches(t)}}var C=Array.prototype.find;function S(){return this.firstElementChild}var A=Array.prototype.filter;function M(){return Array.from(this.children)}function N(t){return new Array(t.length)}function D(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function O(t){return function(){return t}}function B(t,e,n,r,i,a){for(var o,s=0,c=e.length,u=a.length;se?1:t>=e?0:NaN}D.prototype={constructor:D,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var P="http://www.w3.org/1999/xhtml";const j={svg:"http://www.w3.org/2000/svg",xhtml:P,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function Y(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),j.hasOwnProperty(e)?{space:j[e],local:t}:t}function z(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function q(t,e){return function(){this.setAttribute(t,e)}}function H(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function $(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function W(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function V(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function G(t){return function(){this.style.removeProperty(t)}}function X(t,e,n){return function(){this.style.setProperty(t,e,n)}}function Z(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function Q(t,e){return t.style.getPropertyValue(e)||V(t).getComputedStyle(t,null).getPropertyValue(e)}function K(t){return function(){delete this[t]}}function J(t,e){return function(){this[t]=e}}function tt(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function et(t){return t.trim().split(/^|\s+/)}function nt(t){return t.classList||new rt(t)}function rt(t){this._node=t,this._names=et(t.getAttribute("class")||"")}function it(t,e){for(var n=nt(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function Et(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Nt=[null];function Dt(t,e){this._groups=t,this._parents=e}function Ot(){return new Dt([[document.documentElement]],Nt)}Dt.prototype=Ot.prototype={constructor:Dt,select:function(t){"function"!=typeof t&&(t=_(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=x&&(x=_+1);!(b=g[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=F);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?G:"function"==typeof e?Z:X)(t,e,null==n?"":n)):Q(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?K:"function"==typeof e?tt:J)(t,e)):this.node()[t]},classed:function(t,e){var n=et(t+"");if(arguments.length<2){for(var r=nt(this.node()),i=-1,a=n.length;++i{}};function It(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function Pt(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--qt}()}finally{qt=0,function(){for(var t,e,n=zt,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:zt=e);Ut=t,re(r)}(),Vt=0}}function ne(){var t=Xt.now(),e=t-Wt;e>1e3&&(Gt-=e,Wt=t)}function re(t){qt||(Ht&&(Ht=clearTimeout(Ht)),t-Vt>24?(t<1/0&&(Ht=setTimeout(ee,t-Xt.now()-Gt)),$t&&($t=clearInterval($t))):($t||(Wt=Xt.now(),$t=setInterval(ne,1e3)),qt=1,Zt(ee)))}function ie(t,e,n){var r=new Jt;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}Jt.prototype=te.prototype={constructor:Jt,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Qt():+n)+(null==e?0:+e),this._next||Ut===this||(Ut?Ut._next=this:zt=this,Ut=this),this._call=t,this._time=n,re()},stop:function(){this._call&&(this._call=null,this._time=1/0,re())}};var ae=Yt("start","end","cancel","interrupt"),oe=[];function se(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return ie(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function ue(t,e){var n=le(t,e);if(n.state>3)throw new Error("too late; already running");return n}function le(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function he(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var fe,de=180/Math.PI,pe={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function ye(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:he(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:he(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:he(t,n)},{i:s-2,x:he(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?Ue(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?Ue(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=De.exec(t))?new $e(e[1],e[2],e[3],1):(e=Oe.exec(t))?new $e(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Be.exec(t))?Ue(e[1],e[2],e[3],e[4]):(e=Le.exec(t))?Ue(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Ie.exec(t))?Xe(e[1],e[2]/100,e[3]/100,1):(e=Re.exec(t))?Xe(e[1],e[2]/100,e[3]/100,e[4]):Fe.hasOwnProperty(t)?ze(Fe[t]):"transparent"===t?new $e(NaN,NaN,NaN,0):null}function ze(t){return new $e(t>>16&255,t>>8&255,255&t,1)}function Ue(t,e,n,r){return r<=0&&(t=e=n=NaN),new $e(t,e,n,r)}function qe(t){return t instanceof Te||(t=Ye(t)),t?new $e((t=t.rgb()).r,t.g,t.b,t.opacity):new $e}function He(t,e,n,r){return 1===arguments.length?qe(t):new $e(t,e,n,null==r?1:r)}function $e(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function We(){return"#"+Ge(this.r)+Ge(this.g)+Ge(this.b)}function Ve(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function Ge(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function Xe(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Qe(t,e,n,r)}function Ze(t){if(t instanceof Qe)return new Qe(t.h,t.s,t.l,t.opacity);if(t instanceof Te||(t=Ye(t)),!t)return new Qe;if(t instanceof Qe)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new Qe(o,s,c,t.opacity)}function Qe(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function Ke(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function Je(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}we(Te,Ye,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:Pe,formatHex:Pe,formatHsl:function(){return Ze(this).formatHsl()},formatRgb:je,toString:je}),we($e,He,ke(Te,{brighter:function(t){return t=null==t?Ce:Math.pow(Ce,t),new $e(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Ee:Math.pow(Ee,t),new $e(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:We,formatHex:We,formatRgb:Ve,toString:Ve})),we(Qe,(function(t,e,n,r){return 1===arguments.length?Ze(t):new Qe(t,e,n,null==r?1:r)}),ke(Te,{brighter:function(t){return t=null==t?Ce:Math.pow(Ce,t),new Qe(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Ee:Math.pow(Ee,t),new Qe(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new $e(Ke(t>=240?t-240:t+120,i,r),Ke(t,i,r),Ke(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const tn=t=>()=>t;function en(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):tn(isNaN(t)?e:t)}const nn=function t(e){var n=function(t){return 1==(t=+t)?en:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):tn(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=He(t)).r,(e=He(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=en(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function rn(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:he(n,r)})),a=on.lastIndex;return a=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?ce:ue;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var En=Bt.prototype.constructor;function Cn(t){return function(){this.style.removeProperty(t)}}function Sn(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function An(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&Sn(t,a,n)),r}return a._value=e,a}function Mn(t){return function(e){this.textContent=t.call(this,e)}}function Nn(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&Mn(r)),e}return r._value=t,r}var Dn=0;function On(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function Bn(){return++Dn}var Ln=Bt.prototype;On.prototype=function(t){return Bt().transition(t)}.prototype={constructor:On,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=_(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}(this,t)}))},Bt.prototype.transition=function(t){var e,n;t instanceof On?(e=t._id,t=t._name):(e=Bn(),(n=In).time=Qt(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,a=0;a>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?sr(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?sr(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Zn.exec(t))?new lr(e[1],e[2],e[3],1):(e=Qn.exec(t))?new lr(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Kn.exec(t))?sr(e[1],e[2],e[3],e[4]):(e=Jn.exec(t))?sr(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=tr.exec(t))?pr(e[1],e[2]/100,e[3]/100,1):(e=er.exec(t))?pr(e[1],e[2]/100,e[3]/100,e[4]):nr.hasOwnProperty(t)?or(nr[t]):"transparent"===t?new lr(NaN,NaN,NaN,0):null}function or(t){return new lr(t>>16&255,t>>8&255,255&t,1)}function sr(t,e,n,r){return r<=0&&(t=e=n=NaN),new lr(t,e,n,r)}function cr(t){return t instanceof qn||(t=ar(t)),t?new lr((t=t.rgb()).r,t.g,t.b,t.opacity):new lr}function ur(t,e,n,r){return 1===arguments.length?cr(t):new lr(t,e,n,null==r?1:r)}function lr(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function hr(){return"#"+dr(this.r)+dr(this.g)+dr(this.b)}function fr(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function dr(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function pr(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new gr(t,e,n,r)}function yr(t){if(t instanceof gr)return new gr(t.h,t.s,t.l,t.opacity);if(t instanceof qn||(t=ar(t)),!t)return new gr;if(t instanceof gr)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new gr(o,s,c,t.opacity)}function gr(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function mr(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}zn(qn,ar,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:rr,formatHex:rr,formatHsl:function(){return yr(this).formatHsl()},formatRgb:ir,toString:ir}),zn(lr,ur,Un(qn,{brighter:function(t){return t=null==t?$n:Math.pow($n,t),new lr(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Hn:Math.pow(Hn,t),new lr(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:hr,formatHex:hr,formatRgb:fr,toString:fr})),zn(gr,(function(t,e,n,r){return 1===arguments.length?yr(t):new gr(t,e,n,null==r?1:r)}),Un(qn,{brighter:function(t){return t=null==t?$n:Math.pow($n,t),new gr(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Hn:Math.pow(Hn,t),new gr(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new lr(mr(t>=240?t-240:t+120,i,r),mr(t,i,r),mr(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const vr=Math.PI/180,br=180/Math.PI,_r=.96422,xr=.82521,wr=4/29,kr=6/29,Tr=3*kr*kr;function Er(t){if(t instanceof Cr)return new Cr(t.l,t.a,t.b,t.opacity);if(t instanceof Br)return Lr(t);t instanceof lr||(t=cr(t));var e,n,r=Nr(t.r),i=Nr(t.g),a=Nr(t.b),o=Sr((.2225045*r+.7168786*i+.0606169*a)/1);return r===i&&i===a?e=n=o:(e=Sr((.4360747*r+.3850649*i+.1430804*a)/_r),n=Sr((.0139322*r+.0971045*i+.7141733*a)/xr)),new Cr(116*o-16,500*(e-o),200*(o-n),t.opacity)}function Cr(t,e,n,r){this.l=+t,this.a=+e,this.b=+n,this.opacity=+r}function Sr(t){return t>.008856451679035631?Math.pow(t,1/3):t/Tr+wr}function Ar(t){return t>kr?t*t*t:Tr*(t-wr)}function Mr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Nr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Dr(t){if(t instanceof Br)return new Br(t.h,t.c,t.l,t.opacity);if(t instanceof Cr||(t=Er(t)),0===t.a&&0===t.b)return new Br(NaN,0()=>t;function Rr(t,e){return function(n){return t+n*e}}function Fr(t,e){var n=e-t;return n?Rr(t,n):Ir(isNaN(t)?e:t)}function Pr(t){return function(e,n){var r=t((e=Or(e)).h,(n=Or(n)).h),i=Fr(e.c,n.c),a=Fr(e.l,n.l),o=Fr(e.opacity,n.opacity);return function(t){return e.h=r(t),e.c=i(t),e.l=a(t),e.opacity=o(t),e+""}}}const jr=Pr((function(t,e){var n=e-t;return n?Rr(t,n>180||n<-180?n-360*Math.round(n/360):n):Ir(isNaN(t)?e:t)}));Pr(Fr);var Yr=Math.sqrt(50),zr=Math.sqrt(10),Ur=Math.sqrt(2);function qr(t,e,n){var r=(e-t)/Math.max(0,n),i=Math.floor(Math.log(r)/Math.LN10),a=r/Math.pow(10,i);return i>=0?(a>=Yr?10:a>=zr?5:a>=Ur?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(a>=Yr?10:a>=zr?5:a>=Ur?2:1)}function Hr(t,e,n){var r=Math.abs(e-t)/Math.max(0,n),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),a=r/i;return a>=Yr?i*=10:a>=zr?i*=5:a>=Ur&&(i*=2),ee?1:t>=e?0:NaN}function Wr(t){let e=t,n=t,r=t;function i(t,e,i=0,a=t.length){if(i>>1;r(t[n],e)<0?i=n+1:a=n}while(it(e)-n,n=$r,r=(e,n)=>$r(t(e),n)),{left:i,center:function(t,n,r=0,a=t.length){const o=i(t,n,r,a-1);return o>r&&e(t[o-1],n)>-e(t[o],n)?o-1:o},right:function(t,e,i=0,a=t.length){if(i>>1;r(t[n],e)<=0?i=n+1:a=n}while(i>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?gi(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?gi(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=ai.exec(t))?new bi(e[1],e[2],e[3],1):(e=oi.exec(t))?new bi(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=si.exec(t))?gi(e[1],e[2],e[3],e[4]):(e=ci.exec(t))?gi(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=ui.exec(t))?ki(e[1],e[2]/100,e[3]/100,1):(e=li.exec(t))?ki(e[1],e[2]/100,e[3]/100,e[4]):hi.hasOwnProperty(t)?yi(hi[t]):"transparent"===t?new bi(NaN,NaN,NaN,0):null}function yi(t){return new bi(t>>16&255,t>>8&255,255&t,1)}function gi(t,e,n,r){return r<=0&&(t=e=n=NaN),new bi(t,e,n,r)}function mi(t){return t instanceof Kr||(t=pi(t)),t?new bi((t=t.rgb()).r,t.g,t.b,t.opacity):new bi}function vi(t,e,n,r){return 1===arguments.length?mi(t):new bi(t,e,n,null==r?1:r)}function bi(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function _i(){return"#"+wi(this.r)+wi(this.g)+wi(this.b)}function xi(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function wi(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function ki(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Ei(t,e,n,r)}function Ti(t){if(t instanceof Ei)return new Ei(t.h,t.s,t.l,t.opacity);if(t instanceof Kr||(t=pi(t)),!t)return new Ei;if(t instanceof Ei)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new Ei(o,s,c,t.opacity)}function Ei(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function Ci(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function Si(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}Zr(Kr,pi,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:fi,formatHex:fi,formatHsl:function(){return Ti(this).formatHsl()},formatRgb:di,toString:di}),Zr(bi,vi,Qr(Kr,{brighter:function(t){return t=null==t?ti:Math.pow(ti,t),new bi(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Jr:Math.pow(Jr,t),new bi(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:_i,formatHex:_i,formatRgb:xi,toString:xi})),Zr(Ei,(function(t,e,n,r){return 1===arguments.length?Ti(t):new Ei(t,e,n,null==r?1:r)}),Qr(Kr,{brighter:function(t){return t=null==t?ti:Math.pow(ti,t),new Ei(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Jr:Math.pow(Jr,t),new Ei(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new bi(Ci(t>=240?t-240:t+120,i,r),Ci(t,i,r),Ci(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const Ai=t=>()=>t;function Mi(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):Ai(isNaN(t)?e:t)}const Ni=function t(e){var n=function(t){return 1==(t=+t)?Mi:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):Ai(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=vi(t)).r,(e=vi(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=Mi(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function Di(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:Li(n,r)})),a=Fi.lastIndex;return ae&&(n=t,t=e,e=n),u=function(n){return Math.max(t,Math.min(e,n))}),r=c>2?Vi:Wi,i=a=null,h}function h(e){return null==e||isNaN(e=+e)?n:(i||(i=r(o.map(t),s,c)))(t(u(e)))}return h.invert=function(n){return u(e((a||(a=r(s,o.map(t),Li)))(n)))},h.domain=function(t){return arguments.length?(o=Array.from(t,Ui),l()):o.slice()},h.range=function(t){return arguments.length?(s=Array.from(t),l()):s.slice()},h.rangeRound=function(t){return s=Array.from(t),c=zi,l()},h.clamp=function(t){return arguments.length?(u=!!t||Hi,l()):u!==Hi},h.interpolate=function(t){return arguments.length?(c=t,l()):c},h.unknown=function(t){return arguments.length?(n=t,h):n},function(n,r){return t=n,e=r,l()}}()(Hi,Hi)}function Zi(t,e){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(e).domain(t)}return this}var Qi,Ki=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Ji(t){if(!(e=Ki.exec(t)))throw new Error("invalid format: "+t);var e;return new ta({fill:e[1],align:e[2],sign:e[3],symbol:e[4],zero:e[5],width:e[6],comma:e[7],precision:e[8]&&e[8].slice(1),trim:e[9],type:e[10]})}function ta(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function ea(t,e){if((n=(t=e?t.toExponential(e-1):t.toExponential()).indexOf("e"))<0)return null;var n,r=t.slice(0,n);return[r.length>1?r[0]+r.slice(2):r,+t.slice(n+1)]}function na(t){return(t=ea(Math.abs(t)))?t[1]:NaN}function ra(t,e){var n=ea(t,e);if(!n)return t+"";var r=n[0],i=n[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Ji.prototype=ta.prototype,ta.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};const ia={"%":(t,e)=>(100*t).toFixed(e),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,e)=>t.toExponential(e),f:(t,e)=>t.toFixed(e),g:(t,e)=>t.toPrecision(e),o:t=>Math.round(t).toString(8),p:(t,e)=>ra(100*t,e),r:ra,s:function(t,e){var n=ea(t,e);if(!n)return t+"";var r=n[0],i=n[1],a=i-(Qi=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,o=r.length;return a===o?r:a>o?r+new Array(a-o+1).join("0"):a>0?r.slice(0,a)+"."+r.slice(a):"0."+new Array(1-a).join("0")+ea(t,Math.max(0,e+a-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function aa(t){return t}var oa,sa,ca,ua=Array.prototype.map,la=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function ha(t){var e=t.domain;return t.ticks=function(t){var n=e();return function(t,e,n){var r,i,a,o,s=-1;if(n=+n,(t=+t)==(e=+e)&&n>0)return[t];if((r=e0){let n=Math.round(t/o),r=Math.round(e/o);for(n*oe&&--r,a=new Array(i=r-n+1);++se&&--r,a=new Array(i=r-n+1);++s0;){if((i=qr(c,u,n))===r)return a[o]=c,a[s]=u,e(a);if(i>0)c=Math.floor(c/i)*i,u=Math.ceil(u/i)*i;else{if(!(i<0))break;c=Math.ceil(c*i)/i,u=Math.floor(u*i)/i}r=i}return t},t}function fa(){var t=Xi();return t.copy=function(){return Gi(t,fa())},Zi.apply(t,arguments),ha(t)}oa=function(t){var e,n,r=void 0===t.grouping||void 0===t.thousands?aa:(e=ua.call(t.grouping,Number),n=t.thousands+"",function(t,r){for(var i=t.length,a=[],o=0,s=e[0],c=0;i>0&&s>0&&(c+s+1>r&&(s=Math.max(1,r-c)),a.push(t.substring(i-=s,i+s)),!((c+=s+1)>r));)s=e[o=(o+1)%e.length];return a.reverse().join(n)}),i=void 0===t.currency?"":t.currency[0]+"",a=void 0===t.currency?"":t.currency[1]+"",o=void 0===t.decimal?".":t.decimal+"",s=void 0===t.numerals?aa:function(t){return function(e){return e.replace(/[0-9]/g,(function(e){return t[+e]}))}}(ua.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",u=void 0===t.minus?"−":t.minus+"",l=void 0===t.nan?"NaN":t.nan+"";function h(t){var e=(t=Ji(t)).fill,n=t.align,h=t.sign,f=t.symbol,d=t.zero,p=t.width,y=t.comma,g=t.precision,m=t.trim,v=t.type;"n"===v?(y=!0,v="g"):ia[v]||(void 0===g&&(g=12),m=!0,v="g"),(d||"0"===e&&"="===n)&&(d=!0,e="0",n="=");var b="$"===f?i:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",_="$"===f?a:/[%p]/.test(v)?c:"",x=ia[v],w=/[defgprs%]/.test(v);function k(t){var i,a,c,f=b,k=_;if("c"===v)k=x(t)+k,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?l:x(Math.abs(t),g),m&&(t=function(t){t:for(var e,n=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(e+1):t}(t)),T&&0==+t&&"+"!==h&&(T=!1),f=(T?"("===h?h:u:"-"===h||"("===h?"":h)+f,k=("s"===v?la[8+Qi/3]:"")+k+(T&&"("===h?")":""),w)for(i=-1,a=t.length;++i(c=t.charCodeAt(i))||c>57){k=(46===c?o+t.slice(i+1):t.slice(i))+k,t=t.slice(0,i);break}}y&&!d&&(t=r(t,1/0));var E=f.length+t.length+k.length,C=E>1)+f+t+k+C.slice(E);break;default:t=C+f+t+k}return s(t)}return g=void 0===g?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,g)):Math.max(0,Math.min(20,g)),k.toString=function(){return t+""},k}return{format:h,formatPrefix:function(t,e){var n=h(((t=Ji(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(na(e)/3))),i=Math.pow(10,-r),a=la[8+r/3];return function(t){return n(i*t)+a}}}}({thousands:",",grouping:[3],currency:["$",""]}),sa=oa.format,ca=oa.formatPrefix;class da extends Map{constructor(t,e=ya){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:e}}),null!=t)for(const[e,n]of t)this.set(e,n)}get(t){return super.get(pa(this,t))}has(t){return super.has(pa(this,t))}set(t,e){return super.set(function({_intern:t,_key:e},n){const r=e(n);return t.has(r)?t.get(r):(t.set(r,n),n)}(this,t),e)}delete(t){return super.delete(function({_intern:t,_key:e},n){const r=e(n);return t.has(r)&&(n=t.get(r),t.delete(r)),n}(this,t))}}function pa({_intern:t,_key:e},n){const r=e(n);return t.has(r)?t.get(r):n}function ya(t){return null!==t&&"object"==typeof t?t.valueOf():t}Set;const ga=Symbol("implicit");function ma(){var t=new da,e=[],n=[],r=ga;function i(i){let a=t.get(i);if(void 0===a){if(r!==ga)return r;t.set(i,a=e.push(i)-1)}return n[a%n.length]}return i.domain=function(n){if(!arguments.length)return e.slice();e=[],t=new da;for(const r of n)t.has(r)||t.set(r,e.push(r)-1);return i},i.range=function(t){return arguments.length?(n=Array.from(t),i):n.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return ma(e,n).unknown(r)},Zi.apply(i,arguments),i}const va=1e3,ba=6e4,_a=36e5,xa=864e5,wa=6048e5,ka=31536e6;var Ta=new Date,Ea=new Date;function Ca(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=function(e){return t(e=new Date(+e)),e},i.ceil=function(n){return t(n=new Date(n-1)),e(n,1),t(n),n},i.round=function(t){var e=i(t),n=i.ceil(t);return t-e0))return s;do{s.push(o=new Date(+n)),e(n,a),t(n)}while(o=e)for(;t(e),!n(e);)e.setTime(e-1)}),(function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););}))},n&&(i.count=function(e,r){return Ta.setTime(+e),Ea.setTime(+r),t(Ta),t(Ea),Math.floor(n(Ta,Ea))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(e){return r(e)%t==0}:function(e){return i.count(0,e)%t==0}):i:null}),i}var Sa=Ca((function(){}),(function(t,e){t.setTime(+t+e)}),(function(t,e){return e-t}));Sa.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?Ca((function(e){e.setTime(Math.floor(e/t)*t)}),(function(e,n){e.setTime(+e+n*t)}),(function(e,n){return(n-e)/t})):Sa:null};const Aa=Sa;Sa.range;var Ma=Ca((function(t){t.setTime(t-t.getMilliseconds())}),(function(t,e){t.setTime(+t+e*va)}),(function(t,e){return(e-t)/va}),(function(t){return t.getUTCSeconds()}));const Na=Ma;Ma.range;var Da=Ca((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*va)}),(function(t,e){t.setTime(+t+e*ba)}),(function(t,e){return(e-t)/ba}),(function(t){return t.getMinutes()}));const Oa=Da;Da.range;var Ba=Ca((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*va-t.getMinutes()*ba)}),(function(t,e){t.setTime(+t+e*_a)}),(function(t,e){return(e-t)/_a}),(function(t){return t.getHours()}));const La=Ba;Ba.range;var Ia=Ca((t=>t.setHours(0,0,0,0)),((t,e)=>t.setDate(t.getDate()+e)),((t,e)=>(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*ba)/xa),(t=>t.getDate()-1));const Ra=Ia;function Fa(t){return Ca((function(e){e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+7*e)}),(function(t,e){return(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*ba)/wa}))}Ia.range;var Pa=Fa(0),ja=Fa(1),Ya=Fa(2),za=Fa(3),Ua=Fa(4),qa=Fa(5),Ha=Fa(6),$a=(Pa.range,ja.range,Ya.range,za.range,Ua.range,qa.range,Ha.range,Ca((function(t){t.setDate(1),t.setHours(0,0,0,0)}),(function(t,e){t.setMonth(t.getMonth()+e)}),(function(t,e){return e.getMonth()-t.getMonth()+12*(e.getFullYear()-t.getFullYear())}),(function(t){return t.getMonth()})));const Wa=$a;$a.range;var Va=Ca((function(t){t.setMonth(0,1),t.setHours(0,0,0,0)}),(function(t,e){t.setFullYear(t.getFullYear()+e)}),(function(t,e){return e.getFullYear()-t.getFullYear()}),(function(t){return t.getFullYear()}));Va.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Ca((function(e){e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),(function(e,n){e.setFullYear(e.getFullYear()+n*t)})):null};const Ga=Va;Va.range;var Xa=Ca((function(t){t.setUTCSeconds(0,0)}),(function(t,e){t.setTime(+t+e*ba)}),(function(t,e){return(e-t)/ba}),(function(t){return t.getUTCMinutes()}));const Za=Xa;Xa.range;var Qa=Ca((function(t){t.setUTCMinutes(0,0,0)}),(function(t,e){t.setTime(+t+e*_a)}),(function(t,e){return(e-t)/_a}),(function(t){return t.getUTCHours()}));const Ka=Qa;Qa.range;var Ja=Ca((function(t){t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+e)}),(function(t,e){return(e-t)/xa}),(function(t){return t.getUTCDate()-1}));const to=Ja;function eo(t){return Ca((function(e){e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+7*e)}),(function(t,e){return(e-t)/wa}))}Ja.range;var no=eo(0),ro=eo(1),io=eo(2),ao=eo(3),oo=eo(4),so=eo(5),co=eo(6),uo=(no.range,ro.range,io.range,ao.range,oo.range,so.range,co.range,Ca((function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCMonth(t.getUTCMonth()+e)}),(function(t,e){return e.getUTCMonth()-t.getUTCMonth()+12*(e.getUTCFullYear()-t.getUTCFullYear())}),(function(t){return t.getUTCMonth()})));const lo=uo;uo.range;var ho=Ca((function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)}),(function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()}),(function(t){return t.getUTCFullYear()}));ho.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Ca((function(e){e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),(function(e,n){e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null};const fo=ho;function po(t,e,n,r,i,a){const o=[[Na,1,va],[Na,5,5e3],[Na,15,15e3],[Na,30,3e4],[a,1,ba],[a,5,3e5],[a,15,9e5],[a,30,18e5],[i,1,_a],[i,3,108e5],[i,6,216e5],[i,12,432e5],[r,1,xa],[r,2,1728e5],[n,1,wa],[e,1,2592e6],[e,3,7776e6],[t,1,ka]];function s(e,n,r){const i=Math.abs(n-e)/r,a=Wr((([,,t])=>t)).right(o,i);if(a===o.length)return t.every(Hr(e/ka,n/ka,r));if(0===a)return Aa.every(Math.max(Hr(e,n,r),1));const[s,c]=o[i/o[a-1][2][t.toLowerCase(),e])))}function Oo(t,e,n){var r=Eo.exec(e.slice(n,n+1));return r?(t.w=+r[0],n+r[0].length):-1}function Bo(t,e,n){var r=Eo.exec(e.slice(n,n+1));return r?(t.u=+r[0],n+r[0].length):-1}function Lo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.U=+r[0],n+r[0].length):-1}function Io(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.V=+r[0],n+r[0].length):-1}function Ro(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.W=+r[0],n+r[0].length):-1}function Fo(t,e,n){var r=Eo.exec(e.slice(n,n+4));return r?(t.y=+r[0],n+r[0].length):-1}function Po(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function jo(t,e,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function Yo(t,e,n){var r=Eo.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function zo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function Uo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function qo(t,e,n){var r=Eo.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function Ho(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function $o(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function Wo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function Vo(t,e,n){var r=Eo.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function Go(t,e,n){var r=Eo.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function Xo(t,e,n){var r=Co.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function Zo(t,e,n){var r=Eo.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function Qo(t,e,n){var r=Eo.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function Ko(t,e){return Ao(t.getDate(),e,2)}function Jo(t,e){return Ao(t.getHours(),e,2)}function ts(t,e){return Ao(t.getHours()%12||12,e,2)}function es(t,e){return Ao(1+Ra.count(Ga(t),t),e,3)}function ns(t,e){return Ao(t.getMilliseconds(),e,3)}function rs(t,e){return ns(t,e)+"000"}function is(t,e){return Ao(t.getMonth()+1,e,2)}function as(t,e){return Ao(t.getMinutes(),e,2)}function os(t,e){return Ao(t.getSeconds(),e,2)}function ss(t){var e=t.getDay();return 0===e?7:e}function cs(t,e){return Ao(Pa.count(Ga(t)-1,t),e,2)}function us(t){var e=t.getDay();return e>=4||0===e?Ua(t):Ua.ceil(t)}function ls(t,e){return t=us(t),Ao(Ua.count(Ga(t),t)+(4===Ga(t).getDay()),e,2)}function hs(t){return t.getDay()}function fs(t,e){return Ao(ja.count(Ga(t)-1,t),e,2)}function ds(t,e){return Ao(t.getFullYear()%100,e,2)}function ps(t,e){return Ao((t=us(t)).getFullYear()%100,e,2)}function ys(t,e){return Ao(t.getFullYear()%1e4,e,4)}function gs(t,e){var n=t.getDay();return Ao((t=n>=4||0===n?Ua(t):Ua.ceil(t)).getFullYear()%1e4,e,4)}function ms(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+Ao(e/60|0,"0",2)+Ao(e%60,"0",2)}function vs(t,e){return Ao(t.getUTCDate(),e,2)}function bs(t,e){return Ao(t.getUTCHours(),e,2)}function _s(t,e){return Ao(t.getUTCHours()%12||12,e,2)}function xs(t,e){return Ao(1+to.count(fo(t),t),e,3)}function ws(t,e){return Ao(t.getUTCMilliseconds(),e,3)}function ks(t,e){return ws(t,e)+"000"}function Ts(t,e){return Ao(t.getUTCMonth()+1,e,2)}function Es(t,e){return Ao(t.getUTCMinutes(),e,2)}function Cs(t,e){return Ao(t.getUTCSeconds(),e,2)}function Ss(t){var e=t.getUTCDay();return 0===e?7:e}function As(t,e){return Ao(no.count(fo(t)-1,t),e,2)}function Ms(t){var e=t.getUTCDay();return e>=4||0===e?oo(t):oo.ceil(t)}function Ns(t,e){return t=Ms(t),Ao(oo.count(fo(t),t)+(4===fo(t).getUTCDay()),e,2)}function Ds(t){return t.getUTCDay()}function Os(t,e){return Ao(ro.count(fo(t)-1,t),e,2)}function Bs(t,e){return Ao(t.getUTCFullYear()%100,e,2)}function Ls(t,e){return Ao((t=Ms(t)).getUTCFullYear()%100,e,2)}function Is(t,e){return Ao(t.getUTCFullYear()%1e4,e,4)}function Rs(t,e){var n=t.getUTCDay();return Ao((t=n>=4||0===n?oo(t):oo.ceil(t)).getUTCFullYear()%1e4,e,4)}function Fs(){return"+0000"}function Ps(){return"%"}function js(t){return+t}function Ys(t){return Math.floor(+t/1e3)}function zs(t){return new Date(t)}function Us(t){return t instanceof Date?+t:+new Date(+t)}function qs(t,e,n,r,i,a,o,s,c,u){var l=Xi(),h=l.invert,f=l.domain,d=u(".%L"),p=u(":%S"),y=u("%I:%M"),g=u("%I %p"),m=u("%a %d"),v=u("%b %d"),b=u("%B"),_=u("%Y");function x(t){return(c(t)=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:js,s:Ys,S:os,u:ss,U:cs,V:ls,w:hs,W:fs,x:null,X:null,y:ds,Y:ys,Z:ms,"%":Ps},_={a:function(t){return o[t.getUTCDay()]},A:function(t){return a[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:vs,e:vs,f:ks,g:Ls,G:Rs,H:bs,I:_s,j:xs,L:ws,m:Ts,M:Es,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:js,s:Ys,S:Cs,u:Ss,U:As,V:Ns,w:Ds,W:Os,x:null,X:null,y:Bs,Y:Is,Z:Fs,"%":Ps},x={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p.get(r[0].toLowerCase()),n+r[0].length):-1},A:function(t,e,n){var r=h.exec(e.slice(n));return r?(t.w=f.get(r[0].toLowerCase()),n+r[0].length):-1},b:function(t,e,n){var r=m.exec(e.slice(n));return r?(t.m=v.get(r[0].toLowerCase()),n+r[0].length):-1},B:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=g.get(r[0].toLowerCase()),n+r[0].length):-1},c:function(t,n,r){return T(t,e,n,r)},d:Uo,e:Uo,f:Go,g:Po,G:Fo,H:Ho,I:Ho,j:qo,L:Vo,m:zo,M:$o,p:function(t,e,n){var r=u.exec(e.slice(n));return r?(t.p=l.get(r[0].toLowerCase()),n+r[0].length):-1},q:Yo,Q:Zo,s:Qo,S:Wo,u:Bo,U:Lo,V:Io,w:Oo,W:Ro,x:function(t,e,r){return T(t,n,e,r)},X:function(t,e,n){return T(t,r,e,n)},y:Po,Y:Fo,Z:jo,"%":Xo};function w(t,e){return function(n){var r,i,a,o=[],s=-1,c=0,u=t.length;for(n instanceof Date||(n=new Date(+n));++s53)return null;"w"in a||(a.w=1),"Z"in a?(i=(r=_o(xo(a.y,0,1))).getUTCDay(),r=i>4||0===i?ro.ceil(r):ro(r),r=to.offset(r,7*(a.V-1)),a.y=r.getUTCFullYear(),a.m=r.getUTCMonth(),a.d=r.getUTCDate()+(a.w+6)%7):(i=(r=bo(xo(a.y,0,1))).getDay(),r=i>4||0===i?ja.ceil(r):ja(r),r=Ra.offset(r,7*(a.V-1)),a.y=r.getFullYear(),a.m=r.getMonth(),a.d=r.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),i="Z"in a?_o(xo(a.y,0,1)).getUTCDay():bo(xo(a.y,0,1)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(i+5)%7:a.w+7*a.U-(i+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,_o(a)):bo(a)}}function T(t,e,n,r){for(var i,a,o=0,s=e.length,c=n.length;o=c)return-1;if(37===(i=e.charCodeAt(o++))){if(i=e.charAt(o++),!(a=x[i in To?e.charAt(o++):i])||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return b.x=w(n,b),b.X=w(r,b),b.c=w(e,b),_.x=w(n,_),_.X=w(r,_),_.c=w(e,_),{format:function(t){var e=w(t+="",b);return e.toString=function(){return t},e},parse:function(t){var e=k(t+="",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+="",_);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+="",!0);return e.toString=function(){return t},e}}}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),ko=wo.format,wo.parse,wo.utcFormat,wo.utcParse;var Qs=Array.prototype.find;function Ks(){return this.firstElementChild}var Js=Array.prototype.filter;function tc(){return Array.from(this.children)}function ec(t){return new Array(t.length)}function nc(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function rc(t){return function(){return t}}function ic(t,e,n,r,i,a){for(var o,s=0,c=e.length,u=a.length;se?1:t>=e?0:NaN}nc.prototype={constructor:nc,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var uc="http://www.w3.org/1999/xhtml";const lc={svg:"http://www.w3.org/2000/svg",xhtml:uc,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function hc(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),lc.hasOwnProperty(e)?{space:lc[e],local:t}:t}function fc(t){return function(){this.removeAttribute(t)}}function dc(t){return function(){this.removeAttributeNS(t.space,t.local)}}function pc(t,e){return function(){this.setAttribute(t,e)}}function yc(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function gc(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function mc(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function vc(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function bc(t){return function(){this.style.removeProperty(t)}}function _c(t,e,n){return function(){this.style.setProperty(t,e,n)}}function xc(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function wc(t,e){return t.style.getPropertyValue(e)||vc(t).getComputedStyle(t,null).getPropertyValue(e)}function kc(t){return function(){delete this[t]}}function Tc(t,e){return function(){this[t]=e}}function Ec(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function Cc(t){return t.trim().split(/^|\s+/)}function Sc(t){return t.classList||new Ac(t)}function Ac(t){this._node=t,this._names=Cc(t.getAttribute("class")||"")}function Mc(t,e){for(var n=Sc(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function Zc(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var eu=[null];function nu(t,e){this._groups=t,this._parents=e}function ru(){return new nu([[document.documentElement]],eu)}nu.prototype=ru.prototype={constructor:nu,select:function(t){"function"!=typeof t&&(t=$s(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=x&&(x=_+1);!(b=g[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=cc);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?bc:"function"==typeof e?xc:_c)(t,e,null==n?"":n)):wc(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?kc:"function"==typeof e?Ec:Tc)(t,e)):this.node()[t]},classed:function(t,e){var n=Cc(t+"");if(arguments.length<2){for(var r=Sc(this.node()),i=-1,a=n.length;++iuu)if(Math.abs(l*s-c*u)>uu&&i){var f=n-a,d=r-o,p=s*s+c*c,y=f*f+d*d,g=Math.sqrt(p),m=Math.sqrt(h),v=i*Math.tan((su-Math.acos((p+h-y)/(2*g*m)))/2),b=v/m,_=v/g;Math.abs(b-1)>uu&&(this._+="L"+(t+b*u)+","+(e+b*l)),this._+="A"+i+","+i+",0,0,"+ +(l*f>u*d)+","+(this._x1=t+_*s)+","+(this._y1=e+_*c)}else this._+="L"+(this._x1=t)+","+(this._y1=e)},arc:function(t,e,n,r,i,a){t=+t,e=+e,a=!!a;var o=(n=+n)*Math.cos(r),s=n*Math.sin(r),c=t+o,u=e+s,l=1^a,h=a?r-i:i-r;if(n<0)throw new Error("negative radius: "+n);null===this._x1?this._+="M"+c+","+u:(Math.abs(this._x1-c)>uu||Math.abs(this._y1-u)>uu)&&(this._+="L"+c+","+u),n&&(h<0&&(h=h%cu+cu),h>lu?this._+="A"+n+","+n+",0,1,"+l+","+(t-o)+","+(e-s)+"A"+n+","+n+",0,1,"+l+","+(this._x1=c)+","+(this._y1=u):h>uu&&(this._+="A"+n+","+n+",0,"+ +(h>=su)+","+l+","+(this._x1=t+n*Math.cos(i))+","+(this._y1=e+n*Math.sin(i))))},rect:function(t,e,n,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+e)+"h"+ +n+"v"+ +r+"h"+-n+"Z"},toString:function(){return this._}};const du=fu;function pu(t){return function(){return t}}var yu=Math.abs,gu=Math.atan2,mu=Math.cos,vu=Math.max,bu=Math.min,_u=Math.sin,xu=Math.sqrt,wu=1e-12,ku=Math.PI,Tu=ku/2,Eu=2*ku;function Cu(t){return t>1?0:t<-1?ku:Math.acos(t)}function Su(t){return t>=1?Tu:t<=-1?-Tu:Math.asin(t)}function Au(t){return t.innerRadius}function Mu(t){return t.outerRadius}function Nu(t){return t.startAngle}function Du(t){return t.endAngle}function Ou(t){return t&&t.padAngle}function Bu(t,e,n,r,i,a,o,s){var c=n-t,u=r-e,l=o-i,h=s-a,f=h*c-l*u;if(!(f*fN*N+D*D&&(T=C,E=S),{cx:T,cy:E,x01:-l,y01:-h,x11:T*(i/x-1),y11:E*(i/x-1)}}function Iu(){var t=Au,e=Mu,n=pu(0),r=null,i=Nu,a=Du,o=Ou,s=null;function c(){var c,u,l=+t.apply(this,arguments),h=+e.apply(this,arguments),f=i.apply(this,arguments)-Tu,d=a.apply(this,arguments)-Tu,p=yu(d-f),y=d>f;if(s||(s=c=du()),hwu)if(p>Eu-wu)s.moveTo(h*mu(f),h*_u(f)),s.arc(0,0,h,f,d,!y),l>wu&&(s.moveTo(l*mu(d),l*_u(d)),s.arc(0,0,l,d,f,y));else{var g,m,v=f,b=d,_=f,x=d,w=p,k=p,T=o.apply(this,arguments)/2,E=T>wu&&(r?+r.apply(this,arguments):xu(l*l+h*h)),C=bu(yu(h-l)/2,+n.apply(this,arguments)),S=C,A=C;if(E>wu){var M=Su(E/l*_u(T)),N=Su(E/h*_u(T));(w-=2*M)>wu?(_+=M*=y?1:-1,x-=M):(w=0,_=x=(f+d)/2),(k-=2*N)>wu?(v+=N*=y?1:-1,b-=N):(k=0,v=b=(f+d)/2)}var D=h*mu(v),O=h*_u(v),B=l*mu(x),L=l*_u(x);if(C>wu){var I,R=h*mu(b),F=h*_u(b),P=l*mu(_),j=l*_u(_);if(pwu?A>wu?(g=Lu(P,j,D,O,h,A,y),m=Lu(R,F,B,L,h,A,y),s.moveTo(g.cx+g.x01,g.cy+g.y01),Awu&&w>wu?S>wu?(g=Lu(B,L,R,F,l,-S,y),m=Lu(D,O,P,j,l,-S,y),s.lineTo(g.cx+g.x01,g.cy+g.y01),St?1:e>=t?0:NaN}function qu(t){return t}function Hu(){}function $u(t,e,n){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+e)/6,(t._y0+4*t._y1+n)/6)}function Wu(t){this._context=t}function Vu(t){return new Wu(t)}function Gu(t){this._context=t}function Xu(t){this._context=t}function Zu(t){this._context=t}function Qu(t){return t<0?-1:1}function Ku(t,e,n){var r=t._x1-t._x0,i=e-t._x1,a=(t._y1-t._y0)/(r||i<0&&-0),o=(n-t._y1)/(i||r<0&&-0),s=(a*i+o*r)/(r+i);return(Qu(a)+Qu(o))*Math.min(Math.abs(a),Math.abs(o),.5*Math.abs(s))||0}function Ju(t,e){var n=t._x1-t._x0;return n?(3*(t._y1-t._y0)/n-e)/2:e}function tl(t,e,n){var r=t._x0,i=t._y0,a=t._x1,o=t._y1,s=(a-r)/3;t._context.bezierCurveTo(r+s,i+s*e,a-s,o-s*n,a,o)}function el(t){this._context=t}function nl(t){this._context=new rl(t)}function rl(t){this._context=t}function il(t){this._context=t}function al(t){var e,n,r=t.length-1,i=new Array(r),a=new Array(r),o=new Array(r);for(i[0]=0,a[0]=2,o[0]=t[0]+2*t[1],e=1;e=0;--e)i[e]=(o[e]-i[e+1])/a[e];for(a[r-1]=(t[r]+i[r-1])/2,e=0;e=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,e),this._context.lineTo(t,e);else{var n=this._x*(1-this._t)+t*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,e)}}this._x=t,this._y=e}};var sl=new Date,cl=new Date;function ul(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=function(e){return t(e=new Date(+e)),e},i.ceil=function(n){return t(n=new Date(n-1)),e(n,1),t(n),n},i.round=function(t){var e=i(t),n=i.ceil(t);return t-e0))return s;do{s.push(o=new Date(+n)),e(n,a),t(n)}while(o=e)for(;t(e),!n(e);)e.setTime(e-1)}),(function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););}))},n&&(i.count=function(e,r){return sl.setTime(+e),cl.setTime(+r),t(sl),t(cl),Math.floor(n(sl,cl))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(e){return r(e)%t==0}:function(e){return i.count(0,e)%t==0}):i:null}),i}const ll=864e5,hl=6048e5;function fl(t){return ul((function(e){e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+7*e)}),(function(t,e){return(e-t)/hl}))}var dl=fl(0),pl=fl(1),yl=fl(2),gl=fl(3),ml=fl(4),vl=fl(5),bl=fl(6),_l=(dl.range,pl.range,yl.range,gl.range,ml.range,vl.range,bl.range,ul((function(t){t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+e)}),(function(t,e){return(e-t)/ll}),(function(t){return t.getUTCDate()-1})));const xl=_l;function wl(t){return ul((function(e){e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+7*e)}),(function(t,e){return(e-t-6e4*(e.getTimezoneOffset()-t.getTimezoneOffset()))/hl}))}_l.range;var kl=wl(0),Tl=wl(1),El=wl(2),Cl=wl(3),Sl=wl(4),Al=wl(5),Ml=wl(6),Nl=(kl.range,Tl.range,El.range,Cl.range,Sl.range,Al.range,Ml.range,ul((t=>t.setHours(0,0,0,0)),((t,e)=>t.setDate(t.getDate()+e)),((t,e)=>(e-t-6e4*(e.getTimezoneOffset()-t.getTimezoneOffset()))/ll),(t=>t.getDate()-1)));const Dl=Nl;Nl.range;var Ol=ul((function(t){t.setMonth(0,1),t.setHours(0,0,0,0)}),(function(t,e){t.setFullYear(t.getFullYear()+e)}),(function(t,e){return e.getFullYear()-t.getFullYear()}),(function(t){return t.getFullYear()}));Ol.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ul((function(e){e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),(function(e,n){e.setFullYear(e.getFullYear()+n*t)})):null};const Bl=Ol;Ol.range;var Ll=ul((function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)}),(function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()}),(function(t){return t.getUTCFullYear()}));Ll.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ul((function(e){e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),(function(e,n){e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null};const Il=Ll;function Rl(t){if(0<=t.y&&t.y<100){var e=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Fl(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Pl(t,e,n){return{y:t,m:e,d:n,H:0,M:0,S:0,L:0}}Ll.range;var jl,Yl,zl={"-":"",_:" ",0:"0"},Ul=/^\s*\d+/,ql=/^%/,Hl=/[\\^$*+?|[\]().{}]/g;function $l(t,e,n){var r=t<0?"-":"",i=(r?-t:t)+"",a=i.length;return r+(a[t.toLowerCase(),e])))}function Xl(t,e,n){var r=Ul.exec(e.slice(n,n+1));return r?(t.w=+r[0],n+r[0].length):-1}function Zl(t,e,n){var r=Ul.exec(e.slice(n,n+1));return r?(t.u=+r[0],n+r[0].length):-1}function Ql(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.U=+r[0],n+r[0].length):-1}function Kl(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.V=+r[0],n+r[0].length):-1}function Jl(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.W=+r[0],n+r[0].length):-1}function th(t,e,n){var r=Ul.exec(e.slice(n,n+4));return r?(t.y=+r[0],n+r[0].length):-1}function eh(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function nh(t,e,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function rh(t,e,n){var r=Ul.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function ih(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function ah(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function oh(t,e,n){var r=Ul.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function sh(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function ch(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function uh(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function lh(t,e,n){var r=Ul.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function hh(t,e,n){var r=Ul.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function fh(t,e,n){var r=ql.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function dh(t,e,n){var r=Ul.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function ph(t,e,n){var r=Ul.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function yh(t,e){return $l(t.getDate(),e,2)}function gh(t,e){return $l(t.getHours(),e,2)}function mh(t,e){return $l(t.getHours()%12||12,e,2)}function vh(t,e){return $l(1+Dl.count(Bl(t),t),e,3)}function bh(t,e){return $l(t.getMilliseconds(),e,3)}function _h(t,e){return bh(t,e)+"000"}function xh(t,e){return $l(t.getMonth()+1,e,2)}function wh(t,e){return $l(t.getMinutes(),e,2)}function kh(t,e){return $l(t.getSeconds(),e,2)}function Th(t){var e=t.getDay();return 0===e?7:e}function Eh(t,e){return $l(kl.count(Bl(t)-1,t),e,2)}function Ch(t){var e=t.getDay();return e>=4||0===e?Sl(t):Sl.ceil(t)}function Sh(t,e){return t=Ch(t),$l(Sl.count(Bl(t),t)+(4===Bl(t).getDay()),e,2)}function Ah(t){return t.getDay()}function Mh(t,e){return $l(Tl.count(Bl(t)-1,t),e,2)}function Nh(t,e){return $l(t.getFullYear()%100,e,2)}function Dh(t,e){return $l((t=Ch(t)).getFullYear()%100,e,2)}function Oh(t,e){return $l(t.getFullYear()%1e4,e,4)}function Bh(t,e){var n=t.getDay();return $l((t=n>=4||0===n?Sl(t):Sl.ceil(t)).getFullYear()%1e4,e,4)}function Lh(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+$l(e/60|0,"0",2)+$l(e%60,"0",2)}function Ih(t,e){return $l(t.getUTCDate(),e,2)}function Rh(t,e){return $l(t.getUTCHours(),e,2)}function Fh(t,e){return $l(t.getUTCHours()%12||12,e,2)}function Ph(t,e){return $l(1+xl.count(Il(t),t),e,3)}function jh(t,e){return $l(t.getUTCMilliseconds(),e,3)}function Yh(t,e){return jh(t,e)+"000"}function zh(t,e){return $l(t.getUTCMonth()+1,e,2)}function Uh(t,e){return $l(t.getUTCMinutes(),e,2)}function qh(t,e){return $l(t.getUTCSeconds(),e,2)}function Hh(t){var e=t.getUTCDay();return 0===e?7:e}function $h(t,e){return $l(dl.count(Il(t)-1,t),e,2)}function Wh(t){var e=t.getUTCDay();return e>=4||0===e?ml(t):ml.ceil(t)}function Vh(t,e){return t=Wh(t),$l(ml.count(Il(t),t)+(4===Il(t).getUTCDay()),e,2)}function Gh(t){return t.getUTCDay()}function Xh(t,e){return $l(pl.count(Il(t)-1,t),e,2)}function Zh(t,e){return $l(t.getUTCFullYear()%100,e,2)}function Qh(t,e){return $l((t=Wh(t)).getUTCFullYear()%100,e,2)}function Kh(t,e){return $l(t.getUTCFullYear()%1e4,e,4)}function Jh(t,e){var n=t.getUTCDay();return $l((t=n>=4||0===n?ml(t):ml.ceil(t)).getUTCFullYear()%1e4,e,4)}function tf(){return"+0000"}function ef(){return"%"}function nf(t){return+t}function rf(t){return Math.floor(+t/1e3)}jl=function(t){var e=t.dateTime,n=t.date,r=t.time,i=t.periods,a=t.days,o=t.shortDays,s=t.months,c=t.shortMonths,u=Vl(i),l=Gl(i),h=Vl(a),f=Gl(a),d=Vl(o),p=Gl(o),y=Vl(s),g=Gl(s),m=Vl(c),v=Gl(c),b={a:function(t){return o[t.getDay()]},A:function(t){return a[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:yh,e:yh,f:_h,g:Dh,G:Bh,H:gh,I:mh,j:vh,L:bh,m:xh,M:wh,p:function(t){return i[+(t.getHours()>=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:nf,s:rf,S:kh,u:Th,U:Eh,V:Sh,w:Ah,W:Mh,x:null,X:null,y:Nh,Y:Oh,Z:Lh,"%":ef},_={a:function(t){return o[t.getUTCDay()]},A:function(t){return a[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:Ih,e:Ih,f:Yh,g:Qh,G:Jh,H:Rh,I:Fh,j:Ph,L:jh,m:zh,M:Uh,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:nf,s:rf,S:qh,u:Hh,U:$h,V:Vh,w:Gh,W:Xh,x:null,X:null,y:Zh,Y:Kh,Z:tf,"%":ef},x={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p.get(r[0].toLowerCase()),n+r[0].length):-1},A:function(t,e,n){var r=h.exec(e.slice(n));return r?(t.w=f.get(r[0].toLowerCase()),n+r[0].length):-1},b:function(t,e,n){var r=m.exec(e.slice(n));return r?(t.m=v.get(r[0].toLowerCase()),n+r[0].length):-1},B:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=g.get(r[0].toLowerCase()),n+r[0].length):-1},c:function(t,n,r){return T(t,e,n,r)},d:ah,e:ah,f:hh,g:eh,G:th,H:sh,I:sh,j:oh,L:lh,m:ih,M:ch,p:function(t,e,n){var r=u.exec(e.slice(n));return r?(t.p=l.get(r[0].toLowerCase()),n+r[0].length):-1},q:rh,Q:dh,s:ph,S:uh,u:Zl,U:Ql,V:Kl,w:Xl,W:Jl,x:function(t,e,r){return T(t,n,e,r)},X:function(t,e,n){return T(t,r,e,n)},y:eh,Y:th,Z:nh,"%":fh};function w(t,e){return function(n){var r,i,a,o=[],s=-1,c=0,u=t.length;for(n instanceof Date||(n=new Date(+n));++s53)return null;"w"in a||(a.w=1),"Z"in a?(i=(r=Fl(Pl(a.y,0,1))).getUTCDay(),r=i>4||0===i?pl.ceil(r):pl(r),r=xl.offset(r,7*(a.V-1)),a.y=r.getUTCFullYear(),a.m=r.getUTCMonth(),a.d=r.getUTCDate()+(a.w+6)%7):(i=(r=Rl(Pl(a.y,0,1))).getDay(),r=i>4||0===i?Tl.ceil(r):Tl(r),r=Dl.offset(r,7*(a.V-1)),a.y=r.getFullYear(),a.m=r.getMonth(),a.d=r.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),i="Z"in a?Fl(Pl(a.y,0,1)).getUTCDay():Rl(Pl(a.y,0,1)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(i+5)%7:a.w+7*a.U-(i+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,Fl(a)):Rl(a)}}function T(t,e,n,r){for(var i,a,o=0,s=e.length,c=n.length;o=c)return-1;if(37===(i=e.charCodeAt(o++))){if(i=e.charAt(o++),!(a=x[i in zl?e.charAt(o++):i])||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return b.x=w(n,b),b.X=w(r,b),b.c=w(e,b),_.x=w(n,_),_.X=w(r,_),_.c=w(e,_),{format:function(t){var e=w(t+="",b);return e.toString=function(){return t},e},parse:function(t){var e=k(t+="",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+="",_);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+="",!0);return e.toString=function(){return t},e}}}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),Yl=jl.format,jl.parse,jl.utcFormat,jl.utcParse;var af={value:()=>{}};function of(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function uf(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--pf}()}finally{pf=0,function(){for(var t,e,n=ff,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:ff=e);df=t,Af(r)}(),vf=0}}function Sf(){var t=_f.now(),e=t-mf;e>1e3&&(bf-=e,mf=t)}function Af(t){pf||(yf&&(yf=clearTimeout(yf)),t-vf>24?(t<1/0&&(yf=setTimeout(Cf,t-_f.now()-bf)),gf&&(gf=clearInterval(gf))):(gf||(mf=_f.now(),gf=setInterval(Sf,1e3)),pf=1,xf(Cf)))}function Mf(t,e,n){var r=new Tf;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}Tf.prototype=Ef.prototype={constructor:Tf,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?wf():+n)+(null==e?0:+e),this._next||df===this||(df?df._next=this:ff=this,df=this),this._call=t,this._time=n,Af()},stop:function(){this._call&&(this._call=null,this._time=1/0,Af())}};var Nf=hf("start","end","cancel","interrupt"),Df=[];function Of(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return Mf(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function Lf(t,e){var n=If(t,e);if(n.state>3)throw new Error("too late; already running");return n}function If(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function Rf(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var Ff,Pf=180/Math.PI,jf={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function Yf(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:Rf(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:Rf(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:Rf(t,n)},{i:s-2,x:Rf(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:Rf(n,r)})),a=Qf.lastIndex;return a=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?Bf:Lf;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var gd=iu.prototype.constructor;function md(t){return function(){this.style.removeProperty(t)}}function vd(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function bd(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&vd(t,a,n)),r}return a._value=e,a}function _d(t){return function(e){this.textContent=t.call(this,e)}}function xd(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&_d(r)),e}return r._value=t,r}var wd=0;function kd(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function Td(){return++wd}var Ed=iu.prototype;kd.prototype=function(t){return iu().transition(t)}.prototype={constructor:kd,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=$s(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}(this,t)}))},iu.prototype.transition=function(t){var e,n;t instanceof kd?(e=t._id,t=t._name):(e=Td(),(n=Cd).time=wf(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,a=0;ae?1:t>=e?0:NaN}Yd.prototype={constructor:Yd,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var Vd="http://www.w3.org/1999/xhtml";const Gd={svg:"http://www.w3.org/2000/svg",xhtml:Vd,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function Xd(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),Gd.hasOwnProperty(e)?{space:Gd[e],local:t}:t}function Zd(t){return function(){this.removeAttribute(t)}}function Qd(t){return function(){this.removeAttributeNS(t.space,t.local)}}function Kd(t,e){return function(){this.setAttribute(t,e)}}function Jd(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function tp(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function ep(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function np(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function rp(t){return function(){this.style.removeProperty(t)}}function ip(t,e,n){return function(){this.style.setProperty(t,e,n)}}function ap(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function op(t,e){return t.style.getPropertyValue(e)||np(t).getComputedStyle(t,null).getPropertyValue(e)}function sp(t){return function(){delete this[t]}}function cp(t,e){return function(){this[t]=e}}function up(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function lp(t){return t.trim().split(/^|\s+/)}function hp(t){return t.classList||new fp(t)}function fp(t){this._node=t,this._names=lp(t.getAttribute("class")||"")}function dp(t,e){for(var n=hp(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function Lp(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var jp=[null];function Yp(t,e){this._groups=t,this._parents=e}function zp(){return new Yp([[document.documentElement]],jp)}Yp.prototype=zp.prototype={constructor:Yp,select:function(t){"function"!=typeof t&&(t=Md(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=x&&(x=_+1);!(b=g[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=Wd);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?rp:"function"==typeof e?ap:ip)(t,e,null==n?"":n)):op(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?sp:"function"==typeof e?up:cp)(t,e)):this.node()[t]},classed:function(t,e){var n=lp(t+"");if(arguments.length<2){for(var r=hp(this.node()),i=-1,a=n.length;++i{}};function Hp(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function Vp(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--Kp}()}finally{Kp=0,function(){for(var t,e,n=Zp,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:Zp=e);Qp=t,fy(r)}(),ny=0}}function hy(){var t=iy.now(),e=t-ey;e>1e3&&(ry-=e,ey=t)}function fy(t){Kp||(Jp&&(Jp=clearTimeout(Jp)),t-ny>24?(t<1/0&&(Jp=setTimeout(ly,t-iy.now()-ry)),ty&&(ty=clearInterval(ty))):(ty||(ey=iy.now(),ty=setInterval(hy,1e3)),Kp=1,ay(ly)))}function dy(t,e,n){var r=new cy;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}cy.prototype=uy.prototype={constructor:cy,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?oy():+n)+(null==e?0:+e),this._next||Qp===this||(Qp?Qp._next=this:Zp=this,Qp=this),this._call=t,this._time=n,fy()},stop:function(){this._call&&(this._call=null,this._time=1/0,fy())}};var py=Xp("start","end","cancel","interrupt"),yy=[];function gy(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return dy(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function vy(t,e){var n=by(t,e);if(n.state>3)throw new Error("too late; already running");return n}function by(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function _y(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var xy,wy=180/Math.PI,ky={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function Ty(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:_y(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:_y(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:_y(t,n)},{i:s-2,x:_y(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?Qy(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?Qy(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Yy.exec(t))?new tg(e[1],e[2],e[3],1):(e=zy.exec(t))?new tg(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Uy.exec(t))?Qy(e[1],e[2],e[3],e[4]):(e=qy.exec(t))?Qy(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Hy.exec(t))?ig(e[1],e[2]/100,e[3]/100,1):(e=$y.exec(t))?ig(e[1],e[2]/100,e[3]/100,e[4]):Wy.hasOwnProperty(t)?Zy(Wy[t]):"transparent"===t?new tg(NaN,NaN,NaN,0):null}function Zy(t){return new tg(t>>16&255,t>>8&255,255&t,1)}function Qy(t,e,n,r){return r<=0&&(t=e=n=NaN),new tg(t,e,n,r)}function Ky(t){return t instanceof By||(t=Xy(t)),t?new tg((t=t.rgb()).r,t.g,t.b,t.opacity):new tg}function Jy(t,e,n,r){return 1===arguments.length?Ky(t):new tg(t,e,n,null==r?1:r)}function tg(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function eg(){return"#"+rg(this.r)+rg(this.g)+rg(this.b)}function ng(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function rg(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function ig(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new og(t,e,n,r)}function ag(t){if(t instanceof og)return new og(t.h,t.s,t.l,t.opacity);if(t instanceof By||(t=Xy(t)),!t)return new og;if(t instanceof og)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new og(o,s,c,t.opacity)}function og(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function sg(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function cg(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}Dy(By,Xy,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:Vy,formatHex:Vy,formatHsl:function(){return ag(this).formatHsl()},formatRgb:Gy,toString:Gy}),Dy(tg,Jy,Oy(By,{brighter:function(t){return t=null==t?Iy:Math.pow(Iy,t),new tg(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Ly:Math.pow(Ly,t),new tg(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:eg,formatHex:eg,formatRgb:ng,toString:ng})),Dy(og,(function(t,e,n,r){return 1===arguments.length?ag(t):new og(t,e,n,null==r?1:r)}),Oy(By,{brighter:function(t){return t=null==t?Iy:Math.pow(Iy,t),new og(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Ly:Math.pow(Ly,t),new og(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new tg(sg(t>=240?t-240:t+120,i,r),sg(t,i,r),sg(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const ug=t=>()=>t;function lg(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):ug(isNaN(t)?e:t)}const hg=function t(e){var n=function(t){return 1==(t=+t)?lg:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):ug(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=Jy(t)).r,(e=Jy(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=lg(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function fg(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:_y(n,r)})),a=pg.lastIndex;return a=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?my:vy;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var Bg=Up.prototype.constructor;function Lg(t){return function(){this.style.removeProperty(t)}}function Ig(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function Rg(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&Ig(t,a,n)),r}return a._value=e,a}function Fg(t){return function(e){this.textContent=t.call(this,e)}}function Pg(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&Fg(r)),e}return r._value=t,r}var jg=0;function Yg(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function zg(){return++jg}var Ug=Up.prototype;Yg.prototype=function(t){return Up().transition(t)}.prototype={constructor:Yg,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=Md(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}(this,t)}))},Up.prototype.transition=function(t){var e,n;t instanceof Yg?(e=t._id,t=t._name):(e=zg(),(n=qg).time=oy(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,a=0;a0?tm(fm,--lm):0,cm--,10===hm&&(cm=1,sm--),hm}function ym(){return hm=lm2||bm(hm)>3?"":" "}function wm(t,e){for(;--e&&ym()&&!(hm<48||hm>102||hm>57&&hm<65||hm>70&&hm<97););return vm(t,mm()+(e<6&&32==gm()&&32==ym()))}function km(t){for(;ym();)switch(hm){case t:return lm;case 34:case 39:34!==t&&39!==t&&km(hm);break;case 40:41===t&&km(t);break;case 92:ym()}return lm}function Tm(t,e){for(;ym()&&t+hm!==57&&(t+hm!==84||47!==gm()););return"/*"+vm(e,lm-1)+"*"+Zg(47===t?t:ym())}function Em(t){for(;!bm(gm());)ym();return vm(t,lm)}function Cm(t){return function(t){return fm="",t}(Sm("",null,null,null,[""],t=function(t){return sm=cm=1,um=nm(fm=t),lm=0,[]}(t),0,[0],t))}function Sm(t,e,n,r,i,a,o,s,c){for(var u=0,l=0,h=o,f=0,d=0,p=0,y=1,g=1,m=1,v=0,b="",_=i,x=a,w=r,k=b;g;)switch(p=v,v=ym()){case 40:if(108!=p&&58==k.charCodeAt(h-1)){-1!=Jg(k+=Kg(_m(v),"&","&\f"),"&\f")&&(m=-1);break}case 34:case 39:case 91:k+=_m(v);break;case 9:case 10:case 13:case 32:k+=xm(p);break;case 92:k+=wm(mm()-1,7);continue;case 47:switch(gm()){case 42:case 47:im(Mm(Tm(ym(),mm()),e,n),c);break;default:k+="/"}break;case 123*y:s[u++]=nm(k)*m;case 125*y:case 59:case 0:switch(v){case 0:case 125:g=0;case 59+l:d>0&&nm(k)-h&&im(d>32?Nm(k+";",r,n,h-1):Nm(Kg(k," ","")+";",r,n,h-2),c);break;case 59:k+=";";default:if(im(w=Am(k,e,n,u,l,i,s,b,_=[],x=[],h),a),123===v)if(0===l)Sm(k,e,w,w,_,a,h,s,x);else switch(f){case 100:case 109:case 115:Sm(t,w,w,r&&im(Am(t,w,w,0,0,i,s,b,i,_=[],h),x),i,x,h,s,r?_:x);break;default:Sm(k,w,w,w,[""],x,0,s,x)}}u=l=d=0,y=m=1,b=k="",h=o;break;case 58:h=1+nm(k),d=p;default:if(y<1)if(123==v)--y;else if(125==v&&0==y++&&125==pm())continue;switch(k+=Zg(v),v*y){case 38:m=l>0?1:(k+="\f",-1);break;case 44:s[u++]=(nm(k)-1)*m,m=1;break;case 64:45===gm()&&(k+=_m(ym())),f=gm(),l=h=nm(b=k+=Em(mm())),v++;break;case 45:45===p&&2==nm(k)&&(y=0)}}return a}function Am(t,e,n,r,i,a,o,s,c,u,l){for(var h=i-1,f=0===i?a:[""],d=rm(f),p=0,y=0,g=0;p0?f[m]+" "+v:Kg(v,/&\f/g,f[m])))&&(c[g++]=b);return dm(t,e,n,0===i?Vg:s,c,u,l)}function Mm(t,e,n){return dm(t,e,n,Wg,Zg(hm),em(t,2,-2),0)}function Nm(t,e,n,r){return dm(t,e,n,Gg,em(t,0,r),em(t,r+1,-1),r)}const Dm="8.14.0";var Om=n(9609),Bm=n(7856),Lm=n.n(Bm),Im=function(t){var e=t.replace(/\\u[\dA-F]{4}/gi,(function(t){return String.fromCharCode(parseInt(t.replace(/\\u/g,""),16))}));return e=(e=(e=e.replace(/\\x([0-9a-f]{2})/gi,(function(t,e){return String.fromCharCode(parseInt(e,16))}))).replace(/\\[\d\d\d]{3}/gi,(function(t){return String.fromCharCode(parseInt(t.replace(/\\/g,""),8))}))).replace(/\\[\d\d\d]{2}/gi,(function(t){return String.fromCharCode(parseInt(t.replace(/\\/g,""),8))}))},Rm=function(t){for(var e="",n=0;n>=0;){if(!((n=t.indexOf("=0)){e+=t,n=-1;break}e+=t.substr(0,n),(n=(t=t.substr(n+1)).indexOf("<\/script>"))>=0&&(n+=9,t=t.substr(n))}var r=Im(e);return(r=(r=(r=r.replace(/script>/gi,"#")).replace(/javascript:/gi,"#")).replace(/onerror=/gi,"onerror:")).replace(/')}if(void 0!==n)switch(y){case"flowchart":case"flowchart-v2":n(T,gx.bindFunctions);break;case"gantt":n(T,_w.bindFunctions);break;case"class":case"classDiagram":n(T,bb.bindFunctions);break;default:n(T)}else o.debug("CB = undefined!");nT.forEach((function(t){t()})),nT=[];var S="sandbox"===s.securityLevel?"#i"+t:"#d"+t,A=au(S).node();return null!==A&&"function"==typeof A.remove&&au(S).node().remove(),T},parse:function(t){var e=Jv(),n=Hv.detectInit(t,e);n&&o.debug("reinit ",n);var r,i=Hv.detectType(t,e);switch(o.debug("Type "+i),i){case"git":(r=zw()).parser.yy=Pw;break;case"flowchart":case"flowchart-v2":gx.clear(),(r=vx()).parser.yy=gx;break;case"sequence":(r=Bk()).parser.yy=eT;break;case"gantt":(r=Tw()).parser.yy=_w;break;case"class":case"classDiagram":(r=Eb()).parser.yy=bb;break;case"state":case"stateDiagram":(r=VT()).parser.yy=cE;break;case"info":o.debug("info info info"),(r=nk()).parser.yy=tk;break;case"pie":o.debug("pie"),(r=ok()).parser.yy=lk;break;case"er":o.debug("er"),(r=R_()).parser.yy=L_;break;case"journey":o.debug("Journey"),(r=RE()).parser.yy=LE;break;case"requirement":case"requirementDiagram":o.debug("RequirementDiagram"),(r=yk()).parser.yy=xk}return r.parser.yy.graphType=i,r.parser.yy.parseError=function(t,e){throw{str:t,hash:e}},r.parse(t),r},parseDirective:function(t,e,n,r){try{if(void 0!==e)switch(e=e.trim(),n){case"open_directive":oC={};break;case"type_directive":oC.type=e.toLowerCase();break;case"arg_directive":oC.args=JSON.parse(e);break;case"close_directive":(function(t,e,n){switch(o.debug("Directive type=".concat(e.type," with args:"),e.args),e.type){case"init":case"initialize":["config"].forEach((function(t){void 0!==e.args[t]&&("flowchart-v2"===n&&(n="flowchart"),e.args[n]=e.args[t],delete e.args[t])})),o.debug("sanitize in handleDirective",e.args),Uv(e.args),o.debug("sanitize in handleDirective (done)",e.args),e.args,eb(e.args);break;case"wrap":case"nowrap":t&&t.setWrap&&t.setWrap("wrap"===e.type);break;case"themeCss":o.warn("themeCss encountered");break;default:o.warn("Unhandled directive: source: '%%{".concat(e.type,": ").concat(JSON.stringify(e.args?e.args:{}),"}%%"),e)}})(t,oC,r),oC=null}}catch(t){o.error("Error while rendering sequenceDiagram directive: ".concat(e," jison context: ").concat(n)),o.error(t.message)}},initialize:function(t){t&&t.fontFamily&&(t.themeVariables&&t.themeVariables.fontFamily||(t.themeVariables={fontFamily:t.fontFamily})),function(t){Wv=Lv({},t)}(t),t&&t.theme&&ov[t.theme]?t.themeVariables=ov[t.theme].getThemeVariables(t.themeVariables):t&&(t.themeVariables=ov.default.getThemeVariables(t.themeVariables));var e="object"===iC(t)?function(t){return Gv=Lv({},Vv),Gv=Lv(Gv,t),t.theme&&(Gv.themeVariables=ov[t.theme].getThemeVariables(t.themeVariables)),Zv=Qv(Gv,Xv),Gv}(t):Kv();sC(e),s(e.logLevel)},reinitialize:function(){},getConfig:Jv,setConfig:function(t){return Lv(Zv,t),Jv()},getSiteConfig:Kv,updateSiteConfig:function(t){return Gv=Lv(Gv,t),Qv(Gv,Xv),Gv},reset:function(){nb()},globalReset:function(){nb(),sC(Jv())},defaultConfig:Vv});s(Jv().logLevel),nb(Jv());const uC=cC;var lC=function(){hC.startOnLoad?uC.getConfig().startOnLoad&&hC.init():void 0===hC.startOnLoad&&(o.debug("In start, no config"),uC.getConfig().startOnLoad&&hC.init())};"undefined"!=typeof document&&window.addEventListener("load",(function(){lC()}),!1);var hC={startOnLoad:!0,htmlLabels:!0,mermaidAPI:uC,parse:uC.parse,render:uC.render,init:function(){var t,e,n=this,r=uC.getConfig();arguments.length>=2?(void 0!==arguments[0]&&(hC.sequenceConfig=arguments[0]),t=arguments[1]):t=arguments[0],"function"==typeof arguments[arguments.length-1]?(e=arguments[arguments.length-1],o.debug("Callback function found")):void 0!==r.mermaid&&("function"==typeof r.mermaid.callback?(e=r.mermaid.callback,o.debug("Callback function found")):o.debug("No Callback function found")),t=void 0===t?document.querySelectorAll(".mermaid"):"string"==typeof t?document.querySelectorAll(t):t instanceof window.Node?[t]:t,o.debug("Start On Load before: "+hC.startOnLoad),void 0!==hC.startOnLoad&&(o.debug("Start On Load inner: "+hC.startOnLoad),uC.updateSiteConfig({startOnLoad:hC.startOnLoad})),void 0!==hC.ganttConfig&&uC.updateSiteConfig({gantt:hC.ganttConfig});for(var i,a=new Hv.initIdGeneratior(r.deterministicIds,r.deterministicIDSeed),s=function(r){var s=t[r];if(s.getAttribute("data-processed"))return"continue";s.setAttribute("data-processed",!0);var c="mermaid-".concat(a.next());i=s.innerHTML,i=Hv.entityDecode(i).trim().replace(//gi,"
");var u=Hv.detectInit(i);u&&o.debug("Detected early reinit: ",u);try{uC.render(c,i,(function(t,n){s.innerHTML=t,void 0!==e&&e(c),n&&n(s)}),s)}catch(t){o.warn("Syntax Error rendering"),o.warn(t),n.parseError&&n.parseError(t)}},c=0;c{t.exports={graphlib:n(6614),dagre:n(1463),intersect:n(8114),render:n(5787),util:n(8355),version:n(5689)}},9144:(t,e,n)=>{var r=n(8355);function i(t,e,n,i){var a=t.append("marker").attr("id",e).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerUnits","strokeWidth").attr("markerWidth",8).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").style("stroke-width",1).style("stroke-dasharray","1,0");r.applyStyle(a,n[i+"Style"]),n[i+"Class"]&&a.attr("class",n[i+"Class"])}t.exports={default:i,normal:i,vee:function(t,e,n,i){var a=t.append("marker").attr("id",e).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerUnits","strokeWidth").attr("markerWidth",8).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 L 4 5 z").style("stroke-width",1).style("stroke-dasharray","1,0");r.applyStyle(a,n[i+"Style"]),n[i+"Class"]&&a.attr("class",n[i+"Class"])},undirected:function(t,e,n,i){var a=t.append("marker").attr("id",e).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerUnits","strokeWidth").attr("markerWidth",8).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 0 5 L 10 5").style("stroke-width",1).style("stroke-dasharray","1,0");r.applyStyle(a,n[i+"Style"]),n[i+"Class"]&&a.attr("class",n[i+"Class"])}}},5632:(t,e,n)=>{var r=n(8355),i=n(4322),a=n(1322);t.exports=function(t,e){var n,o=e.nodes().filter((function(t){return r.isSubgraph(e,t)})),s=t.selectAll("g.cluster").data(o,(function(t){return t}));return s.selectAll("*").remove(),s.enter().append("g").attr("class","cluster").attr("id",(function(t){return e.node(t).id})).style("opacity",0),s=t.selectAll("g.cluster"),r.applyTransition(s,e).style("opacity",1),s.each((function(t){var n=e.node(t),r=i.select(this);i.select(this).append("rect");var o=r.append("g").attr("class","label");a(o,n,n.clusterLabelPos)})),s.selectAll("rect").each((function(t){var n=e.node(t),a=i.select(this);r.applyStyle(a,n.style)})),n=s.exit?s.exit():s.selectAll(null),r.applyTransition(n,e).style("opacity",0).remove(),s}},6315:(t,e,n)=>{"use strict";var r=n(1034),i=n(1322),a=n(8355),o=n(4322);t.exports=function(t,e){var n,s=t.selectAll("g.edgeLabel").data(e.edges(),(function(t){return a.edgeToId(t)})).classed("update",!0);return s.exit().remove(),s.enter().append("g").classed("edgeLabel",!0).style("opacity",0),(s=t.selectAll("g.edgeLabel")).each((function(t){var n=o.select(this);n.select(".label").remove();var a=e.edge(t),s=i(n,e.edge(t),0,0).classed("label",!0),c=s.node().getBBox();a.labelId&&s.attr("id",a.labelId),r.has(a,"width")||(a.width=c.width),r.has(a,"height")||(a.height=c.height)})),n=s.exit?s.exit():s.selectAll(null),a.applyTransition(n,e).style("opacity",0).remove(),s}},940:(t,e,n)=>{"use strict";var r=n(1034),i=n(7584),a=n(8355),o=n(4322);function s(t,e){var n=(o.line||o.svg.line)().x((function(t){return t.x})).y((function(t){return t.y}));return(n.curve||n.interpolate)(t.curve),n(e)}t.exports=function(t,e,n){var c=t.selectAll("g.edgePath").data(e.edges(),(function(t){return a.edgeToId(t)})).classed("update",!0),u=function(t,e){var n=t.enter().append("g").attr("class","edgePath").style("opacity",0);return n.append("path").attr("class","path").attr("d",(function(t){var n=e.edge(t),i=e.node(t.v).elem;return s(n,r.range(n.points.length).map((function(){return e=(t=i).getBBox(),{x:(n=t.ownerSVGElement.getScreenCTM().inverse().multiply(t.getScreenCTM()).translate(e.width/2,e.height/2)).e,y:n.f};var t,e,n})))})),n.append("defs"),n}(c,e);!function(t,e){var n=t.exit();a.applyTransition(n,e).style("opacity",0).remove()}(c,e);var l=void 0!==c.merge?c.merge(u):c;return a.applyTransition(l,e).style("opacity",1),l.each((function(t){var n=o.select(this),r=e.edge(t);r.elem=this,r.id&&n.attr("id",r.id),a.applyClass(n,r.class,(n.classed("update")?"update ":"")+"edgePath")})),l.selectAll("path.path").each((function(t){var n=e.edge(t);n.arrowheadId=r.uniqueId("arrowhead");var c=o.select(this).attr("marker-end",(function(){return"url("https://github.com/+(t=location.href,e=n.arrowheadId,t.split(%22#")[0]+"#"+e+")");var t,e})).style("fill","none");a.applyTransition(c,e).attr("d",(function(t){return function(t,e){var n=t.edge(e),r=t.node(e.v),a=t.node(e.w),o=n.points.slice(1,n.points.length-1);return o.unshift(i(r,o[0])),o.push(i(a,o[o.length-1])),s(n,o)}(e,t)})),a.applyStyle(c,n.style)})),l.selectAll("defs *").remove(),l.selectAll("defs").each((function(t){var r=e.edge(t);(0,n[r.arrowhead])(o.select(this),r.arrowheadId,r,"arrowhead")})),l}},607:(t,e,n)=>{"use strict";var r=n(1034),i=n(1322),a=n(8355),o=n(4322);t.exports=function(t,e,n){var s,c=e.nodes().filter((function(t){return!a.isSubgraph(e,t)})),u=t.selectAll("g.node").data(c,(function(t){return t})).classed("update",!0);return u.exit().remove(),u.enter().append("g").attr("class","node").style("opacity",0),(u=t.selectAll("g.node")).each((function(t){var s=e.node(t),c=o.select(this);a.applyClass(c,s.class,(c.classed("update")?"update ":"")+"node"),c.select("g.label").remove();var u=c.append("g").attr("class","label"),l=i(u,s),h=n[s.shape],f=r.pick(l.node().getBBox(),"width","height");s.elem=this,s.id&&c.attr("id",s.id),s.labelId&&u.attr("id",s.labelId),r.has(s,"width")&&(f.width=s.width),r.has(s,"height")&&(f.height=s.height),f.width+=s.paddingLeft+s.paddingRight,f.height+=s.paddingTop+s.paddingBottom,u.attr("transform","translate("+(s.paddingLeft-s.paddingRight)/2+","+(s.paddingTop-s.paddingBottom)/2+")");var d=o.select(this);d.select(".label-container").remove();var p=h(d,f,s).classed("label-container",!0);a.applyStyle(p,s.style);var y=p.node().getBBox();s.width=y.width,s.height=y.height})),s=u.exit?u.exit():u.selectAll(null),a.applyTransition(s,e).style("opacity",0).remove(),u}},4322:(t,e,n)=>{var r;if(!r)try{r=n(7188)}catch(t){}r||(r=window.d3),t.exports=r},1463:(t,e,n)=>{var r;try{r=n(681)}catch(t){}r||(r=window.dagre),t.exports=r},6614:(t,e,n)=>{var r;try{r=n(8282)}catch(t){}r||(r=window.graphlib),t.exports=r},8114:(t,e,n)=>{t.exports={node:n(7584),circle:n(6587),ellipse:n(3260),polygon:n(5337),rect:n(8049)}},6587:(t,e,n)=>{var r=n(3260);t.exports=function(t,e,n){return r(t,e,e,n)}},3260:t=>{t.exports=function(t,e,n,r){var i=t.x,a=t.y,o=i-r.x,s=a-r.y,c=Math.sqrt(e*e*s*s+n*n*o*o),u=Math.abs(e*n*o/c);r.x{function e(t,e){return t*e>0}t.exports=function(t,n,r,i){var a,o,s,c,u,l,h,f,d,p,y,g,m;if(!(a=n.y-t.y,s=t.x-n.x,u=n.x*t.y-t.x*n.y,d=a*r.x+s*r.y+u,p=a*i.x+s*i.y+u,0!==d&&0!==p&&e(d,p)||(o=i.y-r.y,c=r.x-i.x,l=i.x*r.y-r.x*i.y,h=o*t.x+c*t.y+l,f=o*n.x+c*n.y+l,0!==h&&0!==f&&e(h,f)||0==(y=a*c-o*s))))return g=Math.abs(y/2),{x:(m=s*l-c*u)<0?(m-g)/y:(m+g)/y,y:(m=o*u-a*l)<0?(m-g)/y:(m+g)/y}}},7584:t=>{t.exports=function(t,e){return t.intersect(e)}},5337:(t,e,n)=>{var r=n(6808);t.exports=function(t,e,n){var i=t.x,a=t.y,o=[],s=Number.POSITIVE_INFINITY,c=Number.POSITIVE_INFINITY;e.forEach((function(t){s=Math.min(s,t.x),c=Math.min(c,t.y)}));for(var u=i-t.width/2-s,l=a-t.height/2-c,h=0;h1&&o.sort((function(t,e){var r=t.x-n.x,i=t.y-n.y,a=Math.sqrt(r*r+i*i),o=e.x-n.x,s=e.y-n.y,c=Math.sqrt(o*o+s*s);return a{t.exports=function(t,e){var n,r,i=t.x,a=t.y,o=e.x-i,s=e.y-a,c=t.width/2,u=t.height/2;return Math.abs(s)*c>Math.abs(o)*u?(s<0&&(u=-u),n=0===s?0:u*o/s,r=u):(o<0&&(c=-c),n=c,r=0===o?0:c*s/o),{x:i+n,y:a+r}}},8284:(t,e,n)=>{var r=n(8355);t.exports=function(t,e){var n=t.append("foreignObject").attr("width","100000"),i=n.append("xhtml:div");i.attr("xmlns","http://www.w3.org/1999/xhtml");var a=e.label;switch(typeof a){case"function":i.insert(a);break;case"object":i.insert((function(){return a}));break;default:i.html(a)}r.applyStyle(i,e.labelStyle),i.style("display","inline-block"),i.style("white-space","nowrap");var o=i.node().getBoundingClientRect();return n.attr("width",o.width).attr("height",o.height),n}},1322:(t,e,n)=>{var r=n(7318),i=n(8284),a=n(8287);t.exports=function(t,e,n){var o=e.label,s=t.append("g");"svg"===e.labelType?a(s,e):"string"!=typeof o||"html"===e.labelType?i(s,e):r(s,e);var c,u=s.node().getBBox();switch(n){case"top":c=-e.height/2;break;case"bottom":c=e.height/2-u.height;break;default:c=-u.height/2}return s.attr("transform","translate("+-u.width/2+","+c+")"),s}},8287:(t,e,n)=>{var r=n(8355);t.exports=function(t,e){var n=t;return n.node().appendChild(e.label),r.applyStyle(n,e.labelStyle),n}},7318:(t,e,n)=>{var r=n(8355);t.exports=function(t,e){for(var n=t.append("text"),i=function(t){for(var e,n="",r=!1,i=0;i{var r;try{r={defaults:n(1747),each:n(6073),isFunction:n(3560),isPlainObject:n(8630),pick:n(9722),has:n(8721),range:n(6026),uniqueId:n(3955)}}catch(t){}r||(r=window._),t.exports=r},6381:(t,e,n)=>{"use strict";var r=n(8355),i=n(4322);t.exports=function(t,e){var n=t.filter((function(){return!i.select(this).classed("update")}));function a(t){var n=e.node(t);return"translate("+n.x+","+n.y+")"}n.attr("transform",a),r.applyTransition(t,e).style("opacity",1).attr("transform",a),r.applyTransition(n.selectAll("rect"),e).attr("width",(function(t){return e.node(t).width})).attr("height",(function(t){return e.node(t).height})).attr("x",(function(t){return-e.node(t).width/2})).attr("y",(function(t){return-e.node(t).height/2}))}},4577:(t,e,n)=>{"use strict";var r=n(8355),i=n(4322),a=n(1034);t.exports=function(t,e){function n(t){var n=e.edge(t);return a.has(n,"x")?"translate("+n.x+","+n.y+")":""}t.filter((function(){return!i.select(this).classed("update")})).attr("transform",n),r.applyTransition(t,e).style("opacity",1).attr("transform",n)}},4849:(t,e,n)=>{"use strict";var r=n(8355),i=n(4322);t.exports=function(t,e){function n(t){var n=e.node(t);return"translate("+n.x+","+n.y+")"}t.filter((function(){return!i.select(this).classed("update")})).attr("transform",n),r.applyTransition(t,e).style("opacity",1).attr("transform",n)}},5787:(t,e,n)=>{var r=n(1034),i=n(4322),a=n(1463).layout;t.exports=function(){var t=n(607),e=n(5632),i=n(6315),u=n(940),l=n(4849),h=n(4577),f=n(6381),d=n(4418),p=n(9144),y=function(n,y){!function(t){t.nodes().forEach((function(e){var n=t.node(e);r.has(n,"label")||t.children(e).length||(n.label=e),r.has(n,"paddingX")&&r.defaults(n,{paddingLeft:n.paddingX,paddingRight:n.paddingX}),r.has(n,"paddingY")&&r.defaults(n,{paddingTop:n.paddingY,paddingBottom:n.paddingY}),r.has(n,"padding")&&r.defaults(n,{paddingLeft:n.padding,paddingRight:n.padding,paddingTop:n.padding,paddingBottom:n.padding}),r.defaults(n,o),r.each(["paddingLeft","paddingRight","paddingTop","paddingBottom"],(function(t){n[t]=Number(n[t])})),r.has(n,"width")&&(n._prevWidth=n.width),r.has(n,"height")&&(n._prevHeight=n.height)})),t.edges().forEach((function(e){var n=t.edge(e);r.has(n,"label")||(n.label=""),r.defaults(n,s)}))}(y);var g=c(n,"output"),m=c(g,"clusters"),v=c(g,"edgePaths"),b=i(c(g,"edgeLabels"),y),_=t(c(g,"nodes"),y,d);a(y),l(_,y),h(b,y),u(v,y,p);var x=e(m,y);f(x,y),function(t){r.each(t.nodes(),(function(e){var n=t.node(e);r.has(n,"_prevWidth")?n.width=n._prevWidth:delete n.width,r.has(n,"_prevHeight")?n.height=n._prevHeight:delete n.height,delete n._prevWidth,delete n._prevHeight}))}(y)};return y.createNodes=function(e){return arguments.length?(t=e,y):t},y.createClusters=function(t){return arguments.length?(e=t,y):e},y.createEdgeLabels=function(t){return arguments.length?(i=t,y):i},y.createEdgePaths=function(t){return arguments.length?(u=t,y):u},y.shapes=function(t){return arguments.length?(d=t,y):d},y.arrows=function(t){return arguments.length?(p=t,y):p},y};var o={paddingLeft:10,paddingRight:10,paddingTop:10,paddingBottom:10,rx:0,ry:0,shape:"rect"},s={arrowhead:"normal",curve:i.curveLinear};function c(t,e){var n=t.select("g."+e);return n.empty()&&(n=t.append("g").attr("class",e)),n}},4418:(t,e,n)=>{"use strict";var r=n(8049),i=n(3260),a=n(6587),o=n(5337);t.exports={rect:function(t,e,n){var i=t.insert("rect",":first-child").attr("rx",n.rx).attr("ry",n.ry).attr("x",-e.width/2).attr("y",-e.height/2).attr("width",e.width).attr("height",e.height);return n.intersect=function(t){return r(n,t)},i},ellipse:function(t,e,n){var r=e.width/2,a=e.height/2,o=t.insert("ellipse",":first-child").attr("x",-e.width/2).attr("y",-e.height/2).attr("rx",r).attr("ry",a);return n.intersect=function(t){return i(n,r,a,t)},o},circle:function(t,e,n){var r=Math.max(e.width,e.height)/2,i=t.insert("circle",":first-child").attr("x",-e.width/2).attr("y",-e.height/2).attr("r",r);return n.intersect=function(t){return a(n,r,t)},i},diamond:function(t,e,n){var r=e.width*Math.SQRT2/2,i=e.height*Math.SQRT2/2,a=[{x:0,y:-i},{x:-r,y:0},{x:0,y:i},{x:r,y:0}],s=t.insert("polygon",":first-child").attr("points",a.map((function(t){return t.x+","+t.y})).join(" "));return n.intersect=function(t){return o(n,a,t)},s}}},8355:(t,e,n)=>{var r=n(1034);t.exports={isSubgraph:function(t,e){return!!t.children(e).length},edgeToId:function(t){return a(t.v)+":"+a(t.w)+":"+a(t.name)},applyStyle:function(t,e){e&&t.attr("style",e)},applyClass:function(t,e,n){e&&t.attr("class",e).attr("class",n+" "+t.attr("class"))},applyTransition:function(t,e){var n=e.graph();if(r.isPlainObject(n)){var i=n.transition;if(r.isFunction(i))return i(t)}return t}};var i=/:/g;function a(t){return t?String(t).replace(i,"\\:"):""}},5689:t=>{t.exports="0.6.4"},7188:(t,e,n)=>{"use strict";n.r(e),n.d(e,{FormatSpecifier:()=>uc,active:()=>Jr,arc:()=>fx,area:()=>vx,areaRadial:()=>Sx,ascending:()=>i,autoType:()=>Fo,axisBottom:()=>it,axisLeft:()=>at,axisRight:()=>rt,axisTop:()=>nt,bisect:()=>u,bisectLeft:()=>c,bisectRight:()=>s,bisector:()=>a,blob:()=>ms,brush:()=>Ai,brushSelection:()=>Ei,brushX:()=>Ci,brushY:()=>Si,buffer:()=>bs,chord:()=>Fi,clientPoint:()=>Dn,cluster:()=>Sd,color:()=>Ve,contourDensity:()=>oo,contours:()=>to,create:()=>j_,creator:()=>ie,cross:()=>f,csv:()=>Ts,csvFormat:()=>To,csvFormatBody:()=>Eo,csvFormatRow:()=>So,csvFormatRows:()=>Co,csvFormatValue:()=>Ao,csvParse:()=>wo,csvParseRows:()=>ko,cubehelix:()=>Ha,curveBasis:()=>sw,curveBasisClosed:()=>uw,curveBasisOpen:()=>hw,curveBundle:()=>dw,curveCardinal:()=>gw,curveCardinalClosed:()=>vw,curveCardinalOpen:()=>_w,curveCatmullRom:()=>kw,curveCatmullRomClosed:()=>Ew,curveCatmullRomOpen:()=>Sw,curveLinear:()=>px,curveLinearClosed:()=>Mw,curveMonotoneX:()=>Fw,curveMonotoneY:()=>Pw,curveNatural:()=>zw,curveStep:()=>qw,curveStepAfter:()=>$w,curveStepBefore:()=>Hw,customEvent:()=>ge,descending:()=>d,deviation:()=>g,dispatch:()=>ft,drag:()=>po,dragDisable:()=>Se,dragEnable:()=>Ae,dsv:()=>ks,dsvFormat:()=>_o,easeBack:()=>hs,easeBackIn:()=>us,easeBackInOut:()=>hs,easeBackOut:()=>ls,easeBounce:()=>os,easeBounceIn:()=>as,easeBounceInOut:()=>ss,easeBounceOut:()=>os,easeCircle:()=>rs,easeCircleIn:()=>es,easeCircleInOut:()=>rs,easeCircleOut:()=>ns,easeCubic:()=>Xr,easeCubicIn:()=>Vr,easeCubicInOut:()=>Xr,easeCubicOut:()=>Gr,easeElastic:()=>ps,easeElasticIn:()=>ds,easeElasticInOut:()=>ys,easeElasticOut:()=>ps,easeExp:()=>ts,easeExpIn:()=>Ko,easeExpInOut:()=>ts,easeExpOut:()=>Jo,easeLinear:()=>jo,easePoly:()=>$o,easePolyIn:()=>qo,easePolyInOut:()=>$o,easePolyOut:()=>Ho,easeQuad:()=>Uo,easeQuadIn:()=>Yo,easeQuadInOut:()=>Uo,easeQuadOut:()=>zo,easeSin:()=>Zo,easeSinIn:()=>Go,easeSinInOut:()=>Zo,easeSinOut:()=>Xo,entries:()=>pa,event:()=>le,extent:()=>m,forceCenter:()=>Bs,forceCollide:()=>Ws,forceLink:()=>Xs,forceManyBody:()=>tc,forceRadial:()=>ec,forceSimulation:()=>Js,forceX:()=>nc,forceY:()=>rc,format:()=>pc,formatDefaultLocale:()=>bc,formatLocale:()=>vc,formatPrefix:()=>yc,formatSpecifier:()=>cc,geoAlbers:()=>Uf,geoAlbersUsa:()=>qf,geoArea:()=>yu,geoAzimuthalEqualArea:()=>Vf,geoAzimuthalEqualAreaRaw:()=>Wf,geoAzimuthalEquidistant:()=>Xf,geoAzimuthalEquidistantRaw:()=>Gf,geoBounds:()=>sl,geoCentroid:()=>bl,geoCircle:()=>Nl,geoClipAntimeridian:()=>Ul,geoClipCircle:()=>ql,geoClipExtent:()=>Vl,geoClipRectangle:()=>Wl,geoConicConformal:()=>ed,geoConicConformalRaw:()=>td,geoConicEqualArea:()=>zf,geoConicEqualAreaRaw:()=>Yf,geoConicEquidistant:()=>ad,geoConicEquidistantRaw:()=>id,geoContains:()=>ph,geoDistance:()=>ah,geoEqualEarth:()=>fd,geoEqualEarthRaw:()=>hd,geoEquirectangular:()=>rd,geoEquirectangularRaw:()=>nd,geoGnomonic:()=>pd,geoGnomonicRaw:()=>dd,geoGraticule:()=>mh,geoGraticule10:()=>vh,geoIdentity:()=>yd,geoInterpolate:()=>bh,geoLength:()=>nh,geoMercator:()=>Qf,geoMercatorRaw:()=>Zf,geoNaturalEarth1:()=>md,geoNaturalEarth1Raw:()=>gd,geoOrthographic:()=>bd,geoOrthographicRaw:()=>vd,geoPath:()=>kf,geoProjection:()=>Ff,geoProjectionMutator:()=>Pf,geoRotation:()=>Sl,geoStereographic:()=>xd,geoStereographicRaw:()=>_d,geoStream:()=>nu,geoTransform:()=>Tf,geoTransverseMercator:()=>kd,geoTransverseMercatorRaw:()=>wd,gray:()=>ka,hcl:()=>Oa,hierarchy:()=>Md,histogram:()=>D,hsl:()=>an,html:()=>Ds,image:()=>Cs,interpolate:()=>Mn,interpolateArray:()=>xn,interpolateBasis:()=>un,interpolateBasisClosed:()=>ln,interpolateBlues:()=>f_,interpolateBrBG:()=>Tb,interpolateBuGn:()=>Ub,interpolateBuPu:()=>Hb,interpolateCividis:()=>k_,interpolateCool:()=>C_,interpolateCubehelix:()=>zp,interpolateCubehelixDefault:()=>T_,interpolateCubehelixLong:()=>Up,interpolateDate:()=>kn,interpolateDiscrete:()=>Sp,interpolateGnBu:()=>Wb,interpolateGreens:()=>p_,interpolateGreys:()=>g_,interpolateHcl:()=>Pp,interpolateHclLong:()=>jp,interpolateHsl:()=>Lp,interpolateHslLong:()=>Ip,interpolateHue:()=>Ap,interpolateInferno:()=>F_,interpolateLab:()=>Rp,interpolateMagma:()=>R_,interpolateNumber:()=>Tn,interpolateNumberArray:()=>bn,interpolateObject:()=>En,interpolateOrRd:()=>Gb,interpolateOranges:()=>w_,interpolatePRGn:()=>Cb,interpolatePiYG:()=>Ab,interpolatePlasma:()=>P_,interpolatePuBu:()=>Kb,interpolatePuBuGn:()=>Zb,interpolatePuOr:()=>Nb,interpolatePuRd:()=>t_,interpolatePurples:()=>v_,interpolateRainbow:()=>A_,interpolateRdBu:()=>Ob,interpolateRdGy:()=>Lb,interpolateRdPu:()=>n_,interpolateRdYlBu:()=>Rb,interpolateRdYlGn:()=>Pb,interpolateReds:()=>__,interpolateRgb:()=>yn,interpolateRgbBasis:()=>mn,interpolateRgbBasisClosed:()=>vn,interpolateRound:()=>Mp,interpolateSinebow:()=>O_,interpolateSpectral:()=>Yb,interpolateString:()=>An,interpolateTransformCss:()=>pr,interpolateTransformSvg:()=>yr,interpolateTurbo:()=>B_,interpolateViridis:()=>I_,interpolateWarm:()=>E_,interpolateYlGn:()=>o_,interpolateYlGnBu:()=>i_,interpolateYlOrBr:()=>c_,interpolateYlOrRd:()=>l_,interpolateZoom:()=>Op,interrupt:()=>ar,interval:()=>fk,isoFormat:()=>uk,isoParse:()=>hk,json:()=>As,keys:()=>fa,lab:()=>Ta,lch:()=>Da,line:()=>mx,lineRadial:()=>Cx,linkHorizontal:()=>Rx,linkRadial:()=>Px,linkVertical:()=>Fx,local:()=>z_,map:()=>na,matcher:()=>mt,max:()=>I,mean:()=>R,median:()=>F,merge:()=>P,min:()=>j,mouse:()=>Bn,namespace:()=>Et,namespaces:()=>Tt,nest:()=>ra,now:()=>Hn,pack:()=>tp,packEnclose:()=>Id,packSiblings:()=>Gd,pairs:()=>l,partition:()=>op,path:()=>Wi,permute:()=>Y,pie:()=>xx,piecewise:()=>qp,pointRadial:()=>Ax,polygonArea:()=>$p,polygonCentroid:()=>Wp,polygonContains:()=>Qp,polygonHull:()=>Zp,polygonLength:()=>Kp,precisionFixed:()=>_c,precisionPrefix:()=>xc,precisionRound:()=>wc,quadtree:()=>Ys,quantile:()=>O,quantize:()=>Hp,radialArea:()=>Sx,radialLine:()=>Cx,randomBates:()=>iy,randomExponential:()=>ay,randomIrwinHall:()=>ry,randomLogNormal:()=>ny,randomNormal:()=>ey,randomUniform:()=>ty,range:()=>k,rgb:()=>Qe,ribbon:()=>Ki,scaleBand:()=>dy,scaleDiverging:()=>ob,scaleDivergingLog:()=>sb,scaleDivergingPow:()=>ub,scaleDivergingSqrt:()=>lb,scaleDivergingSymlog:()=>cb,scaleIdentity:()=>My,scaleImplicit:()=>hy,scaleLinear:()=>Ay,scaleLog:()=>Py,scaleOrdinal:()=>fy,scalePoint:()=>yy,scalePow:()=>Vy,scaleQuantile:()=>Xy,scaleQuantize:()=>Zy,scaleSequential:()=>Jv,scaleSequentialLog:()=>tb,scaleSequentialPow:()=>nb,scaleSequentialQuantile:()=>ib,scaleSequentialSqrt:()=>rb,scaleSequentialSymlog:()=>eb,scaleSqrt:()=>Gy,scaleSymlog:()=>Uy,scaleThreshold:()=>Qy,scaleTime:()=>Yv,scaleUtc:()=>Zv,scan:()=>z,schemeAccent:()=>db,schemeBlues:()=>h_,schemeBrBG:()=>kb,schemeBuGn:()=>zb,schemeBuPu:()=>qb,schemeCategory10:()=>fb,schemeDark2:()=>pb,schemeGnBu:()=>$b,schemeGreens:()=>d_,schemeGreys:()=>y_,schemeOrRd:()=>Vb,schemeOranges:()=>x_,schemePRGn:()=>Eb,schemePaired:()=>yb,schemePastel1:()=>gb,schemePastel2:()=>mb,schemePiYG:()=>Sb,schemePuBu:()=>Qb,schemePuBuGn:()=>Xb,schemePuOr:()=>Mb,schemePuRd:()=>Jb,schemePurples:()=>m_,schemeRdBu:()=>Db,schemeRdGy:()=>Bb,schemeRdPu:()=>e_,schemeRdYlBu:()=>Ib,schemeRdYlGn:()=>Fb,schemeReds:()=>b_,schemeSet1:()=>vb,schemeSet2:()=>bb,schemeSet3:()=>_b,schemeSpectral:()=>jb,schemeTableau10:()=>xb,schemeYlGn:()=>a_,schemeYlGnBu:()=>r_,schemeYlOrBr:()=>s_,schemeYlOrRd:()=>u_,select:()=>Te,selectAll:()=>q_,selection:()=>ke,selector:()=>pt,selectorAll:()=>gt,set:()=>ha,shuffle:()=>U,stack:()=>Xw,stackOffsetDiverging:()=>Qw,stackOffsetExpand:()=>Zw,stackOffsetNone:()=>Ww,stackOffsetSilhouette:()=>Kw,stackOffsetWiggle:()=>Jw,stackOrderAppearance:()=>tk,stackOrderAscending:()=>nk,stackOrderDescending:()=>ik,stackOrderInsideOut:()=>ak,stackOrderNone:()=>Vw,stackOrderReverse:()=>ok,stratify:()=>hp,style:()=>Rt,sum:()=>q,svg:()=>Os,symbol:()=>rw,symbolCircle:()=>jx,symbolCross:()=>Yx,symbolDiamond:()=>qx,symbolSquare:()=>Gx,symbolStar:()=>Vx,symbolTriangle:()=>Zx,symbolWye:()=>ew,symbols:()=>nw,text:()=>xs,thresholdFreedmanDiaconis:()=>B,thresholdScott:()=>L,thresholdSturges:()=>N,tickFormat:()=>Cy,tickIncrement:()=>A,tickStep:()=>M,ticks:()=>S,timeDay:()=>Ag,timeDays:()=>Mg,timeFormat:()=>pm,timeFormatDefaultLocale:()=>Iv,timeFormatLocale:()=>fm,timeFriday:()=>vg,timeFridays:()=>Eg,timeHour:()=>Dg,timeHours:()=>Og,timeInterval:()=>tg,timeMillisecond:()=>Yg,timeMilliseconds:()=>zg,timeMinute:()=>Lg,timeMinutes:()=>Ig,timeMonday:()=>pg,timeMondays:()=>xg,timeMonth:()=>ag,timeMonths:()=>og,timeParse:()=>ym,timeSaturday:()=>bg,timeSaturdays:()=>Cg,timeSecond:()=>Fg,timeSeconds:()=>Pg,timeSunday:()=>dg,timeSundays:()=>_g,timeThursday:()=>mg,timeThursdays:()=>Tg,timeTuesday:()=>yg,timeTuesdays:()=>wg,timeWednesday:()=>gg,timeWednesdays:()=>kg,timeWeek:()=>dg,timeWeeks:()=>_g,timeYear:()=>ng,timeYears:()=>rg,timeout:()=>Kn,timer:()=>Vn,timerFlush:()=>Gn,touch:()=>On,touches:()=>H_,transition:()=>Hr,transpose:()=>H,tree:()=>vp,treemap:()=>kp,treemapBinary:()=>Tp,treemapDice:()=>ap,treemapResquarify:()=>Cp,treemapSlice:()=>bp,treemapSliceDice:()=>Ep,treemapSquarify:()=>wp,tsv:()=>Es,tsvFormat:()=>Oo,tsvFormatBody:()=>Bo,tsvFormatRow:()=>Io,tsvFormatRows:()=>Lo,tsvFormatValue:()=>Ro,tsvParse:()=>No,tsvParseRows:()=>Do,utcDay:()=>im,utcDays:()=>am,utcFormat:()=>gm,utcFriday:()=>Gg,utcFridays:()=>em,utcHour:()=>$v,utcHours:()=>Wv,utcMillisecond:()=>Yg,utcMilliseconds:()=>zg,utcMinute:()=>Gv,utcMinutes:()=>Xv,utcMonday:()=>Hg,utcMondays:()=>Qg,utcMonth:()=>Uv,utcMonths:()=>qv,utcParse:()=>mm,utcSaturday:()=>Xg,utcSaturdays:()=>nm,utcSecond:()=>Fg,utcSeconds:()=>Pg,utcSunday:()=>qg,utcSundays:()=>Zg,utcThursday:()=>Vg,utcThursdays:()=>tm,utcTuesday:()=>$g,utcTuesdays:()=>Kg,utcWednesday:()=>Wg,utcWednesdays:()=>Jg,utcWeek:()=>qg,utcWeeks:()=>Zg,utcYear:()=>sm,utcYears:()=>cm,values:()=>da,variance:()=>y,version:()=>r,voronoi:()=>Kk,window:()=>Ot,xml:()=>Ns,zip:()=>W,zoom:()=>fT,zoomIdentity:()=>nT,zoomTransform:()=>rT});var r="5.16.0";function i(t,e){return te?1:t>=e?0:NaN}function a(t){var e;return 1===t.length&&(e=t,t=function(t,n){return i(e(t),n)}),{left:function(e,n,r,i){for(null==r&&(r=0),null==i&&(i=e.length);r>>1;t(e[a],n)<0?r=a+1:i=a}return r},right:function(e,n,r,i){for(null==r&&(r=0),null==i&&(i=e.length);r>>1;t(e[a],n)>0?i=a:r=a+1}return r}}}var o=a(i),s=o.right,c=o.left;const u=s;function l(t,e){null==e&&(e=h);for(var n=0,r=t.length-1,i=t[0],a=new Array(r<0?0:r);nt?1:e>=t?0:NaN}function p(t){return null===t?NaN:+t}function y(t,e){var n,r,i=t.length,a=0,o=-1,s=0,c=0;if(null==e)for(;++o1)return c/(a-1)}function g(t,e){var n=y(t,e);return n?Math.sqrt(n):n}function m(t,e){var n,r,i,a=t.length,o=-1;if(null==e){for(;++o=n)for(r=i=n;++on&&(r=n),i=n)for(r=i=n;++on&&(r=n),i0)return[t];if((r=e0)for(t=Math.ceil(t/o),e=Math.floor(e/o),a=new Array(i=Math.ceil(e-t+1));++s=0?(a>=T?10:a>=E?5:a>=C?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(a>=T?10:a>=E?5:a>=C?2:1)}function M(t,e,n){var r=Math.abs(e-t)/Math.max(0,n),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),a=r/i;return a>=T?i*=10:a>=E?i*=5:a>=C&&(i*=2),eh;)f.pop(),--d;var p,y=new Array(d+1);for(i=0;i<=d;++i)(p=y[i]=[]).x0=i>0?f[i-1]:l,p.x1=i=1)return+n(t[r-1],r-1,t);var r,i=(r-1)*e,a=Math.floor(i),o=+n(t[a],a,t);return o+(+n(t[a+1],a+1,t)-o)*(i-a)}}function B(t,e,n){return t=_.call(t,p).sort(i),Math.ceil((n-e)/(2*(O(t,.75)-O(t,.25))*Math.pow(t.length,-1/3)))}function L(t,e,n){return Math.ceil((n-e)/(3.5*g(t)*Math.pow(t.length,-1/3)))}function I(t,e){var n,r,i=t.length,a=-1;if(null==e){for(;++a=n)for(r=n;++ar&&(r=n)}else for(;++a=n)for(r=n;++ar&&(r=n);return r}function R(t,e){var n,r=t.length,i=r,a=-1,o=0;if(null==e)for(;++a=0;)for(e=(r=t[i]).length;--e>=0;)n[--o]=r[e];return n}function j(t,e){var n,r,i=t.length,a=-1;if(null==e){for(;++a=n)for(r=n;++an&&(r=n)}else for(;++a=n)for(r=n;++an&&(r=n);return r}function Y(t,e){for(var n=e.length,r=new Array(n);n--;)r[n]=t[e[n]];return r}function z(t,e){if(n=t.length){var n,r,a=0,o=0,s=t[o];for(null==e&&(e=i);++a=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function lt(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;ae?1:t>=e?0:NaN}bt.prototype={constructor:bt,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var kt="http://www.w3.org/1999/xhtml";const Tt={svg:"http://www.w3.org/2000/svg",xhtml:kt,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function Et(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),Tt.hasOwnProperty(e)?{space:Tt[e],local:t}:t}function Ct(t){return function(){this.removeAttribute(t)}}function St(t){return function(){this.removeAttributeNS(t.space,t.local)}}function At(t,e){return function(){this.setAttribute(t,e)}}function Mt(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function Nt(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function Dt(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function Ot(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function Bt(t){return function(){this.style.removeProperty(t)}}function Lt(t,e,n){return function(){this.style.setProperty(t,e,n)}}function It(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function Rt(t,e){return t.style.getPropertyValue(e)||Ot(t).getComputedStyle(t,null).getPropertyValue(e)}function Ft(t){return function(){delete this[t]}}function Pt(t,e){return function(){this[t]=e}}function jt(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function Yt(t){return t.trim().split(/^|\s+/)}function zt(t){return t.classList||new Ut(t)}function Ut(t){this._node=t,this._names=Yt(t.getAttribute("class")||"")}function qt(t,e){for(var n=zt(t),r=-1,i=e.length;++r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var ue={},le=null;function he(t,e,n){return t=fe(t,e,n),function(e){var n=e.relatedTarget;n&&(n===this||8&n.compareDocumentPosition(this))||t.call(this,e)}}function fe(t,e,n){return function(r){var i=le;le=r;try{t.call(this,this.__data__,e,n)}finally{le=i}}}function de(t){return t.trim().split(/^|\s+/).map((function(t){var e="",n=t.indexOf(".");return n>=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function pe(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=x&&(x=_+1);!(b=m[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=wt);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?Bt:"function"==typeof e?It:Lt)(t,e,null==n?"":n)):Rt(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?Ft:"function"==typeof e?jt:Pt)(t,e)):this.node()[t]},classed:function(t,e){var n=Yt(t+"");if(arguments.length<2){for(var r=zt(this.node()),i=-1,a=n.length;++i>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?Xe(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?Xe(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Pe.exec(t))?new Ke(e[1],e[2],e[3],1):(e=je.exec(t))?new Ke(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Ye.exec(t))?Xe(e[1],e[2],e[3],e[4]):(e=ze.exec(t))?Xe(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Ue.exec(t))?nn(e[1],e[2]/100,e[3]/100,1):(e=qe.exec(t))?nn(e[1],e[2]/100,e[3]/100,e[4]):He.hasOwnProperty(t)?Ge(He[t]):"transparent"===t?new Ke(NaN,NaN,NaN,0):null}function Ge(t){return new Ke(t>>16&255,t>>8&255,255&t,1)}function Xe(t,e,n,r){return r<=0&&(t=e=n=NaN),new Ke(t,e,n,r)}function Ze(t){return t instanceof De||(t=Ve(t)),t?new Ke((t=t.rgb()).r,t.g,t.b,t.opacity):new Ke}function Qe(t,e,n,r){return 1===arguments.length?Ze(t):new Ke(t,e,n,null==r?1:r)}function Ke(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function Je(){return"#"+en(this.r)+en(this.g)+en(this.b)}function tn(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function en(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function nn(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new on(t,e,n,r)}function rn(t){if(t instanceof on)return new on(t.h,t.s,t.l,t.opacity);if(t instanceof De||(t=Ve(t)),!t)return new on;if(t instanceof on)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new on(o,s,c,t.opacity)}function an(t,e,n,r){return 1===arguments.length?rn(t):new on(t,e,n,null==r?1:r)}function on(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function sn(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function cn(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}function un(t){var e=t.length-1;return function(n){var r=n<=0?n=0:n>=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=r180||n<-180?n-360*Math.round(n/360):n):hn(isNaN(t)?e:t)}function pn(t,e){var n=e-t;return n?fn(t,n):hn(isNaN(t)?e:t)}Me(De,Ve,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:$e,formatHex:$e,formatHsl:function(){return rn(this).formatHsl()},formatRgb:We,toString:We}),Me(Ke,Qe,Ne(De,{brighter:function(t){return t=null==t?Be:Math.pow(Be,t),new Ke(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Oe:Math.pow(Oe,t),new Ke(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Je,formatHex:Je,formatRgb:tn,toString:tn})),Me(on,an,Ne(De,{brighter:function(t){return t=null==t?Be:Math.pow(Be,t),new on(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Oe:Math.pow(Oe,t),new on(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new Ke(sn(t>=240?t-240:t+120,i,r),sn(t,i,r),sn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const yn=function t(e){var n=function(t){return 1==(t=+t)?pn:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):hn(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=Qe(t)).r,(e=Qe(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=pn(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function gn(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;na&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:Tn(n,r)})),a=Sn.lastIndex;return a=0&&e._call.call(null,t),e=e._next;--Rn}function Xn(){Yn=(jn=Un.now())+zn,Rn=Fn=0;try{Gn()}finally{Rn=0,function(){for(var t,e,n=Ln,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:Ln=e);In=t,Qn(r)}(),Yn=0}}function Zn(){var t=Un.now(),e=t-jn;e>1e3&&(zn-=e,jn=t)}function Qn(t){Rn||(Fn&&(Fn=clearTimeout(Fn)),t-Yn>24?(t<1/0&&(Fn=setTimeout(Xn,t-Un.now()-zn)),Pn&&(Pn=clearInterval(Pn))):(Pn||(jn=Un.now(),Pn=setInterval(Zn,1e3)),Rn=1,qn(Xn)))}function Kn(t,e,n){var r=new Wn;return e=null==e?0:+e,r.restart((function(n){r.stop(),t(n+e)}),e,n),r}Wn.prototype=Vn.prototype={constructor:Wn,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Hn():+n)+(null==e?0:+e),this._next||In===this||(In?In._next=this:Ln=this,In=this),this._call=t,this._time=n,Qn()},stop:function(){this._call&&(this._call=null,this._time=1/0,Qn())}};var Jn=ft("start","end","cancel","interrupt"),tr=[];function er(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return Kn(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function rr(t,e){var n=ir(t,e);if(n.state>3)throw new Error("too late; already running");return n}function ir(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function ar(t,e){var n,r,i,a=t.__transition,o=!0;if(a){for(i in e=null==e?null:e+"",a)(n=a[i]).name===e?(r=n.state>2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}var or,sr,cr,ur,lr=180/Math.PI,hr={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function fr(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:Tn(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:Tn(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:Tn(t,n)},{i:s-2,x:Tn(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?nr:rr;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var Rr=ke.prototype.constructor;function Fr(t){return function(){this.style.removeProperty(t)}}function Pr(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function jr(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&Pr(t,a,n)),r}return a._value=e,a}function Yr(t){return function(e){this.textContent=t.call(this,e)}}function zr(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&Yr(r)),e}return r._value=t,r}var Ur=0;function qr(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function Hr(t){return ke().transition(t)}function $r(){return++Ur}var Wr=ke.prototype;function Vr(t){return t*t*t}function Gr(t){return--t*t*t+1}function Xr(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}qr.prototype=Hr.prototype={constructor:qr,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=pt(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o1&&n.name===e)return new qr([[t]],Kr,e,+r);return null}function ti(t){return function(){return t}}function ei(t,e,n){this.target=t,this.type=e,this.selection=n}function ni(){le.stopImmediatePropagation()}function ri(){le.preventDefault(),le.stopImmediatePropagation()}var ii={name:"drag"},ai={name:"space"},oi={name:"handle"},si={name:"center"};function ci(t){return[+t[0],+t[1]]}function ui(t){return[ci(t[0]),ci(t[1])]}function li(t){return function(e){return On(e,le.touches,t)}}var hi={name:"x",handles:["w","e"].map(bi),input:function(t,e){return null==t?null:[[+t[0],e[0][1]],[+t[1],e[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},fi={name:"y",handles:["n","s"].map(bi),input:function(t,e){return null==t?null:[[e[0][0],+t[0]],[e[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},di={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(bi),input:function(t){return null==t?null:ui(t)},output:function(t){return t}},pi={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},yi={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},gi={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},mi={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},vi={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function bi(t){return{type:t}}function _i(){return!le.ctrlKey&&!le.button}function xi(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function wi(){return navigator.maxTouchPoints||"ontouchstart"in this}function ki(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function Ti(t){return t[0][0]===t[1][0]||t[0][1]===t[1][1]}function Ei(t){var e=t.__brush;return e?e.dim.output(e.selection):null}function Ci(){return Mi(hi)}function Si(){return Mi(fi)}function Ai(){return Mi(di)}function Mi(t){var e,n=xi,r=_i,i=wi,a=!0,o=ft("start","brush","end"),s=6;function c(e){var n=e.property("__brush",y).selectAll(".overlay").data([bi("overlay")]);n.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",pi.overlay).merge(n).each((function(){var t=ki(this).extent;Te(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),e.selectAll(".selection").data([bi("selection")]).enter().append("rect").attr("class","selection").attr("cursor",pi.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=e.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return pi[t.type]})),e.each(u).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",f).filter(i).on("touchstart.brush",f).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function u(){var t=Te(this),e=ki(this).selection;e?(t.selectAll(".selection").style("display",null).attr("x",e[0][0]).attr("y",e[0][1]).attr("width",e[1][0]-e[0][0]).attr("height",e[1][1]-e[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?e[1][0]-s/2:e[0][0]-s/2})).attr("y",(function(t){return"s"===t.type[0]?e[1][1]-s/2:e[0][1]-s/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?e[1][0]-e[0][0]+s:s})).attr("height",(function(t){return"e"===t.type||"w"===t.type?e[1][1]-e[0][1]+s:s}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function l(t,e,n){var r=t.__brush.emitter;return!r||n&&r.clean?new h(t,e,n):r}function h(t,e,n){this.that=t,this.args=e,this.state=t.__brush,this.active=0,this.clean=n}function f(){if((!e||le.touches)&&r.apply(this,arguments)){var n,i,o,s,c,h,f,d,p,y,g,m=this,v=le.target.__data__.type,b="selection"===(a&&le.metaKey?v="overlay":v)?ii:a&&le.altKey?si:oi,_=t===fi?null:mi[v],x=t===hi?null:vi[v],w=ki(m),k=w.extent,T=w.selection,E=k[0][0],C=k[0][1],S=k[1][0],A=k[1][1],M=0,N=0,D=_&&x&&a&&le.shiftKey,O=le.touches?li(le.changedTouches[0].identifier):Bn,B=O(m),L=B,I=l(m,arguments,!0).beforestart();"overlay"===v?(T&&(p=!0),w.selection=T=[[n=t===fi?E:B[0],o=t===hi?C:B[1]],[c=t===fi?S:n,f=t===hi?A:o]]):(n=T[0][0],o=T[0][1],c=T[1][0],f=T[1][1]),i=n,s=o,h=c,d=f;var R=Te(m).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",pi[v]);if(le.touches)I.moved=j,I.ended=z;else{var P=Te(le.view).on("mousemove.brush",j,!0).on("mouseup.brush",z,!0);a&&P.on("keydown.brush",U,!0).on("keyup.brush",q,!0),Se(le.view)}ni(),ar(m),u.call(m),I.start()}function j(){var t=O(m);!D||y||g||(Math.abs(t[0]-L[0])>Math.abs(t[1]-L[1])?g=!0:y=!0),L=t,p=!0,ri(),Y()}function Y(){var t;switch(M=L[0]-B[0],N=L[1]-B[1],b){case ai:case ii:_&&(M=Math.max(E-n,Math.min(S-c,M)),i=n+M,h=c+M),x&&(N=Math.max(C-o,Math.min(A-f,N)),s=o+N,d=f+N);break;case oi:_<0?(M=Math.max(E-n,Math.min(S-n,M)),i=n+M,h=c):_>0&&(M=Math.max(E-c,Math.min(S-c,M)),i=n,h=c+M),x<0?(N=Math.max(C-o,Math.min(A-o,N)),s=o+N,d=f):x>0&&(N=Math.max(C-f,Math.min(A-f,N)),s=o,d=f+N);break;case si:_&&(i=Math.max(E,Math.min(S,n-M*_)),h=Math.max(E,Math.min(S,c+M*_))),x&&(s=Math.max(C,Math.min(A,o-N*x)),d=Math.max(C,Math.min(A,f+N*x)))}h0&&(n=i-M),x<0?f=d-N:x>0&&(o=s-N),b=ai,F.attr("cursor",pi.selection),Y());break;default:return}ri()}function q(){switch(le.keyCode){case 16:D&&(y=g=D=!1,Y());break;case 18:b===si&&(_<0?c=h:_>0&&(n=i),x<0?f=d:x>0&&(o=s),b=oi,Y());break;case 32:b===ai&&(le.altKey?(_&&(c=h-M*_,n=i+M*_),x&&(f=d-N*x,o=s+N*x),b=si):(_<0?c=h:_>0&&(n=i),x<0?f=d:x>0&&(o=s),b=oi),F.attr("cursor",pi[v]),Y());break;default:return}ri()}}function d(){l(this,arguments).moved()}function p(){l(this,arguments).ended()}function y(){var e=this.__brush||{selection:null};return e.extent=ui(n.apply(this,arguments)),e.dim=t,e}return c.move=function(e,n){e.selection?e.on("start.brush",(function(){l(this,arguments).beforestart().start()})).on("interrupt.brush end.brush",(function(){l(this,arguments).end()})).tween("brush",(function(){var e=this,r=e.__brush,i=l(e,arguments),a=r.selection,o=t.input("function"==typeof n?n.apply(this,arguments):n,r.extent),s=Mn(a,o);function c(t){r.selection=1===t&&null===o?null:s(t),u.call(e),i.brush()}return null!==a&&null!==o?c:c(1)})):e.each((function(){var e=this,r=arguments,i=e.__brush,a=t.input("function"==typeof n?n.apply(e,r):n,i.extent),o=l(e,r).beforestart();ar(e),i.selection=null===a?null:a,u.call(e),o.start().brush().end()}))},c.clear=function(t){c.move(t,null)},h.prototype={beforestart:function(){return 1==++this.active&&(this.state.emitter=this,this.starting=!0),this},start:function(){return this.starting?(this.starting=!1,this.emit("start")):this.emit("brush"),this},brush:function(){return this.emit("brush"),this},end:function(){return 0==--this.active&&(delete this.state.emitter,this.emit("end")),this},emit:function(e){ge(new ei(c,e,t.output(this.state.selection)),o.apply,o,[e,this.that,this.args])}},c.extent=function(t){return arguments.length?(n="function"==typeof t?t:ti(ui(t)),c):n},c.filter=function(t){return arguments.length?(r="function"==typeof t?t:ti(!!t),c):r},c.touchable=function(t){return arguments.length?(i="function"==typeof t?t:ti(!!t),c):i},c.handleSize=function(t){return arguments.length?(s=+t,c):s},c.keyModifiers=function(t){return arguments.length?(a=!!t,c):a},c.on=function(){var t=o.on.apply(o,arguments);return t===o?c:t},c}var Ni=Math.cos,Di=Math.sin,Oi=Math.PI,Bi=Oi/2,Li=2*Oi,Ii=Math.max;function Ri(t){return function(e,n){return t(e.source.value+e.target.value,n.source.value+n.target.value)}}function Fi(){var t=0,e=null,n=null,r=null;function i(i){var a,o,s,c,u,l,h=i.length,f=[],d=k(h),p=[],y=[],g=y.groups=new Array(h),m=new Array(h*h);for(a=0,u=-1;++uUi)if(Math.abs(l*s-c*u)>Ui&&i){var f=n-a,d=r-o,p=s*s+c*c,y=f*f+d*d,g=Math.sqrt(p),m=Math.sqrt(h),v=i*Math.tan((Yi-Math.acos((p+h-y)/(2*g*m)))/2),b=v/m,_=v/g;Math.abs(b-1)>Ui&&(this._+="L"+(t+b*u)+","+(e+b*l)),this._+="A"+i+","+i+",0,0,"+ +(l*f>u*d)+","+(this._x1=t+_*s)+","+(this._y1=e+_*c)}else this._+="L"+(this._x1=t)+","+(this._y1=e)},arc:function(t,e,n,r,i,a){t=+t,e=+e,a=!!a;var o=(n=+n)*Math.cos(r),s=n*Math.sin(r),c=t+o,u=e+s,l=1^a,h=a?r-i:i-r;if(n<0)throw new Error("negative radius: "+n);null===this._x1?this._+="M"+c+","+u:(Math.abs(this._x1-c)>Ui||Math.abs(this._y1-u)>Ui)&&(this._+="L"+c+","+u),n&&(h<0&&(h=h%zi+zi),h>qi?this._+="A"+n+","+n+",0,1,"+l+","+(t-o)+","+(e-s)+"A"+n+","+n+",0,1,"+l+","+(this._x1=c)+","+(this._y1=u):h>Ui&&(this._+="A"+n+","+n+",0,"+ +(h>=Yi)+","+l+","+(this._x1=t+n*Math.cos(i))+","+(this._y1=e+n*Math.sin(i))))},rect:function(t,e,n,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+e)+"h"+ +n+"v"+ +r+"h"+-n+"Z"},toString:function(){return this._}};const Wi=$i;function Vi(t){return t.source}function Gi(t){return t.target}function Xi(t){return t.radius}function Zi(t){return t.startAngle}function Qi(t){return t.endAngle}function Ki(){var t=Vi,e=Gi,n=Xi,r=Zi,i=Qi,a=null;function o(){var o,s=Pi.call(arguments),c=t.apply(this,s),u=e.apply(this,s),l=+n.apply(this,(s[0]=c,s)),h=r.apply(this,s)-Bi,f=i.apply(this,s)-Bi,d=l*Ni(h),p=l*Di(h),y=+n.apply(this,(s[0]=u,s)),g=r.apply(this,s)-Bi,m=i.apply(this,s)-Bi;if(a||(a=o=Wi()),a.moveTo(d,p),a.arc(0,0,l,h,f),h===g&&f===m||(a.quadraticCurveTo(0,0,y*Ni(g),y*Di(g)),a.arc(0,0,y,g,m)),a.quadraticCurveTo(0,0,d,p),a.closePath(),o)return a=null,o+""||null}return o.radius=function(t){return arguments.length?(n="function"==typeof t?t:ji(+t),o):n},o.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:ji(+t),o):r},o.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:ji(+t),o):i},o.source=function(e){return arguments.length?(t=e,o):t},o.target=function(t){return arguments.length?(e=t,o):e},o.context=function(t){return arguments.length?(a=null==t?null:t,o):a},o}var Ji="$";function ta(){}function ea(t,e){var n=new ta;if(t instanceof ta)t.each((function(t,e){n.set(e,t)}));else if(Array.isArray(t)){var r,i=-1,a=t.length;if(null==e)for(;++i=r.length)return null!=t&&n.sort(t),null!=e?e(n):n;for(var c,u,l,h=-1,f=n.length,d=r[i++],p=na(),y=o();++hr.length)return t;var a,s=i[n-1];return null!=e&&n>=r.length?a=t.entries():(a=[],t.each((function(t,e){a.push({key:e,values:o(t,n)})}))),null!=s?a.sort((function(t,e){return s(t.key,e.key)})):a}return n={object:function(t){return a(t,0,ia,aa)},map:function(t){return a(t,0,oa,sa)},entries:function(t){return o(a(t,0,oa,sa),0)},key:function(t){return r.push(t),n},sortKeys:function(t){return i[r.length-1]=t,n},sortValues:function(e){return t=e,n},rollup:function(t){return e=t,n}}}function ia(){return{}}function aa(t,e,n){t[e]=n}function oa(){return na()}function sa(t,e,n){t.set(e,n)}function ca(){}var ua=na.prototype;function la(t,e){var n=new ca;if(t instanceof ca)t.each((function(t){n.add(t)}));else if(t){var r=-1,i=t.length;if(null==e)for(;++r.008856451679035631?Math.pow(t,1/3):t/xa+ba}function Sa(t){return t>_a?t*t*t:xa*(t-ba)}function Aa(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Ma(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Na(t){if(t instanceof Ba)return new Ba(t.h,t.c,t.l,t.opacity);if(t instanceof Ea||(t=wa(t)),0===t.a&&0===t.b)return new Ba(NaN,0r!=d>r&&n<(f-u)*(r-l)/(d-l)+u&&(i=-i)}return i}function Qa(t,e,n){var r,i,a,o;return function(t,e,n){return(e[0]-t[0])*(n[1]-t[1])==(n[0]-t[0])*(e[1]-t[1])}(t,e,n)&&(i=t[r=+(t[0]===e[0])],a=n[r],o=e[r],i<=a&&a<=o||o<=a&&a<=i)}function Ka(){}var Ja=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function to(){var t=1,e=1,n=N,r=s;function i(t){var e=n(t);if(Array.isArray(e))e=e.slice().sort(Va);else{var r=m(t),i=r[0],o=r[1];e=M(i,o,e),e=k(Math.floor(i/e)*e,Math.floor(o/e)*e,e)}return e.map((function(e){return a(t,e)}))}function a(n,i){var a=[],s=[];return function(n,r,i){var a,s,c,u,l,h,f=new Array,d=new Array;for(a=s=-1,u=n[0]>=r,Ja[u<<1].forEach(p);++a=r,Ja[c|u<<1].forEach(p);for(Ja[u<<0].forEach(p);++s=r,l=n[s*t]>=r,Ja[u<<1|l<<2].forEach(p);++a=r,h=l,l=n[s*t+a+1]>=r,Ja[c|u<<1|l<<2|h<<3].forEach(p);Ja[u|l<<3].forEach(p)}for(a=-1,l=n[s*t]>=r,Ja[l<<2].forEach(p);++a=r,Ja[l<<2|h<<3].forEach(p);function p(t){var e,n,r=[t[0][0]+a,t[0][1]+s],c=[t[1][0]+a,t[1][1]+s],u=o(r),l=o(c);(e=d[u])?(n=f[l])?(delete d[e.end],delete f[n.start],e===n?(e.ring.push(c),i(e.ring)):f[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete d[e.end],e.ring.push(c),d[e.end=l]=e):(e=f[l])?(n=d[u])?(delete f[e.start],delete d[n.end],e===n?(e.ring.push(c),i(e.ring)):f[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete f[e.start],e.ring.unshift(r),f[e.start=u]=e):f[u]=d[l]={start:u,end:l,ring:[r,c]}}Ja[l<<3].forEach(p)}(n,i,(function(t){r(t,n,i),function(t){for(var e=0,n=t.length,r=t[n-1][1]*t[0][0]-t[n-1][0]*t[0][1];++e0?a.push([t]):s.push(t)})),s.forEach((function(t){for(var e,n=0,r=a.length;n0&&o0&&s0&&a>0))throw new Error("invalid size");return t=r,e=a,i},i.thresholds=function(t){return arguments.length?(n="function"==typeof t?t:Array.isArray(t)?Ga(Wa.call(t)):Ga(t),i):n},i.smooth=function(t){return arguments.length?(r=t?s:Ka,i):r===s},i}function eo(t,e,n){for(var r=t.width,i=t.height,a=1+(n<<1),o=0;o=n&&(s>=a&&(c-=t.data[s-a+o*r]),e.data[s-n+o*r]=c/Math.min(s+1,r-1+a-s,a))}function no(t,e,n){for(var r=t.width,i=t.height,a=1+(n<<1),o=0;o=n&&(s>=a&&(c-=t.data[o+(s-a)*r]),e.data[o+(s-n)*r]=c/Math.min(s+1,i-1+a-s,a))}function ro(t){return t[0]}function io(t){return t[1]}function ao(){return 1}function oo(){var t=ro,e=io,n=ao,r=960,i=500,a=20,o=2,s=3*a,c=r+2*s>>o,u=i+2*s>>o,l=Ga(20);function h(r){var i=new Float32Array(c*u),h=new Float32Array(c*u);r.forEach((function(r,a,l){var h=+t(r,a,l)+s>>o,f=+e(r,a,l)+s>>o,d=+n(r,a,l);h>=0&&h=0&&f>o),no({width:c,height:u,data:h},{width:c,height:u,data:i},a>>o),eo({width:c,height:u,data:i},{width:c,height:u,data:h},a>>o),no({width:c,height:u,data:h},{width:c,height:u,data:i},a>>o),eo({width:c,height:u,data:i},{width:c,height:u,data:h},a>>o),no({width:c,height:u,data:h},{width:c,height:u,data:i},a>>o);var d=l(i);if(!Array.isArray(d)){var p=I(i);d=M(0,p,d),(d=k(0,Math.floor(p/d)*d,d)).shift()}return to().thresholds(d).size([c,u])(i).map(f)}function f(t){return t.value*=Math.pow(2,-2*o),t.coordinates.forEach(d),t}function d(t){t.forEach(p)}function p(t){t.forEach(y)}function y(t){t[0]=t[0]*Math.pow(2,o)-s,t[1]=t[1]*Math.pow(2,o)-s}function g(){return c=r+2*(s=3*a)>>o,u=i+2*s>>o,h}return h.x=function(e){return arguments.length?(t="function"==typeof e?e:Ga(+e),h):t},h.y=function(t){return arguments.length?(e="function"==typeof t?t:Ga(+t),h):e},h.weight=function(t){return arguments.length?(n="function"==typeof t?t:Ga(+t),h):n},h.size=function(t){if(!arguments.length)return[r,i];var e=Math.ceil(t[0]),n=Math.ceil(t[1]);if(!(e>=0||e>=0))throw new Error("invalid size");return r=e,i=n,g()},h.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return o=Math.floor(Math.log(t)/Math.LN2),g()},h.thresholds=function(t){return arguments.length?(l="function"==typeof t?t:Array.isArray(t)?Ga(Wa.call(t)):Ga(t),h):l},h.bandwidth=function(t){if(!arguments.length)return Math.sqrt(a*(a+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return a=Math.round((Math.sqrt(4*t*t+1)-1)/2),g()},h}function so(t){return function(){return t}}function co(t,e,n,r,i,a,o,s,c,u){this.target=t,this.type=e,this.subject=n,this.identifier=r,this.active=i,this.x=a,this.y=o,this.dx=s,this.dy=c,this._=u}function uo(){return!le.ctrlKey&&!le.button}function lo(){return this.parentNode}function ho(t){return null==t?{x:le.x,y:le.y}:t}function fo(){return navigator.maxTouchPoints||"ontouchstart"in this}function po(){var t,e,n,r,i=uo,a=lo,o=ho,s=fo,c={},u=ft("start","drag","end"),l=0,h=0;function f(t){t.on("mousedown.drag",d).filter(s).on("touchstart.drag",g).on("touchmove.drag",m).on("touchend.drag touchcancel.drag",v).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(){if(!r&&i.apply(this,arguments)){var o=b("mouse",a.apply(this,arguments),Bn,this,arguments);o&&(Te(le.view).on("mousemove.drag",p,!0).on("mouseup.drag",y,!0),Se(le.view),Ee(),n=!1,t=le.clientX,e=le.clientY,o("start"))}}function p(){if(Ce(),!n){var r=le.clientX-t,i=le.clientY-e;n=r*r+i*i>h}c.mouse("drag")}function y(){Te(le.view).on("mousemove.drag mouseup.drag",null),Ae(le.view,n),Ce(),c.mouse("end")}function g(){if(i.apply(this,arguments)){var t,e,n=le.changedTouches,r=a.apply(this,arguments),o=n.length;for(t=0;t=a?c=!0:10===(r=t.charCodeAt(o++))?u=!0:13===r&&(u=!0,10===t.charCodeAt(o)&&++o),t.slice(i+1,e-1).replace(/""/g,'"')}for(;o9999?"+"+bo(t,6):bo(t,4)}(t.getUTCFullYear())+"-"+bo(t.getUTCMonth()+1,2)+"-"+bo(t.getUTCDate(),2)+(i?"T"+bo(e,2)+":"+bo(n,2)+":"+bo(r,2)+"."+bo(i,3)+"Z":r?"T"+bo(e,2)+":"+bo(n,2)+":"+bo(r,2)+"Z":n||e?"T"+bo(e,2)+":"+bo(n,2)+"Z":"")}(t):e.test(t+="")?'"'+t.replace(/"/g,'""')+'"':t}return{parse:function(t,e){var n,i,a=r(t,(function(t,r){if(n)return n(t,r-1);i=t,n=e?function(t,e){var n=mo(t);return function(r,i){return e(n(r),i,t)}}(t,e):mo(t)}));return a.columns=i||[],a},parseRows:r,format:function(e,n){return null==n&&(n=vo(e)),[n.map(o).join(t)].concat(i(e,n)).join("\n")},formatBody:function(t,e){return null==e&&(e=vo(t)),i(t,e).join("\n")},formatRows:function(t){return t.map(a).join("\n")},formatRow:a,formatValue:o}}var xo=_o(","),wo=xo.parse,ko=xo.parseRows,To=xo.format,Eo=xo.formatBody,Co=xo.formatRows,So=xo.formatRow,Ao=xo.formatValue,Mo=_o("\t"),No=Mo.parse,Do=Mo.parseRows,Oo=Mo.format,Bo=Mo.formatBody,Lo=Mo.formatRows,Io=Mo.formatRow,Ro=Mo.formatValue;function Fo(t){for(var e in t){var n,r,i=t[e].trim();if(i)if("true"===i)i=!0;else if("false"===i)i=!1;else if("NaN"===i)i=NaN;else if(isNaN(n=+i)){if(!(r=i.match(/^([-+]\d{2})?\d{4}(-\d{2}(-\d{2})?)?(T\d{2}:\d{2}(:\d{2}(\.\d{3})?)?(Z|[-+]\d{2}:\d{2})?)?$/)))continue;Po&&r[4]&&!r[7]&&(i=i.replace(/-/g,"/").replace(/T/," ")),i=new Date(i)}else i=n;else i=null;t[e]=i}return t}var Po=new Date("2019-01-01T00:00").getHours()||new Date("2019-07-01T00:00").getHours();function jo(t){return+t}function Yo(t){return t*t}function zo(t){return t*(2-t)}function Uo(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}var qo=function t(e){function n(t){return Math.pow(t,e)}return e=+e,n.exponent=t,n}(3),Ho=function t(e){function n(t){return 1-Math.pow(1-t,e)}return e=+e,n.exponent=t,n}(3),$o=function t(e){function n(t){return((t*=2)<=1?Math.pow(t,e):2-Math.pow(2-t,e))/2}return e=+e,n.exponent=t,n}(3),Wo=Math.PI,Vo=Wo/2;function Go(t){return 1==+t?1:1-Math.cos(t*Vo)}function Xo(t){return Math.sin(t*Vo)}function Zo(t){return(1-Math.cos(Wo*t))/2}function Qo(t){return 1.0009775171065494*(Math.pow(2,-10*t)-.0009765625)}function Ko(t){return Qo(1-+t)}function Jo(t){return 1-Qo(t)}function ts(t){return((t*=2)<=1?Qo(1-t):2-Qo(t-1))/2}function es(t){return 1-Math.sqrt(1-t*t)}function ns(t){return Math.sqrt(1- --t*t)}function rs(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}var is=7.5625;function as(t){return 1-os(1-t)}function os(t){return(t=+t)<.36363636363636365?is*t*t:t<.7272727272727273?is*(t-=.5454545454545454)*t+.75:t<.9090909090909091?is*(t-=.8181818181818182)*t+.9375:is*(t-=.9545454545454546)*t+.984375}function ss(t){return((t*=2)<=1?1-os(1-t):os(t-1)+1)/2}var cs=1.70158,us=function t(e){function n(t){return(t=+t)*t*(e*(t-1)+t)}return e=+e,n.overshoot=t,n}(cs),ls=function t(e){function n(t){return--t*t*((t+1)*e+t)+1}return e=+e,n.overshoot=t,n}(cs),hs=function t(e){function n(t){return((t*=2)<1?t*t*((e+1)*t-e):(t-=2)*t*((e+1)*t+e)+2)/2}return e=+e,n.overshoot=t,n}(cs),fs=2*Math.PI,ds=function t(e,n){var r=Math.asin(1/(e=Math.max(1,e)))*(n/=fs);function i(t){return e*Qo(- --t)*Math.sin((r-t)/n)}return i.amplitude=function(e){return t(e,n*fs)},i.period=function(n){return t(e,n)},i}(1,.3),ps=function t(e,n){var r=Math.asin(1/(e=Math.max(1,e)))*(n/=fs);function i(t){return 1-e*Qo(t=+t)*Math.sin((t+r)/n)}return i.amplitude=function(e){return t(e,n*fs)},i.period=function(n){return t(e,n)},i}(1,.3),ys=function t(e,n){var r=Math.asin(1/(e=Math.max(1,e)))*(n/=fs);function i(t){return((t=2*t-1)<0?e*Qo(-t)*Math.sin((r-t)/n):2-e*Qo(t)*Math.sin((r+t)/n))/2}return i.amplitude=function(e){return t(e,n*fs)},i.period=function(n){return t(e,n)},i}(1,.3);function gs(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.blob()}function ms(t,e){return fetch(t,e).then(gs)}function vs(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.arrayBuffer()}function bs(t,e){return fetch(t,e).then(vs)}function _s(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.text()}function xs(t,e){return fetch(t,e).then(_s)}function ws(t){return function(e,n,r){return 2===arguments.length&&"function"==typeof n&&(r=n,n=void 0),xs(e,n).then((function(e){return t(e,r)}))}}function ks(t,e,n,r){3===arguments.length&&"function"==typeof n&&(r=n,n=void 0);var i=_o(t);return xs(e,n).then((function(t){return i.parse(t,r)}))}var Ts=ws(wo),Es=ws(No);function Cs(t,e){return new Promise((function(n,r){var i=new Image;for(var a in e)i[a]=e[a];i.onerror=r,i.onload=function(){n(i)},i.src=t}))}function Ss(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);if(204!==t.status&&205!==t.status)return t.json()}function As(t,e){return fetch(t,e).then(Ss)}function Ms(t){return function(e,n){return xs(e,n).then((function(e){return(new DOMParser).parseFromString(e,t)}))}}const Ns=Ms("application/xml");var Ds=Ms("text/html"),Os=Ms("image/svg+xml");function Bs(t,e){var n;function r(){var r,i,a=n.length,o=0,s=0;for(r=0;r=(a=(y+m)/2))?y=a:m=a,(l=n>=(o=(g+v)/2))?g=o:v=o,i=d,!(d=d[h=l<<1|u]))return i[h]=p,t;if(s=+t._x.call(null,d.data),c=+t._y.call(null,d.data),e===s&&n===c)return p.next=d,i?i[h]=p:t._root=p,t;do{i=i?i[h]=new Array(4):t._root=new Array(4),(u=e>=(a=(y+m)/2))?y=a:m=a,(l=n>=(o=(g+v)/2))?g=o:v=o}while((h=l<<1|u)==(f=(c>=o)<<1|s>=a));return i[f]=d,i[h]=p,t}function Fs(t,e,n,r,i){this.node=t,this.x0=e,this.y0=n,this.x1=r,this.y1=i}function Ps(t){return t[0]}function js(t){return t[1]}function Ys(t,e,n){var r=new zs(null==e?Ps:e,null==n?js:n,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function zs(t,e,n,r,i,a){this._x=t,this._y=e,this._x0=n,this._y0=r,this._x1=i,this._y1=a,this._root=void 0}function Us(t){for(var e={data:t.data},n=e;t=t.next;)n=n.next={data:t.data};return e}var qs=Ys.prototype=zs.prototype;function Hs(t){return t.x+t.vx}function $s(t){return t.y+t.vy}function Ws(t){var e,n,r=1,i=1;function a(){for(var t,a,s,c,u,l,h,f=e.length,d=0;dc+d||iu+d||as.index){var p=c-o.x-o.vx,y=u-o.y-o.vy,g=p*p+y*y;gt.r&&(t.r=t[e].r)}function s(){if(e){var r,i,a=e.length;for(n=new Array(a),r=0;rl&&(l=r),ih&&(h=i));if(c>l||u>h)return this;for(this.cover(c,u).cover(l,h),n=0;nt||t>=i||r>e||e>=a;)switch(s=(ef||(a=c.y0)>d||(o=c.x1)=m)<<1|t>=g)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-u],p[p.length-1-u]=c)}else{var v=t-+this._x.call(null,y.data),b=e-+this._y.call(null,y.data),_=v*v+b*b;if(_=(s=(p+g)/2))?p=s:g=s,(l=o>=(c=(y+m)/2))?y=c:m=c,e=d,!(d=d[h=l<<1|u]))return this;if(!d.length)break;(e[h+1&3]||e[h+2&3]||e[h+3&3])&&(n=e,f=h)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):e?(i?e[h]=i:delete e[h],(d=e[0]||e[1]||e[2]||e[3])&&d===(e[3]||e[2]||e[1]||e[0])&&!d.length&&(n?n[f]=d:this._root=d),this):(this._root=i,this)},qs.removeAll=function(t){for(var e=0,n=t.length;e1?(null==n?s.remove(t):s.set(t,d(n)),e):s.get(t)},find:function(e,n,r){var i,a,o,s,c,u=0,l=t.length;for(null==r?r=1/0:r*=r,u=0;u1?(u.on(t,n),e):u.on(t)}}}function tc(){var t,e,n,r,i=Ls(-30),a=1,o=1/0,s=.81;function c(r){var i,a=t.length,o=Ys(t,Zs,Qs).visitAfter(l);for(n=r,i=0;i=o)){(t.data!==e||t.next)&&(0===l&&(d+=(l=Is())*l),0===h&&(d+=(h=Is())*h),d1?r[0]+r.slice(2):r,+t.slice(n+1)]}function ac(t){return(t=ic(Math.abs(t)))?t[1]:NaN}var oc,sc=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function cc(t){if(!(e=sc.exec(t)))throw new Error("invalid format: "+t);var e;return new uc({fill:e[1],align:e[2],sign:e[3],symbol:e[4],zero:e[5],width:e[6],comma:e[7],precision:e[8]&&e[8].slice(1),trim:e[9],type:e[10]})}function uc(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function lc(t,e){var n=ic(t,e);if(!n)return t+"";var r=n[0],i=n[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}cc.prototype=uc.prototype,uc.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};const hc={"%":function(t,e){return(100*t).toFixed(e)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:function(t,e){return t.toExponential(e)},f:function(t,e){return t.toFixed(e)},g:function(t,e){return t.toPrecision(e)},o:function(t){return Math.round(t).toString(8)},p:function(t,e){return lc(100*t,e)},r:lc,s:function(t,e){var n=ic(t,e);if(!n)return t+"";var r=n[0],i=n[1],a=i-(oc=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,o=r.length;return a===o?r:a>o?r+new Array(a-o+1).join("0"):a>0?r.slice(0,a)+"."+r.slice(a):"0."+new Array(1-a).join("0")+ic(t,Math.max(0,e+a-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function fc(t){return t}var dc,pc,yc,gc=Array.prototype.map,mc=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function vc(t){var e,n,r=void 0===t.grouping||void 0===t.thousands?fc:(e=gc.call(t.grouping,Number),n=t.thousands+"",function(t,r){for(var i=t.length,a=[],o=0,s=e[0],c=0;i>0&&s>0&&(c+s+1>r&&(s=Math.max(1,r-c)),a.push(t.substring(i-=s,i+s)),!((c+=s+1)>r));)s=e[o=(o+1)%e.length];return a.reverse().join(n)}),i=void 0===t.currency?"":t.currency[0]+"",a=void 0===t.currency?"":t.currency[1]+"",o=void 0===t.decimal?".":t.decimal+"",s=void 0===t.numerals?fc:function(t){return function(e){return e.replace(/[0-9]/g,(function(e){return t[+e]}))}}(gc.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",u=void 0===t.minus?"-":t.minus+"",l=void 0===t.nan?"NaN":t.nan+"";function h(t){var e=(t=cc(t)).fill,n=t.align,h=t.sign,f=t.symbol,d=t.zero,p=t.width,y=t.comma,g=t.precision,m=t.trim,v=t.type;"n"===v?(y=!0,v="g"):hc[v]||(void 0===g&&(g=12),m=!0,v="g"),(d||"0"===e&&"="===n)&&(d=!0,e="0",n="=");var b="$"===f?i:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",_="$"===f?a:/[%p]/.test(v)?c:"",x=hc[v],w=/[defgprs%]/.test(v);function k(t){var i,a,c,f=b,k=_;if("c"===v)k=x(t)+k,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?l:x(Math.abs(t),g),m&&(t=function(t){t:for(var e,n=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(e+1):t}(t)),T&&0==+t&&"+"!==h&&(T=!1),f=(T?"("===h?h:u:"-"===h||"("===h?"":h)+f,k=("s"===v?mc[8+oc/3]:"")+k+(T&&"("===h?")":""),w)for(i=-1,a=t.length;++i(c=t.charCodeAt(i))||c>57){k=(46===c?o+t.slice(i+1):t.slice(i))+k,t=t.slice(0,i);break}}y&&!d&&(t=r(t,1/0));var E=f.length+t.length+k.length,C=E>1)+f+t+k+C.slice(E);break;default:t=C+f+t+k}return s(t)}return g=void 0===g?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,g)):Math.max(0,Math.min(20,g)),k.toString=function(){return t+""},k}return{format:h,formatPrefix:function(t,e){var n=h(((t=cc(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(ac(e)/3))),i=Math.pow(10,-r),a=mc[8+r/3];return function(t){return n(i*t)+a}}}}function bc(t){return dc=vc(t),pc=dc.format,yc=dc.formatPrefix,dc}function _c(t){return Math.max(0,-ac(Math.abs(t)))}function xc(t,e){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(ac(e)/3)))-ac(Math.abs(t)))}function wc(t,e){return t=Math.abs(t),e=Math.abs(e)-t,Math.max(0,ac(e)-ac(t))+1}function kc(){return new Tc}function Tc(){this.reset()}bc({decimal:".",thousands:",",grouping:[3],currency:["$",""],minus:"-"}),Tc.prototype={constructor:Tc,reset:function(){this.s=this.t=0},add:function(t){Cc(Ec,t,this.t),Cc(this,Ec.s,this.s),this.s?this.t+=Ec.t:this.s=Ec.t},valueOf:function(){return this.s}};var Ec=new Tc;function Cc(t,e,n){var r=t.s=e+n,i=r-e,a=r-i;t.t=e-a+(n-i)}var Sc=1e-6,Ac=1e-12,Mc=Math.PI,Nc=Mc/2,Dc=Mc/4,Oc=2*Mc,Bc=180/Mc,Lc=Mc/180,Ic=Math.abs,Rc=Math.atan,Fc=Math.atan2,Pc=Math.cos,jc=Math.ceil,Yc=Math.exp,zc=(Math.floor,Math.log),Uc=Math.pow,qc=Math.sin,Hc=Math.sign||function(t){return t>0?1:t<0?-1:0},$c=Math.sqrt,Wc=Math.tan;function Vc(t){return t>1?0:t<-1?Mc:Math.acos(t)}function Gc(t){return t>1?Nc:t<-1?-Nc:Math.asin(t)}function Xc(t){return(t=qc(t/2))*t}function Zc(){}function Qc(t,e){t&&Jc.hasOwnProperty(t.type)&&Jc[t.type](t,e)}var Kc={Feature:function(t,e){Qc(t.geometry,e)},FeatureCollection:function(t,e){for(var n=t.features,r=-1,i=n.length;++r=0?1:-1,i=r*n,a=Pc(e=(e*=Lc)/2+Dc),o=qc(e),s=su*o,c=ou*a+s*Pc(i),u=s*r*qc(i);cu.add(Fc(u,c)),au=t,ou=a,su=o}function yu(t){return uu.reset(),nu(t,lu),2*uu}function gu(t){return[Fc(t[1],t[0]),Gc(t[2])]}function mu(t){var e=t[0],n=t[1],r=Pc(n);return[r*Pc(e),r*qc(e),qc(n)]}function vu(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}function bu(t,e){return[t[1]*e[2]-t[2]*e[1],t[2]*e[0]-t[0]*e[2],t[0]*e[1]-t[1]*e[0]]}function _u(t,e){t[0]+=e[0],t[1]+=e[1],t[2]+=e[2]}function xu(t,e){return[t[0]*e,t[1]*e,t[2]*e]}function wu(t){var e=$c(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=e,t[1]/=e,t[2]/=e}var ku,Tu,Eu,Cu,Su,Au,Mu,Nu,Du,Ou,Bu,Lu,Iu,Ru,Fu,Pu,ju,Yu,zu,Uu,qu,Hu,$u,Wu,Vu,Gu,Xu=kc(),Zu={point:Qu,lineStart:Ju,lineEnd:tl,polygonStart:function(){Zu.point=el,Zu.lineStart=nl,Zu.lineEnd=rl,Xu.reset(),lu.polygonStart()},polygonEnd:function(){lu.polygonEnd(),Zu.point=Qu,Zu.lineStart=Ju,Zu.lineEnd=tl,cu<0?(ku=-(Eu=180),Tu=-(Cu=90)):Xu>Sc?Cu=90:Xu<-1e-6&&(Tu=-90),Ou[0]=ku,Ou[1]=Eu},sphere:function(){ku=-(Eu=180),Tu=-(Cu=90)}};function Qu(t,e){Du.push(Ou=[ku=t,Eu=t]),eCu&&(Cu=e)}function Ku(t,e){var n=mu([t*Lc,e*Lc]);if(Nu){var r=bu(Nu,n),i=bu([r[1],-r[0],0],r);wu(i),i=gu(i);var a,o=t-Su,s=o>0?1:-1,c=i[0]*Bc*s,u=Ic(o)>180;u^(s*SuCu&&(Cu=a):u^(s*Su<(c=(c+360)%360-180)&&cCu&&(Cu=e)),u?til(ku,Eu)&&(Eu=t):il(t,Eu)>il(ku,Eu)&&(ku=t):Eu>=ku?(tEu&&(Eu=t)):t>Su?il(ku,t)>il(ku,Eu)&&(Eu=t):il(t,Eu)>il(ku,Eu)&&(ku=t)}else Du.push(Ou=[ku=t,Eu=t]);eCu&&(Cu=e),Nu=n,Su=t}function Ju(){Zu.point=Ku}function tl(){Ou[0]=ku,Ou[1]=Eu,Zu.point=Qu,Nu=null}function el(t,e){if(Nu){var n=t-Su;Xu.add(Ic(n)>180?n+(n>0?360:-360):n)}else Au=t,Mu=e;lu.point(t,e),Ku(t,e)}function nl(){lu.lineStart()}function rl(){el(Au,Mu),lu.lineEnd(),Ic(Xu)>Sc&&(ku=-(Eu=180)),Ou[0]=ku,Ou[1]=Eu,Nu=null}function il(t,e){return(e-=t)<0?e+360:e}function al(t,e){return t[0]-e[0]}function ol(t,e){return t[0]<=t[1]?t[0]<=e&&e<=t[1]:eil(r[0],r[1])&&(r[1]=i[1]),il(i[0],r[1])>il(r[0],r[1])&&(r[0]=i[0])):a.push(r=i);for(o=-1/0,e=0,r=a[n=a.length-1];e<=n;r=i,++e)i=a[e],(s=il(r[1],i[0]))>o&&(o=s,ku=i[0],Eu=r[1])}return Du=Ou=null,ku===1/0||Tu===1/0?[[NaN,NaN],[NaN,NaN]]:[[ku,Tu],[Eu,Cu]]}var cl={sphere:Zc,point:ul,lineStart:hl,lineEnd:pl,polygonStart:function(){cl.lineStart=yl,cl.lineEnd=gl},polygonEnd:function(){cl.lineStart=hl,cl.lineEnd=pl}};function ul(t,e){t*=Lc;var n=Pc(e*=Lc);ll(n*Pc(t),n*qc(t),qc(e))}function ll(t,e,n){++Bu,Iu+=(t-Iu)/Bu,Ru+=(e-Ru)/Bu,Fu+=(n-Fu)/Bu}function hl(){cl.point=fl}function fl(t,e){t*=Lc;var n=Pc(e*=Lc);Wu=n*Pc(t),Vu=n*qc(t),Gu=qc(e),cl.point=dl,ll(Wu,Vu,Gu)}function dl(t,e){t*=Lc;var n=Pc(e*=Lc),r=n*Pc(t),i=n*qc(t),a=qc(e),o=Fc($c((o=Vu*a-Gu*i)*o+(o=Gu*r-Wu*a)*o+(o=Wu*i-Vu*r)*o),Wu*r+Vu*i+Gu*a);Lu+=o,Pu+=o*(Wu+(Wu=r)),ju+=o*(Vu+(Vu=i)),Yu+=o*(Gu+(Gu=a)),ll(Wu,Vu,Gu)}function pl(){cl.point=ul}function yl(){cl.point=ml}function gl(){vl(Hu,$u),cl.point=ul}function ml(t,e){Hu=t,$u=e,t*=Lc,e*=Lc,cl.point=vl;var n=Pc(e);Wu=n*Pc(t),Vu=n*qc(t),Gu=qc(e),ll(Wu,Vu,Gu)}function vl(t,e){t*=Lc;var n=Pc(e*=Lc),r=n*Pc(t),i=n*qc(t),a=qc(e),o=Vu*a-Gu*i,s=Gu*r-Wu*a,c=Wu*i-Vu*r,u=$c(o*o+s*s+c*c),l=Gc(u),h=u&&-l/u;zu+=h*o,Uu+=h*s,qu+=h*c,Lu+=l,Pu+=l*(Wu+(Wu=r)),ju+=l*(Vu+(Vu=i)),Yu+=l*(Gu+(Gu=a)),ll(Wu,Vu,Gu)}function bl(t){Bu=Lu=Iu=Ru=Fu=Pu=ju=Yu=zu=Uu=qu=0,nu(t,cl);var e=zu,n=Uu,r=qu,i=e*e+n*n+r*r;return iMc?t+Math.round(-t/Oc)*Oc:t,e]}function kl(t,e,n){return(t%=Oc)?e||n?xl(El(t),Cl(e,n)):El(t):e||n?Cl(e,n):wl}function Tl(t){return function(e,n){return[(e+=t)>Mc?e-Oc:e<-Mc?e+Oc:e,n]}}function El(t){var e=Tl(t);return e.invert=Tl(-t),e}function Cl(t,e){var n=Pc(t),r=qc(t),i=Pc(e),a=qc(e);function o(t,e){var o=Pc(e),s=Pc(t)*o,c=qc(t)*o,u=qc(e),l=u*n+s*r;return[Fc(c*i-l*a,s*n-u*r),Gc(l*i+c*a)]}return o.invert=function(t,e){var o=Pc(e),s=Pc(t)*o,c=qc(t)*o,u=qc(e),l=u*i-c*a;return[Fc(c*i+u*a,s*n+l*r),Gc(l*n-s*r)]},o}function Sl(t){function e(e){return(e=t(e[0]*Lc,e[1]*Lc))[0]*=Bc,e[1]*=Bc,e}return t=kl(t[0]*Lc,t[1]*Lc,t.length>2?t[2]*Lc:0),e.invert=function(e){return(e=t.invert(e[0]*Lc,e[1]*Lc))[0]*=Bc,e[1]*=Bc,e},e}function Al(t,e,n,r,i,a){if(n){var o=Pc(e),s=qc(e),c=r*n;null==i?(i=e+r*Oc,a=e-c/2):(i=Ml(o,i),a=Ml(o,a),(r>0?ia)&&(i+=r*Oc));for(var u,l=i;r>0?l>a:l1&&e.push(e.pop().concat(e.shift()))},result:function(){var n=e;return e=[],t=null,n}}}function Ol(t,e){return Ic(t[0]-e[0])=0;--a)i.point((l=u[a])[0],l[1]);else r(f.x,f.p.x,-1,i);f=f.p}u=(f=f.o).z,d=!d}while(!f.v);i.lineEnd()}}}function Il(t){if(e=t.length){for(var e,n,r=0,i=t[0];++r=0?1:-1,E=T*k,C=E>Mc,S=y*x;if(Rl.add(Fc(S*T*qc(E),g*w+S*Pc(E))),o+=C?k+T*Oc:k,C^d>=n^b>=n){var A=bu(mu(f),mu(v));wu(A);var M=bu(a,A);wu(M);var N=(C^k>=0?-1:1)*Gc(M[2]);(r>N||r===N&&(A[0]||A[1]))&&(s+=C^k>=0?1:-1)}}return(o<-1e-6||o0){for(h||(i.polygonStart(),h=!0),i.lineStart(),t=0;t1&&2&c&&f.push(f.pop().concat(f.shift())),o.push(f.filter(Yl))}return f}}function Yl(t){return t.length>1}function zl(t,e){return((t=t.x)[0]<0?t[1]-Nc-Sc:Nc-t[1])-((e=e.x)[0]<0?e[1]-Nc-Sc:Nc-e[1])}const Ul=jl((function(){return!0}),(function(t){var e,n=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),e=1},point:function(a,o){var s=a>0?Mc:-Mc,c=Ic(a-n);Ic(c-Mc)0?Nc:-Nc),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(s,r),t.point(a,r),e=0):i!==s&&c>=Mc&&(Ic(n-i)Sc?Rc((qc(e)*(a=Pc(r))*qc(n)-qc(r)*(i=Pc(e))*qc(t))/(i*a*o)):(e+r)/2}(n,r,a,o),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(s,r),e=0),t.point(n=a,r=o),i=s},lineEnd:function(){t.lineEnd(),n=r=NaN},clean:function(){return 2-e}}}),(function(t,e,n,r){var i;if(null==t)i=n*Nc,r.point(-Mc,i),r.point(0,i),r.point(Mc,i),r.point(Mc,0),r.point(Mc,-i),r.point(0,-i),r.point(-Mc,-i),r.point(-Mc,0),r.point(-Mc,i);else if(Ic(t[0]-e[0])>Sc){var a=t[0]0,i=Ic(e)>Sc;function a(t,n){return Pc(t)*Pc(n)>e}function o(t,n,r){var i=[1,0,0],a=bu(mu(t),mu(n)),o=vu(a,a),s=a[0],c=o-s*s;if(!c)return!r&&t;var u=e*o/c,l=-e*s/c,h=bu(i,a),f=xu(i,u);_u(f,xu(a,l));var d=h,p=vu(f,d),y=vu(d,d),g=p*p-y*(vu(f,f)-1);if(!(g<0)){var m=$c(g),v=xu(d,(-p-m)/y);if(_u(v,f),v=gu(v),!r)return v;var b,_=t[0],x=n[0],w=t[1],k=n[1];x<_&&(b=_,_=x,x=b);var T=x-_,E=Ic(T-Mc)0^v[1]<(Ic(v[0]-_)Mc^(_<=v[0]&&v[0]<=x)){var C=xu(d,(-p+m)/y);return _u(C,f),[v,gu(C)]}}}function s(e,n){var i=r?t:Mc-t,a=0;return e<-i?a|=1:e>i&&(a|=2),n<-i?a|=4:n>i&&(a|=8),a}return jl(a,(function(t){var e,n,c,u,l;return{lineStart:function(){u=c=!1,l=1},point:function(h,f){var d,p=[h,f],y=a(h,f),g=r?y?0:s(h,f):y?s(h+(h<0?Mc:-Mc),f):0;if(!e&&(u=c=y)&&t.lineStart(),y!==c&&(!(d=o(e,p))||Ol(e,d)||Ol(p,d))&&(p[2]=1),y!==c)l=0,y?(t.lineStart(),d=o(p,e),t.point(d[0],d[1])):(d=o(e,p),t.point(d[0],d[1],2),t.lineEnd()),e=d;else if(i&&e&&r^y){var m;g&n||!(m=o(p,e,!0))||(l=0,r?(t.lineStart(),t.point(m[0][0],m[0][1]),t.point(m[1][0],m[1][1]),t.lineEnd()):(t.point(m[1][0],m[1][1]),t.lineEnd(),t.lineStart(),t.point(m[0][0],m[0][1],3)))}!y||e&&Ol(e,p)||t.point(p[0],p[1]),e=p,c=y,n=g},lineEnd:function(){c&&t.lineEnd(),e=null},clean:function(){return l|(u&&c)<<1}}}),(function(e,r,i,a){Al(a,t,n,i,e,r)}),r?[0,-t]:[-Mc,t-Mc])}var Hl=1e9,$l=-Hl;function Wl(t,e,n,r){function i(i,a){return t<=i&&i<=n&&e<=a&&a<=r}function a(i,a,s,u){var l=0,h=0;if(null==i||(l=o(i,s))!==(h=o(a,s))||c(i,a)<0^s>0)do{u.point(0===l||3===l?t:n,l>1?r:e)}while((l=(l+s+4)%4)!==h);else u.point(a[0],a[1])}function o(r,i){return Ic(r[0]-t)0?0:3:Ic(r[0]-n)0?2:1:Ic(r[1]-e)0?1:0:i>0?3:2}function s(t,e){return c(t.x,e.x)}function c(t,e){var n=o(t,1),r=o(e,1);return n!==r?n-r:0===n?e[1]-t[1]:1===n?t[0]-e[0]:2===n?t[1]-e[1]:e[0]-t[0]}return function(o){var c,u,l,h,f,d,p,y,g,m,v,b=o,_=Dl(),x={point:w,lineStart:function(){x.point=k,u&&u.push(l=[]),m=!0,g=!1,p=y=NaN},lineEnd:function(){c&&(k(h,f),d&&g&&_.rejoin(),c.push(_.result())),x.point=w,g&&b.lineEnd()},polygonStart:function(){b=_,c=[],u=[],v=!0},polygonEnd:function(){var e=function(){for(var e=0,n=0,i=u.length;nr&&(f-a)*(r-o)>(d-o)*(t-a)&&++e:d<=r&&(f-a)*(r-o)<(d-o)*(t-a)&&--e;return e}(),n=v&&e,i=(c=P(c)).length;(n||i)&&(o.polygonStart(),n&&(o.lineStart(),a(null,null,1,o),o.lineEnd()),i&&Ll(c,s,e,a,o),o.polygonEnd()),b=o,c=u=l=null}};function w(t,e){i(t,e)&&b.point(t,e)}function k(a,o){var s=i(a,o);if(u&&l.push([a,o]),m)h=a,f=o,d=s,m=!1,s&&(b.lineStart(),b.point(a,o));else if(s&&g)b.point(a,o);else{var c=[p=Math.max($l,Math.min(Hl,p)),y=Math.max($l,Math.min(Hl,y))],_=[a=Math.max($l,Math.min(Hl,a)),o=Math.max($l,Math.min(Hl,o))];!function(t,e,n,r,i,a){var o,s=t[0],c=t[1],u=0,l=1,h=e[0]-s,f=e[1]-c;if(o=n-s,h||!(o>0)){if(o/=h,h<0){if(o0){if(o>l)return;o>u&&(u=o)}if(o=i-s,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>u&&(u=o)}else if(h>0){if(o0)){if(o/=f,f<0){if(o0){if(o>l)return;o>u&&(u=o)}if(o=a-c,f||!(o<0)){if(o/=f,f<0){if(o>l)return;o>u&&(u=o)}else if(f>0){if(o0&&(t[0]=s+u*h,t[1]=c+u*f),l<1&&(e[0]=s+l*h,e[1]=c+l*f),!0}}}}}(c,_,t,e,n,r)?s&&(b.lineStart(),b.point(a,o),v=!1):(g||(b.lineStart(),b.point(c[0],c[1])),b.point(_[0],_[1]),s||b.lineEnd(),v=!1)}p=a,y=o,g=s}return x}}function Vl(){var t,e,n,r=0,i=0,a=960,o=500;return n={stream:function(n){return t&&e===n?t:t=Wl(r,i,a,o)(e=n)},extent:function(s){return arguments.length?(r=+s[0][0],i=+s[0][1],a=+s[1][0],o=+s[1][1],t=e=null,n):[[r,i],[a,o]]}}}var Gl,Xl,Zl,Ql=kc(),Kl={sphere:Zc,point:Zc,lineStart:function(){Kl.point=th,Kl.lineEnd=Jl},lineEnd:Zc,polygonStart:Zc,polygonEnd:Zc};function Jl(){Kl.point=Kl.lineEnd=Zc}function th(t,e){Gl=t*=Lc,Xl=qc(e*=Lc),Zl=Pc(e),Kl.point=eh}function eh(t,e){t*=Lc;var n=qc(e*=Lc),r=Pc(e),i=Ic(t-Gl),a=Pc(i),o=r*qc(i),s=Zl*n-Xl*r*a,c=Xl*n+Zl*r*a;Ql.add(Fc($c(o*o+s*s),c)),Gl=t,Xl=n,Zl=r}function nh(t){return Ql.reset(),nu(t,Kl),+Ql}var rh=[null,null],ih={type:"LineString",coordinates:rh};function ah(t,e){return rh[0]=t,rh[1]=e,nh(ih)}var oh={Feature:function(t,e){return ch(t.geometry,e)},FeatureCollection:function(t,e){for(var n=t.features,r=-1,i=n.length;++r0&&(i=ah(t[a],t[a-1]))>0&&n<=i&&r<=i&&(n+r-i)*(1-Math.pow((n-r)/i,2))Sc})).map(c)).concat(k(jc(a/d)*d,i,d).filter((function(t){return Ic(t%y)>Sc})).map(u))}return m.lines=function(){return v().map((function(t){return{type:"LineString",coordinates:t}}))},m.outline=function(){return{type:"Polygon",coordinates:[l(r).concat(h(o).slice(1),l(n).reverse().slice(1),h(s).reverse().slice(1))]}},m.extent=function(t){return arguments.length?m.extentMajor(t).extentMinor(t):m.extentMinor()},m.extentMajor=function(t){return arguments.length?(r=+t[0][0],n=+t[1][0],s=+t[0][1],o=+t[1][1],r>n&&(t=r,r=n,n=t),s>o&&(t=s,s=o,o=t),m.precision(g)):[[r,s],[n,o]]},m.extentMinor=function(n){return arguments.length?(e=+n[0][0],t=+n[1][0],a=+n[0][1],i=+n[1][1],e>t&&(n=e,e=t,t=n),a>i&&(n=a,a=i,i=n),m.precision(g)):[[e,a],[t,i]]},m.step=function(t){return arguments.length?m.stepMajor(t).stepMinor(t):m.stepMinor()},m.stepMajor=function(t){return arguments.length?(p=+t[0],y=+t[1],m):[p,y]},m.stepMinor=function(t){return arguments.length?(f=+t[0],d=+t[1],m):[f,d]},m.precision=function(f){return arguments.length?(g=+f,c=yh(a,i,90),u=gh(e,t,g),l=yh(s,o,90),h=gh(r,n,g),m):g},m.extentMajor([[-180,-89.999999],[180,89.999999]]).extentMinor([[-180,-80.000001],[180,80.000001]])}function vh(){return mh()()}function bh(t,e){var n=t[0]*Lc,r=t[1]*Lc,i=e[0]*Lc,a=e[1]*Lc,o=Pc(r),s=qc(r),c=Pc(a),u=qc(a),l=o*Pc(n),h=o*qc(n),f=c*Pc(i),d=c*qc(i),p=2*Gc($c(Xc(a-r)+o*c*Xc(i-n))),y=qc(p),g=p?function(t){var e=qc(t*=p)/y,n=qc(p-t)/y,r=n*l+e*f,i=n*h+e*d,a=n*s+e*u;return[Fc(i,r)*Bc,Fc(a,$c(r*r+i*i))*Bc]}:function(){return[n*Bc,r*Bc]};return g.distance=p,g}function _h(t){return t}var xh,wh,kh,Th,Eh=kc(),Ch=kc(),Sh={point:Zc,lineStart:Zc,lineEnd:Zc,polygonStart:function(){Sh.lineStart=Ah,Sh.lineEnd=Dh},polygonEnd:function(){Sh.lineStart=Sh.lineEnd=Sh.point=Zc,Eh.add(Ic(Ch)),Ch.reset()},result:function(){var t=Eh/2;return Eh.reset(),t}};function Ah(){Sh.point=Mh}function Mh(t,e){Sh.point=Nh,xh=kh=t,wh=Th=e}function Nh(t,e){Ch.add(Th*t-kh*e),kh=t,Th=e}function Dh(){Nh(xh,wh)}const Oh=Sh;var Bh=1/0,Lh=Bh,Ih=-Bh,Rh=Ih,Fh={point:function(t,e){tIh&&(Ih=t),eRh&&(Rh=e)},lineStart:Zc,lineEnd:Zc,polygonStart:Zc,polygonEnd:Zc,result:function(){var t=[[Bh,Lh],[Ih,Rh]];return Ih=Rh=-(Lh=Bh=1/0),t}};const Ph=Fh;var jh,Yh,zh,Uh,qh=0,Hh=0,$h=0,Wh=0,Vh=0,Gh=0,Xh=0,Zh=0,Qh=0,Kh={point:Jh,lineStart:tf,lineEnd:rf,polygonStart:function(){Kh.lineStart=af,Kh.lineEnd=of},polygonEnd:function(){Kh.point=Jh,Kh.lineStart=tf,Kh.lineEnd=rf},result:function(){var t=Qh?[Xh/Qh,Zh/Qh]:Gh?[Wh/Gh,Vh/Gh]:$h?[qh/$h,Hh/$h]:[NaN,NaN];return qh=Hh=$h=Wh=Vh=Gh=Xh=Zh=Qh=0,t}};function Jh(t,e){qh+=t,Hh+=e,++$h}function tf(){Kh.point=ef}function ef(t,e){Kh.point=nf,Jh(zh=t,Uh=e)}function nf(t,e){var n=t-zh,r=e-Uh,i=$c(n*n+r*r);Wh+=i*(zh+t)/2,Vh+=i*(Uh+e)/2,Gh+=i,Jh(zh=t,Uh=e)}function rf(){Kh.point=Jh}function af(){Kh.point=sf}function of(){cf(jh,Yh)}function sf(t,e){Kh.point=cf,Jh(jh=zh=t,Yh=Uh=e)}function cf(t,e){var n=t-zh,r=e-Uh,i=$c(n*n+r*r);Wh+=i*(zh+t)/2,Vh+=i*(Uh+e)/2,Gh+=i,Xh+=(i=Uh*t-zh*e)*(zh+t),Zh+=i*(Uh+e),Qh+=3*i,Jh(zh=t,Uh=e)}const uf=Kh;function lf(t){this._context=t}lf.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,e){switch(this._point){case 0:this._context.moveTo(t,e),this._point=1;break;case 1:this._context.lineTo(t,e);break;default:this._context.moveTo(t+this._radius,e),this._context.arc(t,e,this._radius,0,Oc)}},result:Zc};var hf,ff,df,pf,yf,gf=kc(),mf={point:Zc,lineStart:function(){mf.point=vf},lineEnd:function(){hf&&bf(ff,df),mf.point=Zc},polygonStart:function(){hf=!0},polygonEnd:function(){hf=null},result:function(){var t=+gf;return gf.reset(),t}};function vf(t,e){mf.point=bf,ff=pf=t,df=yf=e}function bf(t,e){pf-=t,yf-=e,gf.add($c(pf*pf+yf*yf)),pf=t,yf=e}const _f=mf;function xf(){this._string=[]}function wf(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function kf(t,e){var n,r,i=4.5;function a(t){return t&&("function"==typeof i&&r.pointRadius(+i.apply(this,arguments)),nu(t,n(r))),r.result()}return a.area=function(t){return nu(t,n(Oh)),Oh.result()},a.measure=function(t){return nu(t,n(_f)),_f.result()},a.bounds=function(t){return nu(t,n(Ph)),Ph.result()},a.centroid=function(t){return nu(t,n(uf)),uf.result()},a.projection=function(e){return arguments.length?(n=null==e?(t=null,_h):(t=e).stream,a):t},a.context=function(t){return arguments.length?(r=null==t?(e=null,new xf):new lf(e=t),"function"!=typeof i&&r.pointRadius(i),a):e},a.pointRadius=function(t){return arguments.length?(i="function"==typeof t?t:(r.pointRadius(+t),+t),a):i},a.projection(t).context(e)}function Tf(t){return{stream:Ef(t)}}function Ef(t){return function(e){var n=new Cf;for(var r in t)n[r]=t[r];return n.stream=e,n}}function Cf(){}function Sf(t,e,n){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),nu(n,t.stream(Ph)),e(Ph.result()),null!=r&&t.clipExtent(r),t}function Af(t,e,n){return Sf(t,(function(n){var r=e[1][0]-e[0][0],i=e[1][1]-e[0][1],a=Math.min(r/(n[1][0]-n[0][0]),i/(n[1][1]-n[0][1])),o=+e[0][0]+(r-a*(n[1][0]+n[0][0]))/2,s=+e[0][1]+(i-a*(n[1][1]+n[0][1]))/2;t.scale(150*a).translate([o,s])}),n)}function Mf(t,e,n){return Af(t,[[0,0],e],n)}function Nf(t,e,n){return Sf(t,(function(n){var r=+e,i=r/(n[1][0]-n[0][0]),a=(r-i*(n[1][0]+n[0][0]))/2,o=-i*n[0][1];t.scale(150*i).translate([a,o])}),n)}function Df(t,e,n){return Sf(t,(function(n){var r=+e,i=r/(n[1][1]-n[0][1]),a=-i*n[0][0],o=(r-i*(n[1][1]+n[0][1]))/2;t.scale(150*i).translate([a,o])}),n)}xf.prototype={_radius:4.5,_circle:wf(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,e){switch(this._point){case 0:this._string.push("M",t,",",e),this._point=1;break;case 1:this._string.push("L",t,",",e);break;default:null==this._circle&&(this._circle=wf(this._radius)),this._string.push("M",t,",",e,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},Cf.prototype={constructor:Cf,point:function(t,e){this.stream.point(t,e)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Of=Pc(30*Lc);function Bf(t,e){return+e?function(t,e){function n(r,i,a,o,s,c,u,l,h,f,d,p,y,g){var m=u-r,v=l-i,b=m*m+v*v;if(b>4*e&&y--){var _=o+f,x=s+d,w=c+p,k=$c(_*_+x*x+w*w),T=Gc(w/=k),E=Ic(Ic(w)-1)e||Ic((m*M+v*N)/b-.5)>.3||o*f+s*d+c*p2?t[2]%360*Lc:0,M()):[g*Bc,m*Bc,v*Bc]},S.angle=function(t){return arguments.length?(b=t%360*Lc,M()):b*Bc},S.reflectX=function(t){return arguments.length?(_=t?-1:1,M()):_<0},S.reflectY=function(t){return arguments.length?(x=t?-1:1,M()):x<0},S.precision=function(t){return arguments.length?(o=Bf(s,C=t*t),N()):$c(C)},S.fitExtent=function(t,e){return Af(S,t,e)},S.fitSize=function(t,e){return Mf(S,t,e)},S.fitWidth=function(t,e){return Nf(S,t,e)},S.fitHeight=function(t,e){return Df(S,t,e)},function(){return e=t.apply(this,arguments),S.invert=e.invert&&A,M()}}function jf(t){var e=0,n=Mc/3,r=Pf(t),i=r(e,n);return i.parallels=function(t){return arguments.length?r(e=t[0]*Lc,n=t[1]*Lc):[e*Bc,n*Bc]},i}function Yf(t,e){var n=qc(t),r=(n+qc(e))/2;if(Ic(r)=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:o).invert(t)},l.stream=function(n){return t&&e===n?t:(r=[o.stream(e=n),s.stream(n),c.stream(n)],i=r.length,t={point:function(t,e){for(var n=-1;++n0?e<-Nc+Sc&&(e=-Nc+Sc):e>Nc-Sc&&(e=Nc-Sc);var n=i/Uc(Jf(e),r);return[n*qc(r*t),i-n*Pc(r*t)]}return a.invert=function(t,e){var n=i-e,a=Hc(r)*$c(t*t+n*n),o=Fc(t,Ic(n))*Hc(n);return n*r<0&&(o-=Mc*Hc(t)*Hc(n)),[o/r,2*Rc(Uc(i/a,1/r))-Nc]},a}function ed(){return jf(td).scale(109.5).parallels([30,30])}function nd(t,e){return[t,e]}function rd(){return Ff(nd).scale(152.63)}function id(t,e){var n=Pc(t),r=t===e?qc(t):(n-Pc(e))/(e-t),i=n/r+t;if(Ic(r)2?t[2]+90:90]):[(t=n())[0],t[1],t[2]-90]},n([0,0,90]).scale(159.155)}function Td(t,e){return t.parent===e.parent?1:2}function Ed(t,e){return t+e.x}function Cd(t,e){return Math.max(t,e.y)}function Sd(){var t=Td,e=1,n=1,r=!1;function i(i){var a,o=0;i.eachAfter((function(e){var n=e.children;n?(e.x=function(t){return t.reduce(Ed,0)/t.length}(n),e.y=function(t){return 1+t.reduce(Cd,0)}(n)):(e.x=a?o+=t(e,a):0,e.y=0,a=e)}));var s=function(t){for(var e;e=t.children;)t=e[0];return t}(i),c=function(t){for(var e;e=t.children;)t=e[e.length-1];return t}(i),u=s.x-t(s,c)/2,l=c.x+t(c,s)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*e,t.y=(i.y-t.y)*n}:function(t){t.x=(t.x-u)/(l-u)*e,t.y=(1-(i.y?t.y/i.y:1))*n})}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}function Ad(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function Md(t,e){var n,r,i,a,o,s=new Bd(t),c=+t.value&&(s.value=t.value),u=[s];for(null==e&&(e=Nd);n=u.pop();)if(c&&(n.value=+n.data.value),(i=e(n.data))&&(o=i.length))for(n.children=new Array(o),a=o-1;a>=0;--a)u.push(r=n.children[a]=new Bd(i[a])),r.parent=n,r.depth=n.depth+1;return s.eachBefore(Od)}function Nd(t){return t.children}function Dd(t){t.data=t.data.data}function Od(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function Bd(t){this.data=t,this.depth=this.height=0,this.parent=null}hd.invert=function(t,e){for(var n,r=e,i=r*r,a=i*i*i,o=0;o<12&&(a=(i=(r-=n=(r*(od+sd*i+a*(cd+ud*i))-e)/(od+3*sd*i+a*(7*cd+9*ud*i)))*r)*i*i,!(Ic(n)Sc&&--i>0);return[t/(.8707+(a=r*r)*(a*(a*a*a*(.003971-.001529*a)-.013791)-.131979)),r]},vd.invert=$f(Gc),_d.invert=$f((function(t){return 2*Rc(t)})),wd.invert=function(t,e){return[-e,2*Rc(Yc(t))-Nc]},Bd.prototype=Md.prototype={constructor:Bd,count:function(){return this.eachAfter(Ad)},each:function(t){var e,n,r,i,a=this,o=[a];do{for(e=o.reverse(),o=[];a=e.pop();)if(t(a),n=a.children)for(r=0,i=n.length;r=0;--n)i.push(e[n]);return this},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;for(t=n.pop(),e=r.pop();t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){var t=[];return this.each((function(e){t.push(e)})),t},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return Md(this).eachBefore(Dd)}};var Ld=Array.prototype.slice;function Id(t){for(var e,n,r=0,i=(t=function(t){for(var e,n,r=t.length;r;)n=Math.random()*r--|0,e=t[r],t[r]=t[n],t[n]=e;return t}(Ld.call(t))).length,a=[];r0&&n*n>r*r+i*i}function jd(t,e){for(var n=0;n(o*=o)?(r=(u+o-i)/(2*u),a=Math.sqrt(Math.max(0,o/u-r*r)),n.x=t.x-r*s-a*c,n.y=t.y-r*c+a*s):(r=(u+i-o)/(2*u),a=Math.sqrt(Math.max(0,i/u-r*r)),n.x=e.x+r*s-a*c,n.y=e.y+r*c+a*s)):(n.x=e.x+n.r,n.y=e.y)}function Hd(t,e){var n=t.r+e.r-1e-6,r=e.x-t.x,i=e.y-t.y;return n>0&&n*n>r*r+i*i}function $d(t){var e=t._,n=t.next._,r=e.r+n.r,i=(e.x*n.r+n.x*e.r)/r,a=(e.y*n.r+n.y*e.r)/r;return i*i+a*a}function Wd(t){this._=t,this.next=null,this.previous=null}function Vd(t){if(!(i=t.length))return 0;var e,n,r,i,a,o,s,c,u,l,h;if((e=t[0]).x=0,e.y=0,!(i>1))return e.r;if(n=t[1],e.x=-n.r,n.x=e.r,n.y=0,!(i>2))return e.r+n.r;qd(n,e,r=t[2]),e=new Wd(e),n=new Wd(n),r=new Wd(r),e.next=r.previous=n,n.next=e.previous=r,r.next=n.previous=e;t:for(s=3;s0)throw new Error("cycle");return a}return n.id=function(e){return arguments.length?(t=Zd(e),n):t},n.parentId=function(t){return arguments.length?(e=Zd(t),n):e},n}function fp(t,e){return t.parent===e.parent?1:2}function dp(t){var e=t.children;return e?e[0]:t.t}function pp(t){var e=t.children;return e?e[e.length-1]:t.t}function yp(t,e,n){var r=n/(e.i-t.i);e.c-=r,e.s+=n,t.c+=r,e.z+=n,e.m+=n}function gp(t,e,n){return t.a.parent===e.parent?t.a:n}function mp(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function vp(){var t=fp,e=1,n=1,r=null;function i(i){var c=function(t){for(var e,n,r,i,a,o=new mp(t,0),s=[o];e=s.pop();)if(r=e._.children)for(e.children=new Array(a=r.length),i=a-1;i>=0;--i)s.push(n=e.children[i]=new mp(r[i],i)),n.parent=e;return(o.parent=new mp(null,0)).children=[o],o}(i);if(c.eachAfter(a),c.parent.m=-c.z,c.eachBefore(o),r)i.eachBefore(s);else{var u=i,l=i,h=i;i.eachBefore((function(t){t.xl.x&&(l=t),t.depth>h.depth&&(h=t)}));var f=u===l?1:t(u,l)/2,d=f-u.x,p=e/(l.x+f+d),y=n/(h.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*y}))}return i}function a(e){var n=e.children,r=e.parent.children,i=e.i?r[e.i-1]:null;if(n){!function(t){for(var e,n=0,r=0,i=t.children,a=i.length;--a>=0;)(e=i[a]).z+=n,e.m+=n,n+=e.s+(r+=e.c)}(e);var a=(n[0].z+n[n.length-1].z)/2;i?(e.z=i.z+t(e._,i._),e.m=e.z-a):e.z=a}else i&&(e.z=i.z+t(e._,i._));e.parent.A=function(e,n,r){if(n){for(var i,a=e,o=e,s=n,c=a.parent.children[0],u=a.m,l=o.m,h=s.m,f=c.m;s=pp(s),a=dp(a),s&&a;)c=dp(c),(o=pp(o)).a=e,(i=s.z+h-a.z-u+t(s._,a._))>0&&(yp(gp(s,e,r),e,i),u+=i,l+=i),h+=s.m,u+=a.m,f+=c.m,l+=o.m;s&&!pp(o)&&(o.t=s,o.m+=h-l),a&&!dp(c)&&(c.t=a,c.m+=u-f,r=e)}return r}(e,i,e.parent.A||r[0])}function o(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}function bp(t,e,n,r,i){for(var a,o=t.children,s=-1,c=o.length,u=t.value&&(i-n)/t.value;++sf&&(f=s),g=l*l*y,(d=Math.max(f/g,g/h))>p){l-=s;break}p=d}m.push(o={value:l,dice:c1?e:1)},n}(_p);function kp(){var t=wp,e=!1,n=1,r=1,i=[0],a=Qd,o=Qd,s=Qd,c=Qd,u=Qd;function l(t){return t.x0=t.y0=0,t.x1=n,t.y1=r,t.eachBefore(h),i=[0],e&&t.eachBefore(ip),t}function h(e){var n=i[e.depth],r=e.x0+n,l=e.y0+n,h=e.x1-n,f=e.y1-n;h=n-1){var l=s[e];return l.x0=i,l.y0=a,l.x1=o,void(l.y1=c)}for(var h=u[e],f=r/2+h,d=e+1,p=n-1;d>>1;u[y]c-a){var v=(i*m+o*g)/r;t(e,d,g,i,a,v,c),t(d,n,m,v,a,o,c)}else{var b=(a*m+c*g)/r;t(e,d,g,i,a,o,b),t(d,n,m,i,b,o,c)}}(0,c,t.value,e,n,r,i)}function Ep(t,e,n,r,i){(1&t.depth?bp:ap)(t,e,n,r,i)}const Cp=function t(e){function n(t,n,r,i,a){if((o=t._squarify)&&o.ratio===e)for(var o,s,c,u,l,h=-1,f=o.length,d=t.value;++h1?e:1)},n}(_p);function Sp(t){var e=t.length;return function(n){return t[Math.max(0,Math.min(e-1,Math.floor(n*e)))]}}function Ap(t,e){var n=dn(+t,+e);return function(t){var e=n(t);return e-360*Math.floor(e/360)}}function Mp(t,e){return t=+t,e=+e,function(n){return Math.round(t*(1-n)+e*n)}}var Np=Math.SQRT2;function Dp(t){return((t=Math.exp(t))+1/t)/2}function Op(t,e){var n,r,i=t[0],a=t[1],o=t[2],s=e[0],c=e[1],u=e[2],l=s-i,h=c-a,f=l*l+h*h;if(f<1e-12)r=Math.log(u/o)/Np,n=function(t){return[i+t*l,a+t*h,o*Math.exp(Np*t*r)]};else{var d=Math.sqrt(f),p=(u*u-o*o+4*f)/(2*o*2*d),y=(u*u-o*o-4*f)/(2*u*2*d),g=Math.log(Math.sqrt(p*p+1)-p),m=Math.log(Math.sqrt(y*y+1)-y);r=(m-g)/Np,n=function(t){var e,n=t*r,s=Dp(g),c=o/(2*d)*(s*(e=Np*n+g,((e=Math.exp(2*e))-1)/(e+1))-function(t){return((t=Math.exp(t))-1/t)/2}(g));return[i+c*l,a+c*h,o*s/Dp(Np*n+g)]}}return n.duration=1e3*r,n}function Bp(t){return function(e,n){var r=t((e=an(e)).h,(n=an(n)).h),i=pn(e.s,n.s),a=pn(e.l,n.l),o=pn(e.opacity,n.opacity);return function(t){return e.h=r(t),e.s=i(t),e.l=a(t),e.opacity=o(t),e+""}}}const Lp=Bp(dn);var Ip=Bp(pn);function Rp(t,e){var n=pn((t=Ta(t)).l,(e=Ta(e)).l),r=pn(t.a,e.a),i=pn(t.b,e.b),a=pn(t.opacity,e.opacity);return function(e){return t.l=n(e),t.a=r(e),t.b=i(e),t.opacity=a(e),t+""}}function Fp(t){return function(e,n){var r=t((e=Oa(e)).h,(n=Oa(n)).h),i=pn(e.c,n.c),a=pn(e.l,n.l),o=pn(e.opacity,n.opacity);return function(t){return e.h=r(t),e.c=i(t),e.l=a(t),e.opacity=o(t),e+""}}}const Pp=Fp(dn);var jp=Fp(pn);function Yp(t){return function e(n){function r(e,r){var i=t((e=Ha(e)).h,(r=Ha(r)).h),a=pn(e.s,r.s),o=pn(e.l,r.l),s=pn(e.opacity,r.opacity);return function(t){return e.h=i(t),e.s=a(t),e.l=o(Math.pow(t,n)),e.opacity=s(t),e+""}}return n=+n,r.gamma=e,r}(1)}const zp=Yp(dn);var Up=Yp(pn);function qp(t,e){for(var n=0,r=e.length-1,i=e[0],a=new Array(r<0?0:r);n1&&Vp(t[n[r-2]],t[n[r-1]],t[i])<=0;)--r;n[r++]=i}return n.slice(0,r)}function Zp(t){if((n=t.length)<3)return null;var e,n,r=new Array(n),i=new Array(n);for(e=0;e=0;--e)u.push(t[r[a[e]][2]]);for(e=+s;es!=u>s&&o<(c-n)*(s-r)/(u-r)+n&&(l=!l),c=n,u=r;return l}function Kp(t){for(var e,n,r=-1,i=t.length,a=t[i-1],o=a[0],s=a[1],c=0;++r1);return t+n*a*Math.sqrt(-2*Math.log(i)/i)}}return n.source=t,n}(Jp),ny=function t(e){function n(){var t=ey.source(e).apply(this,arguments);return function(){return Math.exp(t())}}return n.source=t,n}(Jp),ry=function t(e){function n(t){return function(){for(var n=0,r=0;rr&&(e=n,n=r,r=e),function(t){return Math.max(n,Math.min(r,t))}}function xy(t,e,n){var r=t[0],i=t[1],a=e[0],o=e[1];return i2?wy:xy,i=a=null,h}function h(e){return isNaN(e=+e)?n:(i||(i=r(o.map(t),s,c)))(t(u(e)))}return h.invert=function(n){return u(e((a||(a=r(s,o.map(t),Tn)))(n)))},h.domain=function(t){return arguments.length?(o=uy.call(t,gy),u===vy||(u=_y(o)),l()):o.slice()},h.range=function(t){return arguments.length?(s=ly.call(t),l()):s.slice()},h.rangeRound=function(t){return s=ly.call(t),c=Mp,l()},h.clamp=function(t){return arguments.length?(u=t?_y(o):vy,h):u!==vy},h.interpolate=function(t){return arguments.length?(c=t,l()):c},h.unknown=function(t){return arguments.length?(n=t,h):n},function(n,r){return t=n,e=r,l()}}function Ey(t,e){return Ty()(t,e)}function Cy(t,e,n,r){var i,a=M(t,e,n);switch((r=cc(null==r?",f":r)).type){case"s":var o=Math.max(Math.abs(t),Math.abs(e));return null!=r.precision||isNaN(i=xc(a,o))||(r.precision=i),yc(r,o);case"":case"e":case"g":case"p":case"r":null!=r.precision||isNaN(i=wc(a,Math.max(Math.abs(t),Math.abs(e))))||(r.precision=i-("e"===r.type));break;case"f":case"%":null!=r.precision||isNaN(i=_c(a))||(r.precision=i-2*("%"===r.type))}return pc(r)}function Sy(t){var e=t.domain;return t.ticks=function(t){var n=e();return S(n[0],n[n.length-1],null==t?10:t)},t.tickFormat=function(t,n){var r=e();return Cy(r[0],r[r.length-1],null==t?10:t,n)},t.nice=function(n){null==n&&(n=10);var r,i=e(),a=0,o=i.length-1,s=i[a],c=i[o];return c0?r=A(s=Math.floor(s/r)*r,c=Math.ceil(c/r)*r,n):r<0&&(r=A(s=Math.ceil(s*r)/r,c=Math.floor(c*r)/r,n)),r>0?(i[a]=Math.floor(s/r)*r,i[o]=Math.ceil(c/r)*r,e(i)):r<0&&(i[a]=Math.ceil(s*r)/r,i[o]=Math.floor(c*r)/r,e(i)),t},t}function Ay(){var t=Ey(vy,vy);return t.copy=function(){return ky(t,Ay())},oy.apply(t,arguments),Sy(t)}function My(t){var e;function n(t){return isNaN(t=+t)?e:t}return n.invert=n,n.domain=n.range=function(e){return arguments.length?(t=uy.call(e,gy),n):t.slice()},n.unknown=function(t){return arguments.length?(e=t,n):e},n.copy=function(){return My(t).unknown(e)},t=arguments.length?uy.call(t,gy):[0,1],Sy(n)}function Ny(t,e){var n,r=0,i=(t=t.slice()).length-1,a=t[r],o=t[i];return o0){for(;fc)break;y.push(h)}}else for(;f=1;--l)if(!((h=u*l)c)break;y.push(h)}}else y=S(f,d,Math.min(d-f,p)).map(n);return r?y.reverse():y},r.tickFormat=function(t,i){if(null==i&&(i=10===a?".0e":","),"function"!=typeof i&&(i=pc(i)),t===1/0)return i;null==t&&(t=10);var o=Math.max(1,a*t/r.ticks().length);return function(t){var r=t/n(Math.round(e(t)));return r*a0?r[i-1]:e[0],i=r?[i[r-1],n]:[i[o-1],i[o]]},o.unknown=function(e){return arguments.length?(t=e,o):o},o.thresholds=function(){return i.slice()},o.copy=function(){return Zy().domain([e,n]).range(a).unknown(t)},oy.apply(Sy(o),arguments)}function Qy(){var t,e=[.5],n=[0,1],r=1;function i(i){return i<=i?n[u(e,i,0,r)]:t}return i.domain=function(t){return arguments.length?(e=ly.call(t),r=Math.min(e.length,n.length-1),i):e.slice()},i.range=function(t){return arguments.length?(n=ly.call(t),r=Math.min(e.length,n.length-1),i):n.slice()},i.invertExtent=function(t){var r=n.indexOf(t);return[e[r-1],e[r]]},i.unknown=function(e){return arguments.length?(t=e,i):t},i.copy=function(){return Qy().domain(e).range(n).unknown(t)},oy.apply(i,arguments)}var Ky=new Date,Jy=new Date;function tg(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=function(e){return t(e=new Date(+e)),e},i.ceil=function(n){return t(n=new Date(n-1)),e(n,1),t(n),n},i.round=function(t){var e=i(t),n=i.ceil(t);return t-e0))return s;do{s.push(o=new Date(+n)),e(n,a),t(n)}while(o=e)for(;t(e),!n(e);)e.setTime(e-1)}),(function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););}))},n&&(i.count=function(e,r){return Ky.setTime(+e),Jy.setTime(+r),t(Ky),t(Jy),Math.floor(n(Ky,Jy))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(e){return r(e)%t==0}:function(e){return i.count(0,e)%t==0}):i:null}),i}var eg=tg((function(t){t.setMonth(0,1),t.setHours(0,0,0,0)}),(function(t,e){t.setFullYear(t.getFullYear()+e)}),(function(t,e){return e.getFullYear()-t.getFullYear()}),(function(t){return t.getFullYear()}));eg.every=function(t){return isFinite(t=Math.floor(t))&&t>0?tg((function(e){e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),(function(e,n){e.setFullYear(e.getFullYear()+n*t)})):null};const ng=eg;var rg=eg.range,ig=tg((function(t){t.setDate(1),t.setHours(0,0,0,0)}),(function(t,e){t.setMonth(t.getMonth()+e)}),(function(t,e){return e.getMonth()-t.getMonth()+12*(e.getFullYear()-t.getFullYear())}),(function(t){return t.getMonth()}));const ag=ig;var og=ig.range,sg=1e3,cg=6e4,ug=36e5,lg=864e5,hg=6048e5;function fg(t){return tg((function(e){e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+7*e)}),(function(t,e){return(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*cg)/hg}))}var dg=fg(0),pg=fg(1),yg=fg(2),gg=fg(3),mg=fg(4),vg=fg(5),bg=fg(6),_g=dg.range,xg=pg.range,wg=yg.range,kg=gg.range,Tg=mg.range,Eg=vg.range,Cg=bg.range,Sg=tg((function(t){t.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+e)}),(function(t,e){return(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*cg)/lg}),(function(t){return t.getDate()-1}));const Ag=Sg;var Mg=Sg.range,Ng=tg((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*sg-t.getMinutes()*cg)}),(function(t,e){t.setTime(+t+e*ug)}),(function(t,e){return(e-t)/ug}),(function(t){return t.getHours()}));const Dg=Ng;var Og=Ng.range,Bg=tg((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*sg)}),(function(t,e){t.setTime(+t+e*cg)}),(function(t,e){return(e-t)/cg}),(function(t){return t.getMinutes()}));const Lg=Bg;var Ig=Bg.range,Rg=tg((function(t){t.setTime(t-t.getMilliseconds())}),(function(t,e){t.setTime(+t+e*sg)}),(function(t,e){return(e-t)/sg}),(function(t){return t.getUTCSeconds()}));const Fg=Rg;var Pg=Rg.range,jg=tg((function(){}),(function(t,e){t.setTime(+t+e)}),(function(t,e){return e-t}));jg.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?tg((function(e){e.setTime(Math.floor(e/t)*t)}),(function(e,n){e.setTime(+e+n*t)}),(function(e,n){return(n-e)/t})):jg:null};const Yg=jg;var zg=jg.range;function Ug(t){return tg((function(e){e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+7*e)}),(function(t,e){return(e-t)/hg}))}var qg=Ug(0),Hg=Ug(1),$g=Ug(2),Wg=Ug(3),Vg=Ug(4),Gg=Ug(5),Xg=Ug(6),Zg=qg.range,Qg=Hg.range,Kg=$g.range,Jg=Wg.range,tm=Vg.range,em=Gg.range,nm=Xg.range,rm=tg((function(t){t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+e)}),(function(t,e){return(e-t)/lg}),(function(t){return t.getUTCDate()-1}));const im=rm;var am=rm.range,om=tg((function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)}),(function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()}),(function(t){return t.getUTCFullYear()}));om.every=function(t){return isFinite(t=Math.floor(t))&&t>0?tg((function(e){e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),(function(e,n){e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null};const sm=om;var cm=om.range;function um(t){if(0<=t.y&&t.y<100){var e=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function lm(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function hm(t,e,n){return{y:t,m:e,d:n,H:0,M:0,S:0,L:0}}function fm(t){var e=t.dateTime,n=t.date,r=t.time,i=t.periods,a=t.days,o=t.shortDays,s=t.months,c=t.shortMonths,u=Tm(i),l=Em(i),h=Tm(a),f=Em(a),d=Tm(o),p=Em(o),y=Tm(s),g=Em(s),m=Tm(c),v=Em(c),b={a:function(t){return o[t.getDay()]},A:function(t){return a[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:Wm,e:Wm,f:Qm,g:cv,G:lv,H:Vm,I:Gm,j:Xm,L:Zm,m:Km,M:Jm,p:function(t){return i[+(t.getHours()>=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:Bv,s:Lv,S:tv,u:ev,U:nv,V:iv,w:av,W:ov,x:null,X:null,y:sv,Y:uv,Z:hv,"%":Ov},_={a:function(t){return o[t.getUTCDay()]},A:function(t){return a[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:fv,e:fv,f:mv,g:Av,G:Nv,H:dv,I:pv,j:yv,L:gv,m:vv,M:bv,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:Bv,s:Lv,S:_v,u:xv,U:wv,V:Tv,w:Ev,W:Cv,x:null,X:null,y:Sv,Y:Mv,Z:Dv,"%":Ov},x={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p[r[0].toLowerCase()],n+r[0].length):-1},A:function(t,e,n){var r=h.exec(e.slice(n));return r?(t.w=f[r[0].toLowerCase()],n+r[0].length):-1},b:function(t,e,n){var r=m.exec(e.slice(n));return r?(t.m=v[r[0].toLowerCase()],n+r[0].length):-1},B:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=g[r[0].toLowerCase()],n+r[0].length):-1},c:function(t,n,r){return T(t,e,n,r)},d:Rm,e:Rm,f:Um,g:Om,G:Dm,H:Pm,I:Pm,j:Fm,L:zm,m:Im,M:jm,p:function(t,e,n){var r=u.exec(e.slice(n));return r?(t.p=l[r[0].toLowerCase()],n+r[0].length):-1},q:Lm,Q:Hm,s:$m,S:Ym,u:Sm,U:Am,V:Mm,w:Cm,W:Nm,x:function(t,e,r){return T(t,n,e,r)},X:function(t,e,n){return T(t,r,e,n)},y:Om,Y:Dm,Z:Bm,"%":qm};function w(t,e){return function(n){var r,i,a,o=[],s=-1,c=0,u=t.length;for(n instanceof Date||(n=new Date(+n));++s53)return null;"w"in a||(a.w=1),"Z"in a?(i=(r=lm(hm(a.y,0,1))).getUTCDay(),r=i>4||0===i?Hg.ceil(r):Hg(r),r=im.offset(r,7*(a.V-1)),a.y=r.getUTCFullYear(),a.m=r.getUTCMonth(),a.d=r.getUTCDate()+(a.w+6)%7):(i=(r=um(hm(a.y,0,1))).getDay(),r=i>4||0===i?pg.ceil(r):pg(r),r=Ag.offset(r,7*(a.V-1)),a.y=r.getFullYear(),a.m=r.getMonth(),a.d=r.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),i="Z"in a?lm(hm(a.y,0,1)).getUTCDay():um(hm(a.y,0,1)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(i+5)%7:a.w+7*a.U-(i+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,lm(a)):um(a)}}function T(t,e,n,r){for(var i,a,o=0,s=e.length,c=n.length;o=c)return-1;if(37===(i=e.charCodeAt(o++))){if(i=e.charAt(o++),!(a=x[i in vm?e.charAt(o++):i])||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return b.x=w(n,b),b.X=w(r,b),b.c=w(e,b),_.x=w(n,_),_.X=w(r,_),_.c=w(e,_),{format:function(t){var e=w(t+="",b);return e.toString=function(){return t},e},parse:function(t){var e=k(t+="",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+="",_);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+="",!0);return e.toString=function(){return t},e}}}var dm,pm,ym,gm,mm,vm={"-":"",_:" ",0:"0"},bm=/^\s*\d+/,_m=/^%/,xm=/[\\^$*+?|[\]().{}]/g;function wm(t,e,n){var r=t<0?"-":"",i=(r?-t:t)+"",a=i.length;return r+(a68?1900:2e3),n+r[0].length):-1}function Bm(t,e,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function Lm(t,e,n){var r=bm.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function Im(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function Rm(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function Fm(t,e,n){var r=bm.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function Pm(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function jm(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function Ym(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function zm(t,e,n){var r=bm.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function Um(t,e,n){var r=bm.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function qm(t,e,n){var r=_m.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function Hm(t,e,n){var r=bm.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function $m(t,e,n){var r=bm.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function Wm(t,e){return wm(t.getDate(),e,2)}function Vm(t,e){return wm(t.getHours(),e,2)}function Gm(t,e){return wm(t.getHours()%12||12,e,2)}function Xm(t,e){return wm(1+Ag.count(ng(t),t),e,3)}function Zm(t,e){return wm(t.getMilliseconds(),e,3)}function Qm(t,e){return Zm(t,e)+"000"}function Km(t,e){return wm(t.getMonth()+1,e,2)}function Jm(t,e){return wm(t.getMinutes(),e,2)}function tv(t,e){return wm(t.getSeconds(),e,2)}function ev(t){var e=t.getDay();return 0===e?7:e}function nv(t,e){return wm(dg.count(ng(t)-1,t),e,2)}function rv(t){var e=t.getDay();return e>=4||0===e?mg(t):mg.ceil(t)}function iv(t,e){return t=rv(t),wm(mg.count(ng(t),t)+(4===ng(t).getDay()),e,2)}function av(t){return t.getDay()}function ov(t,e){return wm(pg.count(ng(t)-1,t),e,2)}function sv(t,e){return wm(t.getFullYear()%100,e,2)}function cv(t,e){return wm((t=rv(t)).getFullYear()%100,e,2)}function uv(t,e){return wm(t.getFullYear()%1e4,e,4)}function lv(t,e){var n=t.getDay();return wm((t=n>=4||0===n?mg(t):mg.ceil(t)).getFullYear()%1e4,e,4)}function hv(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+wm(e/60|0,"0",2)+wm(e%60,"0",2)}function fv(t,e){return wm(t.getUTCDate(),e,2)}function dv(t,e){return wm(t.getUTCHours(),e,2)}function pv(t,e){return wm(t.getUTCHours()%12||12,e,2)}function yv(t,e){return wm(1+im.count(sm(t),t),e,3)}function gv(t,e){return wm(t.getUTCMilliseconds(),e,3)}function mv(t,e){return gv(t,e)+"000"}function vv(t,e){return wm(t.getUTCMonth()+1,e,2)}function bv(t,e){return wm(t.getUTCMinutes(),e,2)}function _v(t,e){return wm(t.getUTCSeconds(),e,2)}function xv(t){var e=t.getUTCDay();return 0===e?7:e}function wv(t,e){return wm(qg.count(sm(t)-1,t),e,2)}function kv(t){var e=t.getUTCDay();return e>=4||0===e?Vg(t):Vg.ceil(t)}function Tv(t,e){return t=kv(t),wm(Vg.count(sm(t),t)+(4===sm(t).getUTCDay()),e,2)}function Ev(t){return t.getUTCDay()}function Cv(t,e){return wm(Hg.count(sm(t)-1,t),e,2)}function Sv(t,e){return wm(t.getUTCFullYear()%100,e,2)}function Av(t,e){return wm((t=kv(t)).getUTCFullYear()%100,e,2)}function Mv(t,e){return wm(t.getUTCFullYear()%1e4,e,4)}function Nv(t,e){var n=t.getUTCDay();return wm((t=n>=4||0===n?Vg(t):Vg.ceil(t)).getUTCFullYear()%1e4,e,4)}function Dv(){return"+0000"}function Ov(){return"%"}function Bv(t){return+t}function Lv(t){return Math.floor(+t/1e3)}function Iv(t){return dm=fm(t),pm=dm.format,ym=dm.parse,gm=dm.utcFormat,mm=dm.utcParse,dm}Iv({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Rv=31536e6;function Fv(t){return new Date(t)}function Pv(t){return t instanceof Date?+t:+new Date(+t)}function jv(t,e,n,r,i,o,s,c,u){var l=Ey(vy,vy),h=l.invert,f=l.domain,d=u(".%L"),p=u(":%S"),y=u("%I:%M"),g=u("%I %p"),m=u("%a %d"),v=u("%b %d"),b=u("%B"),_=u("%Y"),x=[[s,1,1e3],[s,5,5e3],[s,15,15e3],[s,30,3e4],[o,1,6e4],[o,5,3e5],[o,15,9e5],[o,30,18e5],[i,1,36e5],[i,3,108e5],[i,6,216e5],[i,12,432e5],[r,1,864e5],[r,2,1728e5],[n,1,6048e5],[e,1,2592e6],[e,3,7776e6],[t,1,Rv]];function w(a){return(s(a)1)&&(t-=Math.floor(t));var e=Math.abs(t-.5);return S_.h=360*t-100,S_.s=1.5-1.5*e,S_.l=.8-.9*e,S_+""}var M_=Qe(),N_=Math.PI/3,D_=2*Math.PI/3;function O_(t){var e;return t=(.5-t)*Math.PI,M_.r=255*(e=Math.sin(t))*e,M_.g=255*(e=Math.sin(t+N_))*e,M_.b=255*(e=Math.sin(t+D_))*e,M_+""}function B_(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"}function L_(t){var e=t.length;return function(n){return t[Math.max(0,Math.min(e-1,Math.floor(n*e)))]}}const I_=L_(hb("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));var R_=L_(hb("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),F_=L_(hb("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),P_=L_(hb("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));function j_(t){return Te(ie(t).call(document.documentElement))}var Y_=0;function z_(){return new U_}function U_(){this._="@"+(++Y_).toString(36)}function q_(t){return"string"==typeof t?new xe([document.querySelectorAll(t)],[document.documentElement]):new xe([null==t?[]:t],_e)}function H_(t,e){null==e&&(e=Nn().touches);for(var n=0,r=e?e.length:0,i=new Array(r);n1?0:t<-1?tx:Math.acos(t)}function ix(t){return t>=1?ex:t<=-1?-ex:Math.asin(t)}function ax(t){return t.innerRadius}function ox(t){return t.outerRadius}function sx(t){return t.startAngle}function cx(t){return t.endAngle}function ux(t){return t&&t.padAngle}function lx(t,e,n,r,i,a,o,s){var c=n-t,u=r-e,l=o-i,h=s-a,f=h*c-l*u;if(!(f*fN*N+D*D&&(T=C,E=S),{cx:T,cy:E,x01:-l,y01:-h,x11:T*(i/x-1),y11:E*(i/x-1)}}function fx(){var t=ax,e=ox,n=$_(0),r=null,i=sx,a=cx,o=ux,s=null;function c(){var c,u,l=+t.apply(this,arguments),h=+e.apply(this,arguments),f=i.apply(this,arguments)-ex,d=a.apply(this,arguments)-ex,p=W_(d-f),y=d>f;if(s||(s=c=Wi()),hJ_)if(p>nx-J_)s.moveTo(h*G_(f),h*Q_(f)),s.arc(0,0,h,f,d,!y),l>J_&&(s.moveTo(l*G_(d),l*Q_(d)),s.arc(0,0,l,d,f,y));else{var g,m,v=f,b=d,_=f,x=d,w=p,k=p,T=o.apply(this,arguments)/2,E=T>J_&&(r?+r.apply(this,arguments):K_(l*l+h*h)),C=Z_(W_(h-l)/2,+n.apply(this,arguments)),S=C,A=C;if(E>J_){var M=ix(E/l*Q_(T)),N=ix(E/h*Q_(T));(w-=2*M)>J_?(_+=M*=y?1:-1,x-=M):(w=0,_=x=(f+d)/2),(k-=2*N)>J_?(v+=N*=y?1:-1,b-=N):(k=0,v=b=(f+d)/2)}var D=h*G_(v),O=h*Q_(v),B=l*G_(x),L=l*Q_(x);if(C>J_){var I,R=h*G_(b),F=h*Q_(b),P=l*G_(_),j=l*Q_(_);if(pJ_?A>J_?(g=hx(P,j,D,O,h,A,y),m=hx(R,F,B,L,h,A,y),s.moveTo(g.cx+g.x01,g.cy+g.y01),AJ_&&w>J_?S>J_?(g=hx(B,L,R,F,l,-S,y),m=hx(D,O,P,j,l,-S,y),s.lineTo(g.cx+g.x01,g.cy+g.y01),S=l;--h)s.point(g[h],m[h]);s.lineEnd(),s.areaEnd()}y&&(g[u]=+t(f,u,c),m[u]=+n(f,u,c),s.point(e?+e(f,u,c):g[u],r?+r(f,u,c):m[u]))}if(d)return s=null,d+""||null}function u(){return mx().defined(i).curve(o).context(a)}return c.x=function(n){return arguments.length?(t="function"==typeof n?n:$_(+n),e=null,c):t},c.x0=function(e){return arguments.length?(t="function"==typeof e?e:$_(+e),c):t},c.x1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:$_(+t),c):e},c.y=function(t){return arguments.length?(n="function"==typeof t?t:$_(+t),r=null,c):n},c.y0=function(t){return arguments.length?(n="function"==typeof t?t:$_(+t),c):n},c.y1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:$_(+t),c):r},c.lineX0=c.lineY0=function(){return u().x(t).y(n)},c.lineY1=function(){return u().x(t).y(r)},c.lineX1=function(){return u().x(e).y(n)},c.defined=function(t){return arguments.length?(i="function"==typeof t?t:$_(!!t),c):i},c.curve=function(t){return arguments.length?(o=t,null!=a&&(s=o(a)),c):o},c.context=function(t){return arguments.length?(null==t?a=s=null:s=o(a=t),c):a},c}function bx(t,e){return et?1:e>=t?0:NaN}function _x(t){return t}function xx(){var t=_x,e=bx,n=null,r=$_(0),i=$_(nx),a=$_(0);function o(o){var s,c,u,l,h,f=o.length,d=0,p=new Array(f),y=new Array(f),g=+r.apply(this,arguments),m=Math.min(nx,Math.max(-nx,i.apply(this,arguments)-g)),v=Math.min(Math.abs(m)/f,a.apply(this,arguments)),b=v*(m<0?-1:1);for(s=0;s0&&(d+=h);for(null!=e?p.sort((function(t,n){return e(y[t],y[n])})):null!=n&&p.sort((function(t,e){return n(o[t],o[e])})),s=0,u=d?(m-f*b)/d:0;s0?h*u:0)+b,y[c]={data:o[c],index:s,value:h,startAngle:g,endAngle:l,padAngle:v};return y}return o.value=function(e){return arguments.length?(t="function"==typeof e?e:$_(+e),o):t},o.sortValues=function(t){return arguments.length?(e=t,n=null,o):e},o.sort=function(t){return arguments.length?(n=t,e=null,o):n},o.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:$_(+t),o):r},o.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:$_(+t),o):i},o.padAngle=function(t){return arguments.length?(a="function"==typeof t?t:$_(+t),o):a},o}dx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:this._context.lineTo(t,e)}}};var wx=Tx(px);function kx(t){this._curve=t}function Tx(t){function e(e){return new kx(t(e))}return e._curve=t,e}function Ex(t){var e=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?e(Tx(t)):e()._curve},t}function Cx(){return Ex(mx().curve(wx))}function Sx(){var t=vx().curve(wx),e=t.curve,n=t.lineX0,r=t.lineX1,i=t.lineY0,a=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Ex(n())},delete t.lineX0,t.lineEndAngle=function(){return Ex(r())},delete t.lineX1,t.lineInnerRadius=function(){return Ex(i())},delete t.lineY0,t.lineOuterRadius=function(){return Ex(a())},delete t.lineY1,t.curve=function(t){return arguments.length?e(Tx(t)):e()._curve},t}function Ax(t,e){return[(e=+e)*Math.cos(t-=Math.PI/2),e*Math.sin(t)]}kx.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,e){this._curve.point(e*Math.sin(t),e*-Math.cos(t))}};var Mx=Array.prototype.slice;function Nx(t){return t.source}function Dx(t){return t.target}function Ox(t){var e=Nx,n=Dx,r=yx,i=gx,a=null;function o(){var o,s=Mx.call(arguments),c=e.apply(this,s),u=n.apply(this,s);if(a||(a=o=Wi()),t(a,+r.apply(this,(s[0]=c,s)),+i.apply(this,s),+r.apply(this,(s[0]=u,s)),+i.apply(this,s)),o)return a=null,o+""||null}return o.source=function(t){return arguments.length?(e=t,o):e},o.target=function(t){return arguments.length?(n=t,o):n},o.x=function(t){return arguments.length?(r="function"==typeof t?t:$_(+t),o):r},o.y=function(t){return arguments.length?(i="function"==typeof t?t:$_(+t),o):i},o.context=function(t){return arguments.length?(a=null==t?null:t,o):a},o}function Bx(t,e,n,r,i){t.moveTo(e,n),t.bezierCurveTo(e=(e+r)/2,n,e,i,r,i)}function Lx(t,e,n,r,i){t.moveTo(e,n),t.bezierCurveTo(e,n=(n+i)/2,r,n,r,i)}function Ix(t,e,n,r,i){var a=Ax(e,n),o=Ax(e,n=(n+i)/2),s=Ax(r,n),c=Ax(r,i);t.moveTo(a[0],a[1]),t.bezierCurveTo(o[0],o[1],s[0],s[1],c[0],c[1])}function Rx(){return Ox(Bx)}function Fx(){return Ox(Lx)}function Px(){var t=Ox(Ix);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t}const jx={draw:function(t,e){var n=Math.sqrt(e/tx);t.moveTo(n,0),t.arc(0,0,n,0,nx)}},Yx={draw:function(t,e){var n=Math.sqrt(e/5)/2;t.moveTo(-3*n,-n),t.lineTo(-n,-n),t.lineTo(-n,-3*n),t.lineTo(n,-3*n),t.lineTo(n,-n),t.lineTo(3*n,-n),t.lineTo(3*n,n),t.lineTo(n,n),t.lineTo(n,3*n),t.lineTo(-n,3*n),t.lineTo(-n,n),t.lineTo(-3*n,n),t.closePath()}};var zx=Math.sqrt(1/3),Ux=2*zx;const qx={draw:function(t,e){var n=Math.sqrt(e/Ux),r=n*zx;t.moveTo(0,-n),t.lineTo(r,0),t.lineTo(0,n),t.lineTo(-r,0),t.closePath()}};var Hx=Math.sin(tx/10)/Math.sin(7*tx/10),$x=Math.sin(nx/10)*Hx,Wx=-Math.cos(nx/10)*Hx;const Vx={draw:function(t,e){var n=Math.sqrt(.8908130915292852*e),r=$x*n,i=Wx*n;t.moveTo(0,-n),t.lineTo(r,i);for(var a=1;a<5;++a){var o=nx*a/5,s=Math.cos(o),c=Math.sin(o);t.lineTo(c*n,-s*n),t.lineTo(s*r-c*i,c*r+s*i)}t.closePath()}},Gx={draw:function(t,e){var n=Math.sqrt(e),r=-n/2;t.rect(r,r,n,n)}};var Xx=Math.sqrt(3);const Zx={draw:function(t,e){var n=-Math.sqrt(e/(3*Xx));t.moveTo(0,2*n),t.lineTo(-Xx*n,-n),t.lineTo(Xx*n,-n),t.closePath()}};var Qx=-.5,Kx=Math.sqrt(3)/2,Jx=1/Math.sqrt(12),tw=3*(Jx/2+1);const ew={draw:function(t,e){var n=Math.sqrt(e/tw),r=n/2,i=n*Jx,a=r,o=n*Jx+n,s=-a,c=o;t.moveTo(r,i),t.lineTo(a,o),t.lineTo(s,c),t.lineTo(Qx*r-Kx*i,Kx*r+Qx*i),t.lineTo(Qx*a-Kx*o,Kx*a+Qx*o),t.lineTo(Qx*s-Kx*c,Kx*s+Qx*c),t.lineTo(Qx*r+Kx*i,Qx*i-Kx*r),t.lineTo(Qx*a+Kx*o,Qx*o-Kx*a),t.lineTo(Qx*s+Kx*c,Qx*c-Kx*s),t.closePath()}};var nw=[jx,Yx,qx,Gx,Vx,Zx,ew];function rw(){var t=$_(jx),e=$_(64),n=null;function r(){var r;if(n||(n=r=Wi()),t.apply(this,arguments).draw(n,+e.apply(this,arguments)),r)return n=null,r+""||null}return r.type=function(e){return arguments.length?(t="function"==typeof e?e:$_(e),r):t},r.size=function(t){return arguments.length?(e="function"==typeof t?t:$_(+t),r):e},r.context=function(t){return arguments.length?(n=null==t?null:t,r):n},r}function iw(){}function aw(t,e,n){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+e)/6,(t._y0+4*t._y1+n)/6)}function ow(t){this._context=t}function sw(t){return new ow(t)}function cw(t){this._context=t}function uw(t){return new cw(t)}function lw(t){this._context=t}function hw(t){return new lw(t)}function fw(t,e){this._basis=new ow(t),this._beta=e}ow.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:aw(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:aw(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},cw.prototype={areaStart:iw,areaEnd:iw,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._x2=t,this._y2=e;break;case 1:this._point=2,this._x3=t,this._y3=e;break;case 2:this._point=3,this._x4=t,this._y4=e,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+e)/6);break;default:aw(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},lw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var n=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+e)/6;this._line?this._context.lineTo(n,r):this._context.moveTo(n,r);break;case 3:this._point=4;default:aw(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},fw.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,e=this._y,n=t.length-1;if(n>0)for(var r,i=t[0],a=e[0],o=t[n]-i,s=e[n]-a,c=-1;++c<=n;)r=c/n,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*o),this._beta*e[c]+(1-this._beta)*(a+r*s));this._x=this._y=null,this._basis.lineEnd()},point:function(t,e){this._x.push(+t),this._y.push(+e)}};const dw=function t(e){function n(t){return 1===e?new ow(t):new fw(t,e)}return n.beta=function(e){return t(+e)},n}(.85);function pw(t,e,n){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-e),t._y2+t._k*(t._y1-n),t._x2,t._y2)}function yw(t,e){this._context=t,this._k=(1-e)/6}yw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:pw(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2,this._x1=t,this._y1=e;break;case 2:this._point=3;default:pw(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const gw=function t(e){function n(t){return new yw(t,e)}return n.tension=function(e){return t(+e)},n}(0);function mw(t,e){this._context=t,this._k=(1-e)/6}mw.prototype={areaStart:iw,areaEnd:iw,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._x3=t,this._y3=e;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=e);break;case 2:this._point=3,this._x5=t,this._y5=e;break;default:pw(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const vw=function t(e){function n(t){return new mw(t,e)}return n.tension=function(e){return t(+e)},n}(0);function bw(t,e){this._context=t,this._k=(1-e)/6}bw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:pw(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const _w=function t(e){function n(t){return new bw(t,e)}return n.tension=function(e){return t(+e)},n}(0);function xw(t,e,n){var r=t._x1,i=t._y1,a=t._x2,o=t._y2;if(t._l01_a>J_){var s=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*s-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*s-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>J_){var u=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,l=3*t._l23_a*(t._l23_a+t._l12_a);a=(a*u+t._x1*t._l23_2a-e*t._l12_2a)/l,o=(o*u+t._y1*t._l23_2a-n*t._l12_2a)/l}t._context.bezierCurveTo(r,i,a,o,t._x2,t._y2)}function ww(t,e){this._context=t,this._alpha=e}ww.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3;default:xw(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const kw=function t(e){function n(t){return e?new ww(t,e):new yw(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function Tw(t,e){this._context=t,this._alpha=e}Tw.prototype={areaStart:iw,areaEnd:iw,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=e;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=e);break;case 2:this._point=3,this._x5=t,this._y5=e;break;default:xw(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const Ew=function t(e){function n(t){return e?new Tw(t,e):new mw(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function Cw(t,e){this._context=t,this._alpha=e}Cw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:xw(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const Sw=function t(e){function n(t){return e?new Cw(t,e):new bw(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function Aw(t){this._context=t}function Mw(t){return new Aw(t)}function Nw(t){return t<0?-1:1}function Dw(t,e,n){var r=t._x1-t._x0,i=e-t._x1,a=(t._y1-t._y0)/(r||i<0&&-0),o=(n-t._y1)/(i||r<0&&-0),s=(a*i+o*r)/(r+i);return(Nw(a)+Nw(o))*Math.min(Math.abs(a),Math.abs(o),.5*Math.abs(s))||0}function Ow(t,e){var n=t._x1-t._x0;return n?(3*(t._y1-t._y0)/n-e)/2:e}function Bw(t,e,n){var r=t._x0,i=t._y0,a=t._x1,o=t._y1,s=(a-r)/3;t._context.bezierCurveTo(r+s,i+s*e,a-s,o-s*n,a,o)}function Lw(t){this._context=t}function Iw(t){this._context=new Rw(t)}function Rw(t){this._context=t}function Fw(t){return new Lw(t)}function Pw(t){return new Iw(t)}function jw(t){this._context=t}function Yw(t){var e,n,r=t.length-1,i=new Array(r),a=new Array(r),o=new Array(r);for(i[0]=0,a[0]=2,o[0]=t[0]+2*t[1],e=1;e=0;--e)i[e]=(o[e]-i[e+1])/a[e];for(a[r-1]=(t[r]+i[r-1])/2,e=0;e1)for(var n,r,i,a=1,o=t[e[0]],s=o.length;a=0;)n[e]=e;return n}function Gw(t,e){return t[e]}function Xw(){var t=$_([]),e=Vw,n=Ww,r=Gw;function i(i){var a,o,s=t.apply(this,arguments),c=i.length,u=s.length,l=new Array(u);for(a=0;a0){for(var n,r,i,a=0,o=t[0].length;a0)for(var n,r,i,a,o,s,c=0,u=t[e[0]].length;c0?(r[0]=a,r[1]=a+=i):i<0?(r[1]=o,r[0]=o+=i):(r[0]=0,r[1]=i)}function Kw(t,e){if((n=t.length)>0){for(var n,r=0,i=t[e[0]],a=i.length;r0&&(r=(n=t[e[0]]).length)>0){for(var n,r,i,a=0,o=1;oa&&(a=e,r=n);return r}function nk(t){var e=t.map(rk);return Vw(t).sort((function(t,n){return e[t]-e[n]}))}function rk(t){for(var e,n=0,r=-1,i=t.length;++r=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,e),this._context.lineTo(t,e);else{var n=this._x*(1-this._t)+t*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,e)}}this._x=t,this._y=e}};var sk="%Y-%m-%dT%H:%M:%S.%LZ",ck=Date.prototype.toISOString?function(t){return t.toISOString()}:gm(sk);const uk=ck;var lk=+new Date("2000-01-01T00:00:00.000Z")?function(t){var e=new Date(t);return isNaN(e)?null:e}:mm(sk);const hk=lk;function fk(t,e,n){var r=new Wn,i=e;return null==e?(r.restart(t,e,n),r):(e=+e,n=null==n?Hn():+n,r.restart((function a(o){o+=i,r.restart(a,i+=e,n),t(o)}),e,n),r)}function dk(t){return function(){return t}}function pk(t){return t[0]}function yk(t){return t[1]}function gk(){this._=null}function mk(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function vk(t,e){var n=e,r=e.R,i=n.U;i?i.L===n?i.L=r:i.R=r:t._=r,r.U=i,n.U=r,n.R=r.L,n.R&&(n.R.U=n),r.L=n}function bk(t,e){var n=e,r=e.L,i=n.U;i?i.L===n?i.L=r:i.R=r:t._=r,r.U=i,n.U=r,n.L=r.R,n.L&&(n.L.U=n),r.R=n}function _k(t){for(;t.L;)t=t.L;return t}gk.prototype={constructor:gk,insert:function(t,e){var n,r,i;if(t){if(e.P=t,e.N=t.N,t.N&&(t.N.P=e),t.N=e,t.R){for(t=t.R;t.L;)t=t.L;t.L=e}else t.R=e;n=t}else this._?(t=_k(this._),e.P=null,e.N=t,t.P=t.L=e,n=t):(e.P=e.N=null,this._=e,n=null);for(e.L=e.R=null,e.U=n,e.C=!0,t=e;n&&n.C;)n===(r=n.U).L?(i=r.R)&&i.C?(n.C=i.C=!1,r.C=!0,t=r):(t===n.R&&(vk(this,n),n=(t=n).U),n.C=!1,r.C=!0,bk(this,r)):(i=r.L)&&i.C?(n.C=i.C=!1,r.C=!0,t=r):(t===n.L&&(bk(this,n),n=(t=n).U),n.C=!1,r.C=!0,vk(this,r)),n=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var e,n,r,i=t.U,a=t.L,o=t.R;if(n=a?o?_k(o):a:o,i?i.L===t?i.L=n:i.R=n:this._=n,a&&o?(r=n.C,n.C=t.C,n.L=a,a.U=n,n!==o?(i=n.U,n.U=t.U,t=n.R,i.L=t,n.R=o,o.U=n):(n.U=i,i=n,t=n.R)):(r=t.C,t=n),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((e=i.R).C&&(e.C=!1,i.C=!0,vk(this,i),e=i.R),e.L&&e.L.C||e.R&&e.R.C){e.R&&e.R.C||(e.L.C=!1,e.C=!0,bk(this,e),e=i.R),e.C=i.C,i.C=e.R.C=!1,vk(this,i),t=this._;break}}else if((e=i.L).C&&(e.C=!1,i.C=!0,bk(this,i),e=i.L),e.L&&e.L.C||e.R&&e.R.C){e.L&&e.L.C||(e.R.C=!1,e.C=!0,vk(this,e),e=i.L),e.C=i.C,i.C=e.L.C=!1,bk(this,i),t=this._;break}e.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};const xk=gk;function wk(t,e,n,r){var i=[null,null],a=Wk.push(i)-1;return i.left=t,i.right=e,n&&Tk(i,t,e,n),r&&Tk(i,e,t,r),Hk[t.index].halfedges.push(a),Hk[e.index].halfedges.push(a),i}function kk(t,e,n){var r=[e,n];return r.left=t,r}function Tk(t,e,n,r){t[0]||t[1]?t.left===n?t[1]=r:t[0]=r:(t[0]=r,t.left=e,t.right=n)}function Ek(t,e,n,r,i){var a,o=t[0],s=t[1],c=o[0],u=o[1],l=0,h=1,f=s[0]-c,d=s[1]-u;if(a=e-c,f||!(a>0)){if(a/=f,f<0){if(a0){if(a>h)return;a>l&&(l=a)}if(a=r-c,f||!(a<0)){if(a/=f,f<0){if(a>h)return;a>l&&(l=a)}else if(f>0){if(a0)){if(a/=d,d<0){if(a0){if(a>h)return;a>l&&(l=a)}if(a=i-u,d||!(a<0)){if(a/=d,d<0){if(a>h)return;a>l&&(l=a)}else if(d>0){if(a0||h<1)||(l>0&&(t[0]=[c+l*f,u+l*d]),h<1&&(t[1]=[c+h*f,u+h*d]),!0)}}}}}function Ck(t,e,n,r,i){var a=t[1];if(a)return!0;var o,s,c=t[0],u=t.left,l=t.right,h=u[0],f=u[1],d=l[0],p=l[1],y=(h+d)/2,g=(f+p)/2;if(p===f){if(y=r)return;if(h>d){if(c){if(c[1]>=i)return}else c=[y,n];a=[y,i]}else{if(c){if(c[1]1)if(h>d){if(c){if(c[1]>=i)return}else c=[(n-s)/o,n];a=[(i-s)/o,i]}else{if(c){if(c[1]=r)return}else c=[e,o*e+s];a=[r,o*r+s]}else{if(c){if(c[0]=-Gk)){var d=c*c+u*u,p=l*l+h*h,y=(h*d-u*p)/f,g=(c*p-l*d)/f,m=Dk.pop()||new Ok;m.arc=t,m.site=i,m.x=y+o,m.y=(m.cy=g+s)+Math.sqrt(y*y+g*g),t.circle=m;for(var v=null,b=$k._;b;)if(m.yVk)s=s.L;else{if(!((i=a-Uk(s,o))>Vk)){r>-Vk?(e=s.P,n=s):i>-Vk?(e=s,n=s.N):e=n=s;break}if(!s.R){e=s;break}s=s.R}!function(t){Hk[t.index]={site:t,halfedges:[]}}(t);var c=Fk(t);if(qk.insert(e,c),e||n){if(e===n)return Lk(e),n=Fk(e.site),qk.insert(c,n),c.edge=n.edge=wk(e.site,c.site),Bk(e),void Bk(n);if(n){Lk(e),Lk(n);var u=e.site,l=u[0],h=u[1],f=t[0]-l,d=t[1]-h,p=n.site,y=p[0]-l,g=p[1]-h,m=2*(f*g-d*y),v=f*f+d*d,b=y*y+g*g,_=[(g*v-d*b)/m+l,(f*b-y*v)/m+h];Tk(n.edge,u,p,_),c.edge=wk(u,t,null,_),n.edge=wk(t,p,null,_),Bk(e),Bk(n)}else c.edge=wk(e.site,c.site)}}function zk(t,e){var n=t.site,r=n[0],i=n[1],a=i-e;if(!a)return r;var o=t.P;if(!o)return-1/0;var s=(n=o.site)[0],c=n[1],u=c-e;if(!u)return s;var l=s-r,h=1/a-1/u,f=l/u;return h?(-f+Math.sqrt(f*f-2*h*(l*l/(-2*u)-c+u/2+i-a/2)))/h+r:(r+s)/2}function Uk(t,e){var n=t.N;if(n)return zk(n,e);var r=t.site;return r[1]===e?r[0]:1/0}var qk,Hk,$k,Wk,Vk=1e-6,Gk=1e-12;function Xk(t,e,n){return(t[0]-n[0])*(e[1]-t[1])-(t[0]-e[0])*(n[1]-t[1])}function Zk(t,e){return e[1]-t[1]||e[0]-t[0]}function Qk(t,e){var n,r,i,a=t.sort(Zk).pop();for(Wk=[],Hk=new Array(t.length),qk=new xk,$k=new xk;;)if(i=Nk,a&&(!i||a[1]Vk||Math.abs(i[0][1]-i[1][1])>Vk)||delete Wk[a]}(o,s,c,u),function(t,e,n,r){var i,a,o,s,c,u,l,h,f,d,p,y,g=Hk.length,m=!0;for(i=0;iVk||Math.abs(y-f)>Vk)&&(c.splice(s,0,Wk.push(kk(o,d,Math.abs(p-t)Vk?[t,Math.abs(h-t)Vk?[Math.abs(f-r)Vk?[n,Math.abs(h-n)Vk?[Math.abs(f-e)=s)return null;var c=t-i.site[0],u=e-i.site[1],l=c*c+u*u;do{i=a.cells[r=o],o=null,i.halfedges.forEach((function(n){var r=a.edges[n],s=r.left;if(s!==i.site&&s||(s=r.right)){var c=t-s[0],u=e-s[1],h=c*c+u*u;hr?(r+i)/2:Math.min(0,r)||Math.max(0,i),o>a?(a+o)/2:Math.min(0,a)||Math.max(0,o))}function fT(){var t,e,n=oT,r=sT,i=hT,a=uT,o=lT,s=[0,1/0],c=[[-1/0,-1/0],[1/0,1/0]],u=250,l=Op,h=ft("start","zoom","end"),f=500,d=0;function p(t){t.property("__zoom",cT).on("wheel.zoom",x).on("mousedown.zoom",w).on("dblclick.zoom",k).filter(o).on("touchstart.zoom",T).on("touchmove.zoom",E).on("touchend.zoom touchcancel.zoom",C).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function y(t,e){return(e=Math.max(s[0],Math.min(s[1],e)))===t.k?t:new eT(e,t.x,t.y)}function g(t,e,n){var r=e[0]-n[0]*t.k,i=e[1]-n[1]*t.k;return r===t.x&&i===t.y?t:new eT(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function v(t,e,n){t.on("start.zoom",(function(){b(this,arguments).start()})).on("interrupt.zoom end.zoom",(function(){b(this,arguments).end()})).tween("zoom",(function(){var t=this,i=arguments,a=b(t,i),o=r.apply(t,i),s=null==n?m(o):"function"==typeof n?n.apply(t,i):n,c=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),u=t.__zoom,h="function"==typeof e?e.apply(t,i):e,f=l(u.invert(s).concat(c/u.k),h.invert(s).concat(c/h.k));return function(t){if(1===t)t=h;else{var e=f(t),n=c/e[2];t=new eT(n,s[0]-e[0]*n,s[1]-e[1]*n)}a.zoom(null,t)}}))}function b(t,e,n){return!n&&t.__zooming||new _(t,e)}function _(t,e){this.that=t,this.args=e,this.active=0,this.extent=r.apply(t,e),this.taps=0}function x(){if(n.apply(this,arguments)){var t=b(this,arguments),e=this.__zoom,r=Math.max(s[0],Math.min(s[1],e.k*Math.pow(2,a.apply(this,arguments)))),o=Bn(this);if(t.wheel)t.mouse[0][0]===o[0]&&t.mouse[0][1]===o[1]||(t.mouse[1]=e.invert(t.mouse[0]=o)),clearTimeout(t.wheel);else{if(e.k===r)return;t.mouse=[o,e.invert(o)],ar(this),t.start()}aT(),t.wheel=setTimeout(u,150),t.zoom("mouse",i(g(y(e,r),t.mouse[0],t.mouse[1]),t.extent,c))}function u(){t.wheel=null,t.end()}}function w(){if(!e&&n.apply(this,arguments)){var t=b(this,arguments,!0),r=Te(le.view).on("mousemove.zoom",u,!0).on("mouseup.zoom",l,!0),a=Bn(this),o=le.clientX,s=le.clientY;Se(le.view),iT(),t.mouse=[a,this.__zoom.invert(a)],ar(this),t.start()}function u(){if(aT(),!t.moved){var e=le.clientX-o,n=le.clientY-s;t.moved=e*e+n*n>d}t.zoom("mouse",i(g(t.that.__zoom,t.mouse[0]=Bn(t.that),t.mouse[1]),t.extent,c))}function l(){r.on("mousemove.zoom mouseup.zoom",null),Ae(le.view,t.moved),aT(),t.end()}}function k(){if(n.apply(this,arguments)){var t=this.__zoom,e=Bn(this),a=t.invert(e),o=t.k*(le.shiftKey?.5:2),s=i(g(y(t,o),e,a),r.apply(this,arguments),c);aT(),u>0?Te(this).transition().duration(u).call(v,s,e):Te(this).call(p.transform,s)}}function T(){if(n.apply(this,arguments)){var e,r,i,a,o=le.touches,s=o.length,c=b(this,arguments,le.changedTouches.length===s);for(iT(),r=0;r{t.exports={graphlib:n(574),layout:n(8123),debug:n(7570),util:{time:n(1138).time,notime:n(1138).notime},version:n(8177)}},1207:(t,e,n)=>{"use strict";var r=n(8436),i=n(4079);t.exports={run:function(t){var e="greedy"===t.graph().acyclicer?i(t,function(t){return function(e){return t.edge(e).weight}}(t)):function(t){var e=[],n={},i={};return r.forEach(t.nodes(),(function a(o){r.has(i,o)||(i[o]=!0,n[o]=!0,r.forEach(t.outEdges(o),(function(t){r.has(n,t.w)?e.push(t):a(t.w)})),delete n[o])})),e}(t);r.forEach(e,(function(e){var n=t.edge(e);t.removeEdge(e),n.forwardName=e.name,n.reversed=!0,t.setEdge(e.w,e.v,n,r.uniqueId("rev"))}))},undo:function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);if(n.reversed){t.removeEdge(e);var r=n.forwardName;delete n.reversed,delete n.forwardName,t.setEdge(e.w,e.v,n,r)}}))}}},1133:(t,e,n)=>{var r=n(8436),i=n(1138);function a(t,e,n,r,a,o){var s={width:0,height:0,rank:o,borderType:e},c=a[e][o-1],u=i.addDummyNode(t,"border",s,n);a[e][o]=u,t.setParent(u,r),c&&t.setEdge(c,u,{weight:1})}t.exports=function(t){r.forEach(t.children(),(function e(n){var i=t.children(n),o=t.node(n);if(i.length&&r.forEach(i,e),r.has(o,"minRank")){o.borderLeft=[],o.borderRight=[];for(var s=o.minRank,c=o.maxRank+1;s{"use strict";var r=n(8436);function i(t){r.forEach(t.nodes(),(function(e){a(t.node(e))})),r.forEach(t.edges(),(function(e){a(t.edge(e))}))}function a(t){var e=t.width;t.width=t.height,t.height=e}function o(t){t.y=-t.y}function s(t){var e=t.x;t.x=t.y,t.y=e}t.exports={adjust:function(t){var e=t.graph().rankdir.toLowerCase();"lr"!==e&&"rl"!==e||i(t)},undo:function(t){var e=t.graph().rankdir.toLowerCase();"bt"!==e&&"rl"!==e||function(t){r.forEach(t.nodes(),(function(e){o(t.node(e))})),r.forEach(t.edges(),(function(e){var n=t.edge(e);r.forEach(n.points,o),r.has(n,"y")&&o(n)}))}(t),"lr"!==e&&"rl"!==e||(function(t){r.forEach(t.nodes(),(function(e){s(t.node(e))})),r.forEach(t.edges(),(function(e){var n=t.edge(e);r.forEach(n.points,s),r.has(n,"x")&&s(n)}))}(t),i(t))}}},7822:t=>{function e(){var t={};t._next=t._prev=t,this._sentinel=t}function n(t){t._prev._next=t._next,t._next._prev=t._prev,delete t._next,delete t._prev}function r(t,e){if("_next"!==t&&"_prev"!==t)return e}t.exports=e,e.prototype.dequeue=function(){var t=this._sentinel,e=t._prev;if(e!==t)return n(e),e},e.prototype.enqueue=function(t){var e=this._sentinel;t._prev&&t._next&&n(t),t._next=e._next,e._next._prev=t,e._next=t,t._prev=e},e.prototype.toString=function(){for(var t=[],e=this._sentinel,n=e._prev;n!==e;)t.push(JSON.stringify(n,r)),n=n._prev;return"["+t.join(", ")+"]"}},7570:(t,e,n)=>{var r=n(8436),i=n(1138),a=n(574).Graph;t.exports={debugOrdering:function(t){var e=i.buildLayerMatrix(t),n=new a({compound:!0,multigraph:!0}).setGraph({});return r.forEach(t.nodes(),(function(e){n.setNode(e,{label:e}),n.setParent(e,"layer"+t.node(e).rank)})),r.forEach(t.edges(),(function(t){n.setEdge(t.v,t.w,{},t.name)})),r.forEach(e,(function(t,e){var i="layer"+e;n.setNode(i,{rank:"same"}),r.reduce(t,(function(t,e){return n.setEdge(t,e,{style:"invis"}),e}))})),n}}},574:(t,e,n)=>{var r;try{r=n(8282)}catch(t){}r||(r=window.graphlib),t.exports=r},4079:(t,e,n)=>{var r=n(8436),i=n(574).Graph,a=n(7822);t.exports=function(t,e){if(t.nodeCount()<=1)return[];var n=function(t,e){var n=new i,o=0,s=0;r.forEach(t.nodes(),(function(t){n.setNode(t,{v:t,in:0,out:0})})),r.forEach(t.edges(),(function(t){var r=n.edge(t.v,t.w)||0,i=e(t),a=r+i;n.setEdge(t.v,t.w,a),s=Math.max(s,n.node(t.v).out+=i),o=Math.max(o,n.node(t.w).in+=i)}));var u=r.range(s+o+3).map((function(){return new a})),l=o+1;return r.forEach(n.nodes(),(function(t){c(u,l,n.node(t))})),{graph:n,buckets:u,zeroIdx:l}}(t,e||o),u=function(t,e,n){for(var r,i=[],a=e[e.length-1],o=e[0];t.nodeCount();){for(;r=o.dequeue();)s(t,e,n,r);for(;r=a.dequeue();)s(t,e,n,r);if(t.nodeCount())for(var c=e.length-2;c>0;--c)if(r=e[c].dequeue()){i=i.concat(s(t,e,n,r,!0));break}}return i}(n.graph,n.buckets,n.zeroIdx);return r.flatten(r.map(u,(function(e){return t.outEdges(e.v,e.w)})),!0)};var o=r.constant(1);function s(t,e,n,i,a){var o=a?[]:void 0;return r.forEach(t.inEdges(i.v),(function(r){var i=t.edge(r),s=t.node(r.v);a&&o.push({v:r.v,w:r.w}),s.out-=i,c(e,n,s)})),r.forEach(t.outEdges(i.v),(function(r){var i=t.edge(r),a=r.w,o=t.node(a);o.in-=i,c(e,n,o)})),t.removeNode(i.v),o}function c(t,e,n){n.out?n.in?t[n.out-n.in+e].enqueue(n):t[t.length-1].enqueue(n):t[0].enqueue(n)}},8123:(t,e,n)=>{"use strict";var r=n(8436),i=n(1207),a=n(5995),o=n(8093),s=n(1138).normalizeRanks,c=n(4219),u=n(1138).removeEmptyRanks,l=n(2981),h=n(1133),f=n(3258),d=n(3408),p=n(7873),y=n(1138),g=n(574).Graph;t.exports=function(t,e){var n=e&&e.debugTiming?y.time:y.notime;n("layout",(function(){var e=n(" buildLayoutGraph",(function(){return function(t){var e=new g({multigraph:!0,compound:!0}),n=C(t.graph());return e.setGraph(r.merge({},v,E(n,m),r.pick(n,b))),r.forEach(t.nodes(),(function(n){var i=C(t.node(n));e.setNode(n,r.defaults(E(i,_),x)),e.setParent(n,t.parent(n))})),r.forEach(t.edges(),(function(n){var i=C(t.edge(n));e.setEdge(n,r.merge({},k,E(i,w),r.pick(i,T)))})),e}(t)}));n(" runLayout",(function(){!function(t,e){e(" makeSpaceForEdgeLabels",(function(){!function(t){var e=t.graph();e.ranksep/=2,r.forEach(t.edges(),(function(n){var r=t.edge(n);r.minlen*=2,"c"!==r.labelpos.toLowerCase()&&("TB"===e.rankdir||"BT"===e.rankdir?r.width+=r.labeloffset:r.height+=r.labeloffset)}))}(t)})),e(" removeSelfEdges",(function(){!function(t){r.forEach(t.edges(),(function(e){if(e.v===e.w){var n=t.node(e.v);n.selfEdges||(n.selfEdges=[]),n.selfEdges.push({e,label:t.edge(e)}),t.removeEdge(e)}}))}(t)})),e(" acyclic",(function(){i.run(t)})),e(" nestingGraph.run",(function(){l.run(t)})),e(" rank",(function(){o(y.asNonCompoundGraph(t))})),e(" injectEdgeLabelProxies",(function(){!function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);if(n.width&&n.height){var r=t.node(e.v),i={rank:(t.node(e.w).rank-r.rank)/2+r.rank,e};y.addDummyNode(t,"edge-proxy",i,"_ep")}}))}(t)})),e(" removeEmptyRanks",(function(){u(t)})),e(" nestingGraph.cleanup",(function(){l.cleanup(t)})),e(" normalizeRanks",(function(){s(t)})),e(" assignRankMinMax",(function(){!function(t){var e=0;r.forEach(t.nodes(),(function(n){var i=t.node(n);i.borderTop&&(i.minRank=t.node(i.borderTop).rank,i.maxRank=t.node(i.borderBottom).rank,e=r.max(e,i.maxRank))})),t.graph().maxRank=e}(t)})),e(" removeEdgeLabelProxies",(function(){!function(t){r.forEach(t.nodes(),(function(e){var n=t.node(e);"edge-proxy"===n.dummy&&(t.edge(n.e).labelRank=n.rank,t.removeNode(e))}))}(t)})),e(" normalize.run",(function(){a.run(t)})),e(" parentDummyChains",(function(){c(t)})),e(" addBorderSegments",(function(){h(t)})),e(" order",(function(){d(t)})),e(" insertSelfEdges",(function(){!function(t){var e=y.buildLayerMatrix(t);r.forEach(e,(function(e){var n=0;r.forEach(e,(function(e,i){var a=t.node(e);a.order=i+n,r.forEach(a.selfEdges,(function(e){y.addDummyNode(t,"selfedge",{width:e.label.width,height:e.label.height,rank:a.rank,order:i+ ++n,e:e.e,label:e.label},"_se")})),delete a.selfEdges}))}))}(t)})),e(" adjustCoordinateSystem",(function(){f.adjust(t)})),e(" position",(function(){p(t)})),e(" positionSelfEdges",(function(){!function(t){r.forEach(t.nodes(),(function(e){var n=t.node(e);if("selfedge"===n.dummy){var r=t.node(n.e.v),i=r.x+r.width/2,a=r.y,o=n.x-i,s=r.height/2;t.setEdge(n.e,n.label),t.removeNode(e),n.label.points=[{x:i+2*o/3,y:a-s},{x:i+5*o/6,y:a-s},{x:i+o,y:a},{x:i+5*o/6,y:a+s},{x:i+2*o/3,y:a+s}],n.label.x=n.x,n.label.y=n.y}}))}(t)})),e(" removeBorderNodes",(function(){!function(t){r.forEach(t.nodes(),(function(e){if(t.children(e).length){var n=t.node(e),i=t.node(n.borderTop),a=t.node(n.borderBottom),o=t.node(r.last(n.borderLeft)),s=t.node(r.last(n.borderRight));n.width=Math.abs(s.x-o.x),n.height=Math.abs(a.y-i.y),n.x=o.x+n.width/2,n.y=i.y+n.height/2}})),r.forEach(t.nodes(),(function(e){"border"===t.node(e).dummy&&t.removeNode(e)}))}(t)})),e(" normalize.undo",(function(){a.undo(t)})),e(" fixupEdgeLabelCoords",(function(){!function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);if(r.has(n,"x"))switch("l"!==n.labelpos&&"r"!==n.labelpos||(n.width-=n.labeloffset),n.labelpos){case"l":n.x-=n.width/2+n.labeloffset;break;case"r":n.x+=n.width/2+n.labeloffset}}))}(t)})),e(" undoCoordinateSystem",(function(){f.undo(t)})),e(" translateGraph",(function(){!function(t){var e=Number.POSITIVE_INFINITY,n=0,i=Number.POSITIVE_INFINITY,a=0,o=t.graph(),s=o.marginx||0,c=o.marginy||0;function u(t){var r=t.x,o=t.y,s=t.width,c=t.height;e=Math.min(e,r-s/2),n=Math.max(n,r+s/2),i=Math.min(i,o-c/2),a=Math.max(a,o+c/2)}r.forEach(t.nodes(),(function(e){u(t.node(e))})),r.forEach(t.edges(),(function(e){var n=t.edge(e);r.has(n,"x")&&u(n)})),e-=s,i-=c,r.forEach(t.nodes(),(function(n){var r=t.node(n);r.x-=e,r.y-=i})),r.forEach(t.edges(),(function(n){var a=t.edge(n);r.forEach(a.points,(function(t){t.x-=e,t.y-=i})),r.has(a,"x")&&(a.x-=e),r.has(a,"y")&&(a.y-=i)})),o.width=n-e+s,o.height=a-i+c}(t)})),e(" assignNodeIntersects",(function(){!function(t){r.forEach(t.edges(),(function(e){var n,r,i=t.edge(e),a=t.node(e.v),o=t.node(e.w);i.points?(n=i.points[0],r=i.points[i.points.length-1]):(i.points=[],n=o,r=a),i.points.unshift(y.intersectRect(a,n)),i.points.push(y.intersectRect(o,r))}))}(t)})),e(" reversePoints",(function(){!function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);n.reversed&&n.points.reverse()}))}(t)})),e(" acyclic.undo",(function(){i.undo(t)}))}(e,n)})),n(" updateInputGraph",(function(){!function(t,e){r.forEach(t.nodes(),(function(n){var r=t.node(n),i=e.node(n);r&&(r.x=i.x,r.y=i.y,e.children(n).length&&(r.width=i.width,r.height=i.height))})),r.forEach(t.edges(),(function(n){var i=t.edge(n),a=e.edge(n);i.points=a.points,r.has(a,"x")&&(i.x=a.x,i.y=a.y)})),t.graph().width=e.graph().width,t.graph().height=e.graph().height}(t,e)}))}))};var m=["nodesep","edgesep","ranksep","marginx","marginy"],v={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"},b=["acyclicer","ranker","rankdir","align"],_=["width","height"],x={width:0,height:0},w=["minlen","weight","width","height","labeloffset"],k={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"},T=["labelpos"];function E(t,e){return r.mapValues(r.pick(t,e),Number)}function C(t){var e={};return r.forEach(t,(function(t,n){e[n.toLowerCase()]=t})),e}},8436:(t,e,n)=>{var r;try{r={cloneDeep:n(361),constant:n(5703),defaults:n(1747),each:n(6073),filter:n(3105),find:n(3311),flatten:n(5564),forEach:n(4486),forIn:n(2620),has:n(8721),isUndefined:n(2353),last:n(928),map:n(5161),mapValues:n(6604),max:n(6162),merge:n(3857),min:n(3632),minBy:n(2762),now:n(7771),pick:n(9722),range:n(6026),reduce:n(4061),sortBy:n(9734),uniqueId:n(3955),values:n(2628),zipObject:n(7287)}}catch(t){}r||(r=window._),t.exports=r},2981:(t,e,n)=>{var r=n(8436),i=n(1138);function a(t,e,n,o,s,c,u){var l=t.children(u);if(l.length){var h=i.addBorderNode(t,"_bt"),f=i.addBorderNode(t,"_bb"),d=t.node(u);t.setParent(h,u),d.borderTop=h,t.setParent(f,u),d.borderBottom=f,r.forEach(l,(function(r){a(t,e,n,o,s,c,r);var i=t.node(r),l=i.borderTop?i.borderTop:r,d=i.borderBottom?i.borderBottom:r,p=i.borderTop?o:2*o,y=l!==d?1:s-c[u]+1;t.setEdge(h,l,{weight:p,minlen:y,nestingEdge:!0}),t.setEdge(d,f,{weight:p,minlen:y,nestingEdge:!0})})),t.parent(u)||t.setEdge(e,h,{weight:0,minlen:s+c[u]})}else u!==e&&t.setEdge(e,u,{weight:0,minlen:n})}t.exports={run:function(t){var e=i.addDummyNode(t,"root",{},"_root"),n=function(t){var e={};function n(i,a){var o=t.children(i);o&&o.length&&r.forEach(o,(function(t){n(t,a+1)})),e[i]=a}return r.forEach(t.children(),(function(t){n(t,1)})),e}(t),o=r.max(r.values(n))-1,s=2*o+1;t.graph().nestingRoot=e,r.forEach(t.edges(),(function(e){t.edge(e).minlen*=s}));var c=function(t){return r.reduce(t.edges(),(function(e,n){return e+t.edge(n).weight}),0)}(t)+1;r.forEach(t.children(),(function(r){a(t,e,s,c,o,n,r)})),t.graph().nodeRankFactor=s},cleanup:function(t){var e=t.graph();t.removeNode(e.nestingRoot),delete e.nestingRoot,r.forEach(t.edges(),(function(e){t.edge(e).nestingEdge&&t.removeEdge(e)}))}}},5995:(t,e,n)=>{"use strict";var r=n(8436),i=n(1138);t.exports={run:function(t){t.graph().dummyChains=[],r.forEach(t.edges(),(function(e){!function(t,e){var n,r,a,o=e.v,s=t.node(o).rank,c=e.w,u=t.node(c).rank,l=e.name,h=t.edge(e),f=h.labelRank;if(u!==s+1){for(t.removeEdge(e),a=0,++s;s{var r=n(8436);t.exports=function(t,e,n){var i,a={};r.forEach(n,(function(n){for(var r,o,s=t.parent(n);s;){if((r=t.parent(s))?(o=a[r],a[r]=s):(o=i,i=s),o&&o!==s)return void e.setEdge(o,s);s=r}}))}},5439:(t,e,n)=>{var r=n(8436);t.exports=function(t,e){return r.map(e,(function(e){var n=t.inEdges(e);if(n.length){var i=r.reduce(n,(function(e,n){var r=t.edge(n),i=t.node(n.v);return{sum:e.sum+r.weight*i.order,weight:e.weight+r.weight}}),{sum:0,weight:0});return{v:e,barycenter:i.sum/i.weight,weight:i.weight}}return{v:e}}))}},3128:(t,e,n)=>{var r=n(8436),i=n(574).Graph;t.exports=function(t,e,n){var a=function(t){for(var e;t.hasNode(e=r.uniqueId("_root")););return e}(t),o=new i({compound:!0}).setGraph({root:a}).setDefaultNodeLabel((function(e){return t.node(e)}));return r.forEach(t.nodes(),(function(i){var s=t.node(i),c=t.parent(i);(s.rank===e||s.minRank<=e&&e<=s.maxRank)&&(o.setNode(i),o.setParent(i,c||a),r.forEach(t[n](i),(function(e){var n=e.v===i?e.w:e.v,a=o.edge(n,i),s=r.isUndefined(a)?0:a.weight;o.setEdge(n,i,{weight:t.edge(e).weight+s})})),r.has(s,"minRank")&&o.setNode(i,{borderLeft:s.borderLeft[e],borderRight:s.borderRight[e]}))})),o}},6630:(t,e,n)=>{"use strict";var r=n(8436);function i(t,e,n){for(var i=r.zipObject(n,r.map(n,(function(t,e){return e}))),a=r.flatten(r.map(e,(function(e){return r.sortBy(r.map(t.outEdges(e),(function(e){return{pos:i[e.w],weight:t.edge(e).weight}})),"pos")})),!0),o=1;o0;)e%2&&(n+=c[e+1]),c[e=e-1>>1]+=t.weight;u+=t.weight*n}))),u}t.exports=function(t,e){for(var n=0,r=1;r{"use strict";var r=n(8436),i=n(2588),a=n(6630),o=n(1026),s=n(3128),c=n(5093),u=n(574).Graph,l=n(1138);function h(t,e,n){return r.map(e,(function(e){return s(t,e,n)}))}function f(t,e){var n=new u;r.forEach(t,(function(t){var i=t.graph().root,a=o(t,i,n,e);r.forEach(a.vs,(function(e,n){t.node(e).order=n})),c(t,n,a.vs)}))}function d(t,e){r.forEach(e,(function(e){r.forEach(e,(function(e,n){t.node(e).order=n}))}))}t.exports=function(t){var e=l.maxRank(t),n=h(t,r.range(1,e+1),"inEdges"),o=h(t,r.range(e-1,-1,-1),"outEdges"),s=i(t);d(t,s);for(var c,u=Number.POSITIVE_INFINITY,p=0,y=0;y<4;++p,++y){f(p%2?n:o,p%4>=2),s=l.buildLayerMatrix(t);var g=a(t,s);g{"use strict";var r=n(8436);t.exports=function(t){var e={},n=r.filter(t.nodes(),(function(e){return!t.children(e).length})),i=r.max(r.map(n,(function(e){return t.node(e).rank}))),a=r.map(r.range(i+1),(function(){return[]})),o=r.sortBy(n,(function(e){return t.node(e).rank}));return r.forEach(o,(function n(i){if(!r.has(e,i)){e[i]=!0;var o=t.node(i);a[o.rank].push(i),r.forEach(t.successors(i),n)}})),a}},9567:(t,e,n)=>{"use strict";var r=n(8436);t.exports=function(t,e){var n={};return r.forEach(t,(function(t,e){var i=n[t.v]={indegree:0,in:[],out:[],vs:[t.v],i:e};r.isUndefined(t.barycenter)||(i.barycenter=t.barycenter,i.weight=t.weight)})),r.forEach(e.edges(),(function(t){var e=n[t.v],i=n[t.w];r.isUndefined(e)||r.isUndefined(i)||(i.indegree++,e.out.push(n[t.w]))})),function(t){var e=[];function n(t){return function(e){var n,i,a,o;e.merged||(r.isUndefined(e.barycenter)||r.isUndefined(t.barycenter)||e.barycenter>=t.barycenter)&&(i=e,a=0,o=0,(n=t).weight&&(a+=n.barycenter*n.weight,o+=n.weight),i.weight&&(a+=i.barycenter*i.weight,o+=i.weight),n.vs=i.vs.concat(n.vs),n.barycenter=a/o,n.weight=o,n.i=Math.min(i.i,n.i),i.merged=!0)}}function i(e){return function(n){n.in.push(e),0==--n.indegree&&t.push(n)}}for(;t.length;){var a=t.pop();e.push(a),r.forEach(a.in.reverse(),n(a)),r.forEach(a.out,i(a))}return r.map(r.filter(e,(function(t){return!t.merged})),(function(t){return r.pick(t,["vs","i","barycenter","weight"])}))}(r.filter(n,(function(t){return!t.indegree})))}},1026:(t,e,n)=>{var r=n(8436),i=n(5439),a=n(9567),o=n(7304);t.exports=function t(e,n,s,c){var u=e.children(n),l=e.node(n),h=l?l.borderLeft:void 0,f=l?l.borderRight:void 0,d={};h&&(u=r.filter(u,(function(t){return t!==h&&t!==f})));var p=i(e,u);r.forEach(p,(function(n){if(e.children(n.v).length){var i=t(e,n.v,s,c);d[n.v]=i,r.has(i,"barycenter")&&(a=n,o=i,r.isUndefined(a.barycenter)?(a.barycenter=o.barycenter,a.weight=o.weight):(a.barycenter=(a.barycenter*a.weight+o.barycenter*o.weight)/(a.weight+o.weight),a.weight+=o.weight))}var a,o}));var y=a(p,s);!function(t,e){r.forEach(t,(function(t){t.vs=r.flatten(t.vs.map((function(t){return e[t]?e[t].vs:t})),!0)}))}(y,d);var g=o(y,c);if(h&&(g.vs=r.flatten([h,g.vs,f],!0),e.predecessors(h).length)){var m=e.node(e.predecessors(h)[0]),v=e.node(e.predecessors(f)[0]);r.has(g,"barycenter")||(g.barycenter=0,g.weight=0),g.barycenter=(g.barycenter*g.weight+m.order+v.order)/(g.weight+2),g.weight+=2}return g}},7304:(t,e,n)=>{var r=n(8436),i=n(1138);function a(t,e,n){for(var i;e.length&&(i=r.last(e)).i<=n;)e.pop(),t.push(i.vs),n++;return n}t.exports=function(t,e){var n,o=i.partition(t,(function(t){return r.has(t,"barycenter")})),s=o.lhs,c=r.sortBy(o.rhs,(function(t){return-t.i})),u=[],l=0,h=0,f=0;s.sort((n=!!e,function(t,e){return t.barycentere.barycenter?1:n?e.i-t.i:t.i-e.i})),f=a(u,c,f),r.forEach(s,(function(t){f+=t.vs.length,u.push(t.vs),l+=t.barycenter*t.weight,h+=t.weight,f=a(u,c,f)}));var d={vs:r.flatten(u,!0)};return h&&(d.barycenter=l/h,d.weight=h),d}},4219:(t,e,n)=>{var r=n(8436);t.exports=function(t){var e=function(t){var e={},n=0;return r.forEach(t.children(),(function i(a){var o=n;r.forEach(t.children(a),i),e[a]={low:o,lim:n++}})),e}(t);r.forEach(t.graph().dummyChains,(function(n){for(var r=t.node(n),i=r.edgeObj,a=function(t,e,n,r){var i,a,o=[],s=[],c=Math.min(e[n].low,e[r].low),u=Math.max(e[n].lim,e[r].lim);i=n;do{i=t.parent(i),o.push(i)}while(i&&(e[i].low>c||u>e[i].lim));for(a=i,i=r;(i=t.parent(i))!==a;)s.push(i);return{path:o.concat(s.reverse()),lca:a}}(t,e,i.v,i.w),o=a.path,s=a.lca,c=0,u=o[c],l=!0;n!==i.w;){if(r=t.node(n),l){for(;(u=o[c])!==s&&t.node(u).maxRank{"use strict";var r=n(8436),i=n(574).Graph,a=n(1138);function o(t,e){var n={};return r.reduce(e,(function(e,i){var a=0,o=0,s=e.length,u=r.last(i);return r.forEach(i,(function(e,l){var h=function(t,e){if(t.node(e).dummy)return r.find(t.predecessors(e),(function(e){return t.node(e).dummy}))}(t,e),f=h?t.node(h).order:s;(h||e===u)&&(r.forEach(i.slice(o,l+1),(function(e){r.forEach(t.predecessors(e),(function(r){var i=t.node(r),o=i.order;!(os)&&c(n,e,u)}))}))}return r.reduce(e,(function(e,n){var a,o=-1,s=0;return r.forEach(n,(function(r,c){if("border"===t.node(r).dummy){var u=t.predecessors(r);u.length&&(a=t.node(u[0]).order,i(n,s,c,o,a),s=c,o=a)}i(n,s,n.length,a,e.length)})),n})),n}function c(t,e,n){if(e>n){var r=e;e=n,n=r}var i=t[e];i||(t[e]=i={}),i[n]=!0}function u(t,e,n){if(e>n){var i=e;e=n,n=i}return r.has(t[e],n)}function l(t,e,n,i){var a={},o={},s={};return r.forEach(e,(function(t){r.forEach(t,(function(t,e){a[t]=t,o[t]=t,s[t]=e}))})),r.forEach(e,(function(t){var e=-1;r.forEach(t,(function(t){var c=i(t);if(c.length){c=r.sortBy(c,(function(t){return s[t]}));for(var l=(c.length-1)/2,h=Math.floor(l),f=Math.ceil(l);h<=f;++h){var d=c[h];o[t]===t&&e{"use strict";var r=n(8436),i=n(1138),a=n(3573).positionX;t.exports=function(t){(function(t){var e=i.buildLayerMatrix(t),n=t.graph().ranksep,a=0;r.forEach(e,(function(e){var i=r.max(r.map(e,(function(e){return t.node(e).height})));r.forEach(e,(function(e){t.node(e).y=a+i/2})),a+=i+n}))})(t=i.asNonCompoundGraph(t)),r.forEach(a(t),(function(e,n){t.node(n).x=e}))}},300:(t,e,n)=>{"use strict";var r=n(8436),i=n(574).Graph,a=n(6681).slack;function o(t,e){return r.forEach(t.nodes(),(function n(i){r.forEach(e.nodeEdges(i),(function(r){var o=r.v,s=i===o?r.w:o;t.hasNode(s)||a(e,r)||(t.setNode(s,{}),t.setEdge(i,s,{}),n(s))}))})),t.nodeCount()}function s(t,e){return r.minBy(e.edges(),(function(n){if(t.hasNode(n.v)!==t.hasNode(n.w))return a(e,n)}))}function c(t,e,n){r.forEach(t.nodes(),(function(t){e.node(t).rank+=n}))}t.exports=function(t){var e,n,r=new i({directed:!1}),u=t.nodes()[0],l=t.nodeCount();for(r.setNode(u,{});o(r,t){"use strict";var r=n(6681).longestPath,i=n(300),a=n(2472);t.exports=function(t){switch(t.graph().ranker){case"network-simplex":default:!function(t){a(t)}(t);break;case"tight-tree":!function(t){r(t),i(t)}(t);break;case"longest-path":o(t)}};var o=r},2472:(t,e,n)=>{"use strict";var r=n(8436),i=n(300),a=n(6681).slack,o=n(6681).longestPath,s=n(574).alg.preorder,c=n(574).alg.postorder,u=n(1138).simplify;function l(t){t=u(t),o(t);var e,n=i(t);for(d(n),h(n,t);e=y(n);)m(n,t,e,g(n,t,e))}function h(t,e){var n=c(t,t.nodes());n=n.slice(0,n.length-1),r.forEach(n,(function(n){!function(t,e,n){var r=t.node(n).parent;t.edge(n,r).cutvalue=f(t,e,n)}(t,e,n)}))}function f(t,e,n){var i=t.node(n).parent,a=!0,o=e.edge(n,i),s=0;return o||(a=!1,o=e.edge(i,n)),s=o.weight,r.forEach(e.nodeEdges(n),(function(r){var o,c,u=r.v===n,l=u?r.w:r.v;if(l!==i){var h=u===a,f=e.edge(r).weight;if(s+=h?f:-f,o=n,c=l,t.hasEdge(o,c)){var d=t.edge(n,l).cutvalue;s+=h?-d:d}}})),s}function d(t,e){arguments.length<2&&(e=t.nodes()[0]),p(t,{},1,e)}function p(t,e,n,i,a){var o=n,s=t.node(i);return e[i]=!0,r.forEach(t.neighbors(i),(function(a){r.has(e,a)||(n=p(t,e,n,a,i))})),s.low=o,s.lim=n++,a?s.parent=a:delete s.parent,n}function y(t){return r.find(t.edges(),(function(e){return t.edge(e).cutvalue<0}))}function g(t,e,n){var i=n.v,o=n.w;e.hasEdge(i,o)||(i=n.w,o=n.v);var s=t.node(i),c=t.node(o),u=s,l=!1;s.lim>c.lim&&(u=c,l=!0);var h=r.filter(e.edges(),(function(e){return l===v(0,t.node(e.v),u)&&l!==v(0,t.node(e.w),u)}));return r.minBy(h,(function(t){return a(e,t)}))}function m(t,e,n,i){var a=n.v,o=n.w;t.removeEdge(a,o),t.setEdge(i.v,i.w,{}),d(t),h(t,e),function(t,e){var n=r.find(t.nodes(),(function(t){return!e.node(t).parent})),i=s(t,n);i=i.slice(1),r.forEach(i,(function(n){var r=t.node(n).parent,i=e.edge(n,r),a=!1;i||(i=e.edge(r,n),a=!0),e.node(n).rank=e.node(r).rank+(a?i.minlen:-i.minlen)}))}(t,e)}function v(t,e,n){return n.low<=e.lim&&e.lim<=n.lim}t.exports=l,l.initLowLimValues=d,l.initCutValues=h,l.calcCutValue=f,l.leaveEdge=y,l.enterEdge=g,l.exchangeEdges=m},6681:(t,e,n)=>{"use strict";var r=n(8436);t.exports={longestPath:function(t){var e={};r.forEach(t.sources(),(function n(i){var a=t.node(i);if(r.has(e,i))return a.rank;e[i]=!0;var o=r.min(r.map(t.outEdges(i),(function(e){return n(e.w)-t.edge(e).minlen})));return o!==Number.POSITIVE_INFINITY&&null!=o||(o=0),a.rank=o}))},slack:function(t,e){return t.node(e.w).rank-t.node(e.v).rank-t.edge(e).minlen}}},1138:(t,e,n)=>{"use strict";var r=n(8436),i=n(574).Graph;function a(t,e,n,i){var a;do{a=r.uniqueId(i)}while(t.hasNode(a));return n.dummy=e,t.setNode(a,n),a}function o(t){return r.max(r.map(t.nodes(),(function(e){var n=t.node(e).rank;if(!r.isUndefined(n))return n})))}t.exports={addDummyNode:a,simplify:function(t){var e=(new i).setGraph(t.graph());return r.forEach(t.nodes(),(function(n){e.setNode(n,t.node(n))})),r.forEach(t.edges(),(function(n){var r=e.edge(n.v,n.w)||{weight:0,minlen:1},i=t.edge(n);e.setEdge(n.v,n.w,{weight:r.weight+i.weight,minlen:Math.max(r.minlen,i.minlen)})})),e},asNonCompoundGraph:function(t){var e=new i({multigraph:t.isMultigraph()}).setGraph(t.graph());return r.forEach(t.nodes(),(function(n){t.children(n).length||e.setNode(n,t.node(n))})),r.forEach(t.edges(),(function(n){e.setEdge(n,t.edge(n))})),e},successorWeights:function(t){var e=r.map(t.nodes(),(function(e){var n={};return r.forEach(t.outEdges(e),(function(e){n[e.w]=(n[e.w]||0)+t.edge(e).weight})),n}));return r.zipObject(t.nodes(),e)},predecessorWeights:function(t){var e=r.map(t.nodes(),(function(e){var n={};return r.forEach(t.inEdges(e),(function(e){n[e.v]=(n[e.v]||0)+t.edge(e).weight})),n}));return r.zipObject(t.nodes(),e)},intersectRect:function(t,e){var n,r,i=t.x,a=t.y,o=e.x-i,s=e.y-a,c=t.width/2,u=t.height/2;if(!o&&!s)throw new Error("Not possible to find intersection inside of the rectangle");return Math.abs(s)*c>Math.abs(o)*u?(s<0&&(u=-u),n=u*o/s,r=u):(o<0&&(c=-c),n=c,r=c*s/o),{x:i+n,y:a+r}},buildLayerMatrix:function(t){var e=r.map(r.range(o(t)+1),(function(){return[]}));return r.forEach(t.nodes(),(function(n){var i=t.node(n),a=i.rank;r.isUndefined(a)||(e[a][i.order]=n)})),e},normalizeRanks:function(t){var e=r.min(r.map(t.nodes(),(function(e){return t.node(e).rank})));r.forEach(t.nodes(),(function(n){var i=t.node(n);r.has(i,"rank")&&(i.rank-=e)}))},removeEmptyRanks:function(t){var e=r.min(r.map(t.nodes(),(function(e){return t.node(e).rank}))),n=[];r.forEach(t.nodes(),(function(r){var i=t.node(r).rank-e;n[i]||(n[i]=[]),n[i].push(r)}));var i=0,a=t.graph().nodeRankFactor;r.forEach(n,(function(e,n){r.isUndefined(e)&&n%a!=0?--i:i&&r.forEach(e,(function(e){t.node(e).rank+=i}))}))},addBorderNode:function(t,e,n,r){var i={width:0,height:0};return arguments.length>=4&&(i.rank=n,i.order=r),a(t,"border",i,e)},maxRank:o,partition:function(t,e){var n={lhs:[],rhs:[]};return r.forEach(t,(function(t){e(t)?n.lhs.push(t):n.rhs.push(t)})),n},time:function(t,e){var n=r.now();try{return e()}finally{console.log(t+" time: "+(r.now()-n)+"ms")}},notime:function(t,e){return e()}}},8177:t=>{t.exports="0.8.5"},7856:function(t){t.exports=function(){"use strict";var t=Object.hasOwnProperty,e=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,i=Object.getOwnPropertyDescriptor,a=Object.freeze,o=Object.seal,s=Object.create,c="undefined"!=typeof Reflect&&Reflect,u=c.apply,l=c.construct;u||(u=function(t,e,n){return t.apply(e,n)}),a||(a=function(t){return t}),o||(o=function(t){return t}),l||(l=function(t,e){return new(Function.prototype.bind.apply(t,[null].concat(function(t){if(Array.isArray(t)){for(var e=0,n=Array(t.length);e1?n-1:0),i=1;i/gm),j=o(/^data-[\-\w.\u00B7-\uFFFF]/),Y=o(/^aria-[\-\w]+$/),z=o(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),U=o(/^(?:\w+script|data):/i),q=o(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),H="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function $(t){if(Array.isArray(t)){for(var e=0,n=Array(t.length);e0&&void 0!==arguments[0]?arguments[0]:W(),n=function(e){return t(e)};if(n.version="2.3.5",n.removed=[],!e||!e.document||9!==e.document.nodeType)return n.isSupported=!1,n;var r=e.document,i=e.document,o=e.DocumentFragment,s=e.HTMLTemplateElement,c=e.Node,u=e.Element,l=e.NodeFilter,h=e.NamedNodeMap,w=void 0===h?e.NamedNodeMap||e.MozNamedAttrMap:h,G=e.HTMLFormElement,X=e.DOMParser,Z=e.trustedTypes,Q=u.prototype,K=E(Q,"cloneNode"),J=E(Q,"nextSibling"),tt=E(Q,"childNodes"),et=E(Q,"parentNode");if("function"==typeof s){var nt=i.createElement("template");nt.content&&nt.content.ownerDocument&&(i=nt.content.ownerDocument)}var rt=V(Z,r),it=rt?rt.createHTML(""):"",at=i,ot=at.implementation,st=at.createNodeIterator,ct=at.createDocumentFragment,ut=at.getElementsByTagName,lt=r.importNode,ht={};try{ht=T(i).documentMode?i.documentMode:{}}catch(t){}var ft={};n.isSupported="function"==typeof et&&ot&&void 0!==ot.createHTMLDocument&&9!==ht;var dt=F,pt=P,yt=j,gt=Y,mt=U,vt=q,bt=z,_t=null,xt=k({},[].concat($(C),$(S),$(A),$(N),$(O))),wt=null,kt=k({},[].concat($(B),$(L),$(I),$(R))),Tt=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Et=null,Ct=null,St=!0,At=!0,Mt=!1,Nt=!1,Dt=!1,Ot=!1,Bt=!1,Lt=!1,It=!1,Rt=!1,Ft=!0,Pt=!0,jt=!1,Yt={},zt=null,Ut=k({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),qt=null,Ht=k({},["audio","video","img","source","image","track"]),$t=null,Wt=k({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Vt="http://www.w3.org/1998/Math/MathML",Gt="http://www.w3.org/2000/svg",Xt="http://www.w3.org/1999/xhtml",Zt=Xt,Qt=!1,Kt=void 0,Jt=["application/xhtml+xml","text/html"],te="text/html",ee=void 0,ne=null,re=i.createElement("form"),ie=function(t){return t instanceof RegExp||t instanceof Function},ae=function(t){ne&&ne===t||(t&&"object"===(void 0===t?"undefined":H(t))||(t={}),t=T(t),_t="ALLOWED_TAGS"in t?k({},t.ALLOWED_TAGS):xt,wt="ALLOWED_ATTR"in t?k({},t.ALLOWED_ATTR):kt,$t="ADD_URI_SAFE_ATTR"in t?k(T(Wt),t.ADD_URI_SAFE_ATTR):Wt,qt="ADD_DATA_URI_TAGS"in t?k(T(Ht),t.ADD_DATA_URI_TAGS):Ht,zt="FORBID_CONTENTS"in t?k({},t.FORBID_CONTENTS):Ut,Et="FORBID_TAGS"in t?k({},t.FORBID_TAGS):{},Ct="FORBID_ATTR"in t?k({},t.FORBID_ATTR):{},Yt="USE_PROFILES"in t&&t.USE_PROFILES,St=!1!==t.ALLOW_ARIA_ATTR,At=!1!==t.ALLOW_DATA_ATTR,Mt=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Nt=t.SAFE_FOR_TEMPLATES||!1,Dt=t.WHOLE_DOCUMENT||!1,Lt=t.RETURN_DOM||!1,It=t.RETURN_DOM_FRAGMENT||!1,Rt=t.RETURN_TRUSTED_TYPE||!1,Bt=t.FORCE_BODY||!1,Ft=!1!==t.SANITIZE_DOM,Pt=!1!==t.KEEP_CONTENT,jt=t.IN_PLACE||!1,bt=t.ALLOWED_URI_REGEXP||bt,Zt=t.NAMESPACE||Xt,t.CUSTOM_ELEMENT_HANDLING&&ie(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Tt.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&ie(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Tt.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Tt.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Kt=Kt=-1===Jt.indexOf(t.PARSER_MEDIA_TYPE)?te:t.PARSER_MEDIA_TYPE,ee="application/xhtml+xml"===Kt?function(t){return t}:y,Nt&&(At=!1),It&&(Lt=!0),Yt&&(_t=k({},[].concat($(O))),wt=[],!0===Yt.html&&(k(_t,C),k(wt,B)),!0===Yt.svg&&(k(_t,S),k(wt,L),k(wt,R)),!0===Yt.svgFilters&&(k(_t,A),k(wt,L),k(wt,R)),!0===Yt.mathMl&&(k(_t,N),k(wt,I),k(wt,R))),t.ADD_TAGS&&(_t===xt&&(_t=T(_t)),k(_t,t.ADD_TAGS)),t.ADD_ATTR&&(wt===kt&&(wt=T(wt)),k(wt,t.ADD_ATTR)),t.ADD_URI_SAFE_ATTR&&k($t,t.ADD_URI_SAFE_ATTR),t.FORBID_CONTENTS&&(zt===Ut&&(zt=T(zt)),k(zt,t.FORBID_CONTENTS)),Pt&&(_t["#text"]=!0),Dt&&k(_t,["html","head","body"]),_t.table&&(k(_t,["tbody"]),delete Et.tbody),a&&a(t),ne=t)},oe=k({},["mi","mo","mn","ms","mtext"]),se=k({},["foreignobject","desc","title","annotation-xml"]),ce=k({},S);k(ce,A),k(ce,M);var ue=k({},N);k(ue,D);var le=function(t){var e=et(t);e&&e.tagName||(e={namespaceURI:Xt,tagName:"template"});var n=y(t.tagName),r=y(e.tagName);if(t.namespaceURI===Gt)return e.namespaceURI===Xt?"svg"===n:e.namespaceURI===Vt?"svg"===n&&("annotation-xml"===r||oe[r]):Boolean(ce[n]);if(t.namespaceURI===Vt)return e.namespaceURI===Xt?"math"===n:e.namespaceURI===Gt?"math"===n&&se[r]:Boolean(ue[n]);if(t.namespaceURI===Xt){if(e.namespaceURI===Gt&&!se[r])return!1;if(e.namespaceURI===Vt&&!oe[r])return!1;var i=k({},["title","style","font","a","script"]);return!ue[n]&&(i[n]||!ce[n])}return!1},he=function(t){p(n.removed,{element:t});try{t.parentNode.removeChild(t)}catch(e){try{t.outerHTML=it}catch(e){t.remove()}}},fe=function(t,e){try{p(n.removed,{attribute:e.getAttributeNode(t),from:e})}catch(t){p(n.removed,{attribute:null,from:e})}if(e.removeAttribute(t),"is"===t&&!wt[t])if(Lt||It)try{he(e)}catch(t){}else try{e.setAttribute(t,"")}catch(t){}},de=function(t){var e=void 0,n=void 0;if(Bt)t=""+t;else{var r=g(t,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===Kt&&(t=''+t+"");var a=rt?rt.createHTML(t):t;if(Zt===Xt)try{e=(new X).parseFromString(a,Kt)}catch(t){}if(!e||!e.documentElement){e=ot.createDocument(Zt,"template",null);try{e.documentElement.innerHTML=Qt?"":a}catch(t){}}var o=e.body||e.documentElement;return t&&n&&o.insertBefore(i.createTextNode(n),o.childNodes[0]||null),Zt===Xt?ut.call(e,Dt?"html":"body")[0]:Dt?e.documentElement:o},pe=function(t){return st.call(t.ownerDocument||t,t,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT,null,!1)},ye=function(t){return t instanceof G&&("string"!=typeof t.nodeName||"string"!=typeof t.textContent||"function"!=typeof t.removeChild||!(t.attributes instanceof w)||"function"!=typeof t.removeAttribute||"function"!=typeof t.setAttribute||"string"!=typeof t.namespaceURI||"function"!=typeof t.insertBefore)},ge=function(t){return"object"===(void 0===c?"undefined":H(c))?t instanceof c:t&&"object"===(void 0===t?"undefined":H(t))&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},me=function(t,e,r){ft[t]&&f(ft[t],(function(t){t.call(n,e,r,ne)}))},ve=function(t){var e=void 0;if(me("beforeSanitizeElements",t,null),ye(t))return he(t),!0;if(g(t.nodeName,/[\u0080-\uFFFF]/))return he(t),!0;var r=ee(t.nodeName);if(me("uponSanitizeElement",t,{tagName:r,allowedTags:_t}),!ge(t.firstElementChild)&&(!ge(t.content)||!ge(t.content.firstElementChild))&&_(/<[/\w]/g,t.innerHTML)&&_(/<[/\w]/g,t.textContent))return he(t),!0;if("select"===r&&_(/