From 0fe2c5a2c0eac1a9f8a322446f152ff03681856e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 2 Sep 2022 19:26:59 +0200 Subject: [PATCH] feat: introduce AnnotationDependentResourceConfigurator concept The idea is to be able to configure dependents using annotations directly so that we can remove the special handling of KubernetesDependentResource from AnnotationControllerConfiguration. This results in dependent resources being instantiated and configured directly when processed from the annotations in the managed case, thus rendering the DependentResourceFactory concept obsolete. This should also lead to further simplification later. --- .../AnnotationControllerConfiguration.java | 85 ++++++------------- .../api/config/ConfigurationService.java | 3 +- .../ControllerConfigurationOverrider.java | 57 +++++-------- .../api/config/NamespaceChangeable.java | 4 +- .../operator/api/config/Utils.java | 9 +- .../dependent/DependentResourceSpec.java | 44 +++++++--- .../dependent/DependentResourceFactory.java | 17 ++-- ...notationDependentResourceConfigurator.java | 11 +++ .../DependentResourceConfigurator.java | 4 + .../KubernetesDependentResource.java | 60 ++++++++++++- .../KubernetesDependentResourceConfig.java | 16 ++-- .../workflow/ManagedWorkflowSupport.java | 12 +-- .../ConfigurationServiceOverriderTest.java | 15 ---- .../ControllerConfigurationOverriderTest.java | 17 +++- .../operator/api/config/UtilsTest.java | 6 +- .../workflow/ManagedWorkflowTestUtils.java | 5 +- .../sample/dependent/SchemaConfig.java | 23 +++++ .../dependent/SchemaDependentResource.java | 31 ++++++- .../sample/MySQLSchemaOperatorE2E.java | 17 ++-- 19 files changed, 257 insertions(+), 179 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaConfig.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 index 8eb713fa73..43c61319ac 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 @@ -2,7 +2,6 @@ import java.lang.annotation.Annotation; import java.time.Duration; -import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -15,20 +14,18 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Constants; 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.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; 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.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.retry.Retry; @@ -158,7 +155,7 @@ public Retry getRetry() { @SuppressWarnings("unchecked") - private void configureFromAnnotatedReconciler(T instance) { + private void configureFromAnnotatedReconciler(Object instance) { if (instance instanceof AnnotationConfigurable) { AnnotationConfigurable configurable = (AnnotationConfigurable) instance; final Class configurationClass = @@ -171,6 +168,22 @@ private void configureFromAnnotatedReconciler(T instance) { } } + @SuppressWarnings("unchecked") + private void configureFromCustomAnnotation(Object instance) { + if (instance instanceof AnnotationDependentResourceConfigurator) { + AnnotationDependentResourceConfigurator configurator = + (AnnotationDependentResourceConfigurator) instance; + final Class configurationClass = + (Class) Utils.getFirstTypeArgumentFromInterface( + instance.getClass(), AnnotationDependentResourceConfigurator.class); + final var configAnnotation = instance.getClass().getAnnotation(configurationClass); + // always called even if the annotation is null so that implementations can provide default + // values + final var config = configurator.configFrom(configAnnotation, this); + configurator.configureWith(config); + } + } + @Override @SuppressWarnings("unchecked") public Optional> onAddFilter() { @@ -208,11 +221,7 @@ public List getDependentResources() { final var specsMap = new LinkedHashMap(dependents.length); for (Dependent dependent : dependents) { - Object config = null; final Class dependentType = dependent.type(); - if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { - config = createKubernetesResourceConfig(dependentType); - } final var name = getName(dependent, dependentType); var spec = specsMap.get(name); @@ -220,10 +229,16 @@ public List getDependentResources() { throw new IllegalArgumentException( "A DependentResource named '" + name + "' already exists: " + spec); } + + final var dependentResource = Utils.instantiateAndConfigureIfNeeded(dependentType, + DependentResource.class, + Utils.contextFor(this, dependentType, Dependent.class), + this::configureFromCustomAnnotation); + var eventSourceName = dependent.useEventSourceWithName(); eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName; final var context = Utils.contextFor(this, dependentType, null); - spec = new DependentResourceSpec(dependentType, config, name, + spec = new DependentResourceSpec(dependentResource, name, Set.of(dependent.dependsOn()), Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), @@ -245,52 +260,6 @@ private String getName(Dependent dependent, Class d return name; } - @SuppressWarnings({"rawtypes", "unchecked"}) - private Object createKubernetesResourceConfig(Class dependentType) { - - Object config; - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - - var namespaces = getNamespaces(); - var configuredNS = false; - String labelSelector = null; - OnAddFilter onAddFilter = null; - OnUpdateFilter onUpdateFilter = null; - OnDeleteFilter onDeleteFilter = null; - GenericFilter genericFilter = null; - ResourceDiscriminator resourceDiscriminator = null; - if (kubeDependent != null) { - if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, - kubeDependent.namespaces())) { - namespaces = Set.of(kubeDependent.namespaces()); - configuredNS = true; - } - final var fromAnnotation = kubeDependent.labelSelector(); - labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation; - - final var context = - Utils.contextFor(this, dependentType, null); - onAddFilter = Utils.instantiate(kubeDependent.onAddFilter(), OnAddFilter.class, context); - onUpdateFilter = - Utils.instantiate(kubeDependent.onUpdateFilter(), OnUpdateFilter.class, context); - onDeleteFilter = - Utils.instantiate(kubeDependent.onDeleteFilter(), OnDeleteFilter.class, context); - genericFilter = - Utils.instantiate(kubeDependent.genericFilter(), GenericFilter.class, context); - - resourceDiscriminator = - Utils.instantiate(kubeDependent.resourceDiscriminator(), - ResourceDiscriminator.class, context); - } - - config = - new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS, - resourceDiscriminator, onAddFilter, - onUpdateFilter, onDeleteFilter, genericFilter); - - return config; - } - public static T valueOrDefault( ControllerConfiguration controllerConfiguration, Function mapper, 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 00e0b84526..d03cd9f2fa 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 @@ -140,8 +140,9 @@ default ObjectMapper getObjectMapper() { return Serialization.jsonMapper(); } + @Deprecated(forRemoval = true) default DependentResourceFactory dependentResourceFactory() { - return new DependentResourceFactory() {}; + return null; } default Optional getLeaderElectionConfiguration() { 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 511596db00..c36aa51d2e 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 @@ -6,12 +6,11 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -159,6 +158,7 @@ public ControllerConfigurationOverrider withGenericFilter(GenericFilter ge return this; } + @SuppressWarnings("unchecked") public ControllerConfigurationOverrider replacingNamedDependentResourceConfig(String name, Object dependentResourceConfig) { @@ -166,35 +166,31 @@ public ControllerConfigurationOverrider replacingNamedDependentResourceConfig if (current == null) { throw new IllegalArgumentException("Cannot find a DependentResource named: " + name); } - replaceConfig(name, dependentResourceConfig, current); - return this; - } - private void replaceConfig(String name, Object newConfig, DependentResourceSpec current) { - namedDependentResourceSpecs.put(name, - new DependentResourceSpec<>(current.getDependentResourceClass(), newConfig, name, - current.getDependsOn(), current.getReadyCondition(), current.getReconcileCondition(), - current.getDeletePostCondition(), current.getUseEventSourceWithName().orElse(null))); + var dependentResource = current.getDependentResource(); + if (dependentResource instanceof DependentResourceConfigurator) { + var configurator = (DependentResourceConfigurator) dependentResource; + configurator.configureWith(dependentResourceConfig); + } + + return this; } - @SuppressWarnings("unchecked") public ControllerConfiguration build() { - // propagate namespaces if needed - final List newDependentSpecs; final var hasModifiedNamespaces = !original.getNamespaces().equals(namespaces); - newDependentSpecs = namedDependentResourceSpecs.entrySet().stream() - .map(drsEntry -> { - final var spec = drsEntry.getValue(); - - // if the spec has a config and it's a KubernetesDependentResourceConfig, update the - // namespaces if needed, otherwise, just return the existing spec - final Optional maybeConfig = spec.getDependentResourceConfiguration(); - return maybeConfig.filter(KubernetesDependentResourceConfig.class::isInstance) - .map(KubernetesDependentResourceConfig.class::cast) - .filter(Predicate.not(KubernetesDependentResourceConfig::wereNamespacesConfigured)) - .map(c -> updateSpec(drsEntry.getKey(), spec, c)) - .orElse(drsEntry.getValue()); - }).collect(Collectors.toUnmodifiableList()); + final var newDependentSpecs = namedDependentResourceSpecs.values().stream() + .peek(spec -> { + // if the dependent resource has a NamespaceChangeable config + // update the namespaces if needed, otherwise, do nothing + if (hasModifiedNamespaces) { + final Optional maybeConfig = spec.getDependentResourceConfiguration(); + maybeConfig + .filter(NamespaceChangeable.class::isInstance) + .map(NamespaceChangeable.class::cast) + .filter(NamespaceChangeable::allowsNamespaceChanges) + .ifPresent(nc -> nc.changeNamespaces(namespaces)); + } + }).collect(Collectors.toList()); return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), @@ -215,15 +211,6 @@ public ControllerConfiguration build() { newDependentSpecs); } - @SuppressWarnings({"rawtypes", "unchecked"}) - private DependentResourceSpec updateSpec(String name, DependentResourceSpec spec, - KubernetesDependentResourceConfig c) { - return new DependentResourceSpec(spec.getDependentResourceClass(), - c.setNamespaces(namespaces), name, spec.getDependsOn(), spec.getReadyCondition(), - spec.getReconcileCondition(), spec.getDeletePostCondition(), - (String) spec.getUseEventSourceWithName().orElse(null)); - } - public static ControllerConfigurationOverrider override( ControllerConfiguration original) { return new ControllerConfigurationOverrider<>(original); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/NamespaceChangeable.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/NamespaceChangeable.java index 4e3cb19ed9..426b179438 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/NamespaceChangeable.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/NamespaceChangeable.java @@ -16,9 +16,9 @@ public interface NamespaceChangeable { */ void changeNamespaces(Set namespaces); + @SuppressWarnings("unused") default void changeNamespaces(String... namespaces) { - changeNamespaces( - namespaces != null ? Set.of(namespaces) : DEFAULT_NAMESPACES_SET); + changeNamespaces(namespaces != null ? Set.of(namespaces) : DEFAULT_NAMESPACES_SET); } default boolean allowsNamespaceChanges() { 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 98d835af58..fd0f612fa1 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 @@ -111,6 +111,11 @@ public static Class getFirstTypeArgumentFromExtendedClass(Class clazz) { public static Class getFirstTypeArgumentFromInterface(Class clazz, Class expectedImplementedInterface) { + return getTypeArgumentFromInterfaceByIndex(clazz, expectedImplementedInterface, 0); + } + + public static Class getTypeArgumentFromInterfaceByIndex(Class clazz, + Class expectedImplementedInterface, int index) { if (expectedImplementedInterface.isAssignableFrom(clazz)) { final var genericInterfaces = clazz.getGenericInterfaces(); Optional> target = Optional.empty(); @@ -122,7 +127,7 @@ public static Class getFirstTypeArgumentFromInterface(Class clazz, .map(ParameterizedType.class::cast) .findFirst() .map(t -> { - final Type argument = t.getActualTypeArguments()[0]; + final Type argument = t.getActualTypeArguments()[index]; if (argument instanceof Class) { return (Class) argument; } @@ -148,7 +153,7 @@ public static Class getFirstTypeArgumentFromInterface(Class clazz, // try the parent var parent = clazz.getSuperclass(); if (!Object.class.equals(parent)) { - return getFirstTypeArgumentFromInterface(parent, expectedImplementedInterface); + return getTypeArgumentFromInterfaceByIndex(parent, expectedImplementedInterface, index); } } throw new IllegalArgumentException("Couldn't retrieve generic parameter type from " 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 426c1d3325..5d0b8c6b01 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,14 +4,14 @@ import java.util.Optional; import java.util.Set; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; -public class DependentResourceSpec, C> { +public class DependentResourceSpec { - private final Class dependentResourceClass; - - private final C dependentResourceConfig; + private final DependentResource dependentResource; private final String name; @@ -25,12 +25,11 @@ public class DependentResourceSpec, C> { private final String useEventSourceWithName; - public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig, + public DependentResourceSpec(DependentResource dependentResource, String name, Set dependsOn, Condition readyCondition, Condition reconcileCondition, Condition deletePostCondition, String useEventSourceWithName) { - this.dependentResourceClass = dependentResourceClass; - this.dependentResourceConfig = dependentResourceConfig; + this.dependentResource = dependentResource; this.name = name; this.dependsOn = dependsOn; this.readyCondition = readyCondition; @@ -39,12 +38,28 @@ public DependentResourceSpec(Class dependentResourceClass, C dependentResourc this.useEventSourceWithName = useEventSourceWithName; } - public Class getDependentResourceClass() { - return dependentResourceClass; + public DependentResourceSpec(DependentResourceSpec other) { + this.dependentResource = other.dependentResource; + this.name = other.name; + this.dependsOn = other.dependsOn; + this.readyCondition = other.readyCondition; + this.reconcileCondition = other.reconcileCondition; + this.deletePostCondition = other.deletePostCondition; + this.useEventSourceWithName = other.useEventSourceWithName; + } + + @SuppressWarnings("unchecked") + public Class> getDependentResourceClass() { + return (Class>) dependentResource.getClass(); } + @SuppressWarnings({"unchecked", "rawtypes"}) public Optional getDependentResourceConfiguration() { - return Optional.ofNullable(dependentResourceConfig); + if (dependentResource instanceof DependentResourceConfigurator) { + var configurator = (DependentResourceConfigurator) dependentResource; + return configurator.configuration(); + } + return Optional.empty(); } public String getName() { @@ -54,8 +69,7 @@ public String getName() { @Override public String toString() { return "DependentResourceSpec{ name='" + name + - "', type=" + dependentResourceClass.getCanonicalName() + - ", config=" + dependentResourceConfig + '}'; + "', type=" + getDependentResourceClass().getCanonicalName() + '}'; } @Override @@ -66,7 +80,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - DependentResourceSpec that = (DependentResourceSpec) o; + DependentResourceSpec that = (DependentResourceSpec) o; return name.equals(that.name); } @@ -94,6 +108,10 @@ public Condition getDeletePostCondition() { return deletePostCondition; } + public DependentResource getDependentResource() { + return dependentResource; + } + public Optional getUseEventSourceWithName() { return Optional.ofNullable(useEventSourceWithName); } 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 index 11b6e1f059..139d70b002 100644 --- 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 @@ -1,23 +1,18 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; -import java.lang.reflect.InvocationTargetException; - +import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +@Deprecated +@SuppressWarnings({"rawtypes", "unchecked"}) public interface DependentResourceFactory { - default > T createFrom(DependentResourceSpec spec) { + default DependentResource createFrom(DependentResourceSpec spec) { return createFrom(spec.getDependentResourceClass()); } - default > T createFrom(Class dependentResourceClass) { - try { - return dependentResourceClass.getConstructor().newInstance(); - } catch (InstantiationException | NoSuchMethodException | IllegalAccessException - | InvocationTargetException e) { - throw new IllegalArgumentException("Cannot instantiate DependentResource " - + dependentResourceClass.getCanonicalName(), e); - } + default T createFrom(Class dependentResourceClass) { + return (T) Utils.instantiate(dependentResourceClass, DependentResource.class, null); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java new file mode 100644 index 0000000000..d65249b753 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; + +import java.lang.annotation.Annotation; + +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; + +public interface AnnotationDependentResourceConfigurator + extends DependentResourceConfigurator { + + C configFrom(A annotation, ControllerConfiguration parentConfiguration); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java index bbb4f75da9..2b361626aa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java @@ -1,5 +1,9 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; +import java.util.Optional; + public interface DependentResourceConfigurator { void configureWith(C config); + + Optional configuration(); } 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 070c8f8ffe..72cd25ac5b 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 @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; +import java.util.Arrays; import java.util.HashMap; +import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -10,19 +12,26 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; 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.ResourceDiscriminator; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource; 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.SecondaryToPrimaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @@ -31,7 +40,7 @@ public abstract class KubernetesDependentResource extends AbstractEventSourceHolderDependentResource> implements KubernetesClientAware, - DependentResourceConfigurator> { + AnnotationDependentResourceConfigurator> { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -39,7 +48,7 @@ public abstract class KubernetesDependentResource matcher; private final ResourceUpdatePreProcessor processor; private final boolean garbageCollected = this instanceof GarbageCollected; - private KubernetesDependentResourceConfig kubernetesDependentResourceConfig; + private KubernetesDependentResourceConfig kubernetesDependentResourceConfig; @SuppressWarnings("unchecked") public KubernetesDependentResource(Class resourceType) { @@ -239,4 +248,49 @@ private void cleanupAfterEventFiltering(ResourceID resourceID) { ((InformerEventSource) eventSource().orElseThrow()) .cleanupOnCreateOrUpdateEventFiltering(resourceID); } + + @Override + @SuppressWarnings("unchecked") + public KubernetesDependentResourceConfig configFrom(KubernetesDependent kubeDependent, + ControllerConfiguration parentConfiguration) { + var namespaces = parentConfiguration.getNamespaces(); + var configuredNS = false; + String labelSelector = null; + OnAddFilter onAddFilter = null; + OnUpdateFilter onUpdateFilter = null; + OnDeleteFilter onDeleteFilter = null; + GenericFilter genericFilter = null; + ResourceDiscriminator resourceDiscriminator = null; + if (kubeDependent != null) { + if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, kubeDependent.namespaces())) { + namespaces = Set.of(kubeDependent.namespaces()); + configuredNS = true; + } + + final var fromAnnotation = kubeDependent.labelSelector(); + labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation; + + final var context = + Utils.contextFor(parentConfiguration, getClass(), kubeDependent.annotationType()); + onAddFilter = Utils.instantiate(kubeDependent.onAddFilter(), OnAddFilter.class, context); + onUpdateFilter = + Utils.instantiate(kubeDependent.onUpdateFilter(), OnUpdateFilter.class, context); + onDeleteFilter = + Utils.instantiate(kubeDependent.onDeleteFilter(), OnDeleteFilter.class, context); + genericFilter = + Utils.instantiate(kubeDependent.genericFilter(), GenericFilter.class, context); + + resourceDiscriminator = + Utils.instantiate(kubeDependent.resourceDiscriminator(), ResourceDiscriminator.class, + context); + } + + return new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS, + resourceDiscriminator, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter); + } + + @Override + public Optional> configuration() { + return Optional.ofNullable(kubernetesDependentResourceConfig); + } } 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 5cab02d28c..02a3c8dd78 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 @@ -2,6 +2,7 @@ import java.util.Set; +import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -11,7 +12,7 @@ import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET; -public class KubernetesDependentResourceConfig { +public class KubernetesDependentResourceConfig implements NamespaceChangeable { private Set namespaces = Constants.SAME_AS_CONTROLLER_NAMESPACES_SET; private String labelSelector = NO_VALUE_SET; @@ -48,12 +49,6 @@ public KubernetesDependentResourceConfig(Set namespaces, String labelSel null, null); } - public KubernetesDependentResourceConfig setNamespaces(Set namespaces) { - this.namespacesWereConfigured = true; - this.namespaces = namespaces; - return this; - } - public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) { this.labelSelector = labelSelector; return this; @@ -94,4 +89,11 @@ public ResourceDiscriminator getResourceDiscriminator() { return resourceDiscriminator; } + @Override + public void changeNamespaces(Set namespaces) { + if (!wereNamespacesConfigured()) { + this.namespacesWereConfigured = true; + this.namespaces = namespaces; + } + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java index a6e6d27ce3..7a5a7521ba 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java @@ -12,11 +12,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.workflow.builder.WorkflowBuilder; @@ -73,11 +71,10 @@ public

Workflow

createWorkflow( return workflowBuilder.build(); } - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({"rawtypes"}) public DependentResource createAndConfigureFrom(DependentResourceSpec spec, KubernetesClient client) { - final var dependentResource = - ConfigurationServiceProvider.instance().dependentResourceFactory().createFrom(spec); + final var dependentResource = spec.getDependentResource(); if (dependentResource instanceof KubernetesClientAware) { ((KubernetesClientAware) dependentResource).setKubernetesClient(client); @@ -95,11 +92,6 @@ public DependentResource createAndConfigureFrom(DependentResourceSpec spec, + EventSourceReferencer.class.getSimpleName()); } }); - - if (dependentResource instanceof DependentResourceConfigurator) { - final var configurator = (DependentResourceConfigurator) dependentResource; - spec.getDependentResourceConfiguration().ifPresent(configurator::configureWith); - } return dependentResource; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java index 878118ba25..0ed9a9f4f6 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverriderTest.java @@ -9,10 +9,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,12 +21,6 @@ class ConfigurationServiceOverriderTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final LeaderElectionConfiguration LEADER_ELECTION_CONFIGURATION = new LeaderElectionConfiguration("foo", "fooNS"); - private static final DependentResourceFactory FACTORY = new DependentResourceFactory() { - @Override - public > T createFrom(DependentResourceSpec spec) { - return DependentResourceFactory.super.createFrom(spec); - } - }; private static final Cloner CLONER = new Cloner() { @Override @@ -86,11 +77,6 @@ public Cloner getResourceCloner() { return CLONER; } - @Override - public DependentResourceFactory dependentResourceFactory() { - return FACTORY; - } - @Override public Optional getLeaderElectionConfiguration() { return Optional.of(LEADER_ELECTION_CONFIGURATION); @@ -123,7 +109,6 @@ public R clone(R object) { overridden.concurrentReconciliationThreads()); assertNotEquals(config.getTerminationTimeoutSeconds(), overridden.getTerminationTimeoutSeconds()); - assertNotEquals(config.dependentResourceFactory(), overridden.dependentResourceFactory()); assertNotEquals(config.getClientConfiguration(), overridden.getClientConfiguration()); assertNotEquals(config.getExecutorService(), overridden.getExecutorService()); assertNotEquals(config.getMetrics(), overridden.getMetrics()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java index 4dd6938728..f9f75d6796 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; @@ -10,6 +11,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; @@ -69,7 +71,10 @@ public NamedDependentResource() { } } - private static class ExternalDependentResource implements DependentResource { + private static class ExternalDependentResource implements DependentResource, + DependentResourceConfigurator { + + private String config = "UNSET"; @Override public ReconcileResult reconcile(ConfigMap primary, Context context) { @@ -80,6 +85,16 @@ public ReconcileResult reconcile(ConfigMap primary, Context c public Class resourceType() { return Object.class; } + + @Override + public void configureWith(String config) { + this.config = config; + } + + @Override + public Optional configuration() { + return Optional.of(config); + } } } 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 cc4fe9bc48..11200f55d9 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 @@ -9,7 +9,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.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; @@ -97,8 +97,8 @@ void getsFirstTypeArgumentFromInterface() { () -> Utils.getFirstTypeArgumentFromInterface(TestKubernetesDependentResource.class, DependentResource.class)); - assertThat(Utils.getFirstTypeArgumentFromInterface(TestKubernetesDependentResource.class, - DependentResourceConfigurator.class)) + assertThat(Utils.getTypeArgumentFromInterfaceByIndex(TestKubernetesDependentResource.class, + AnnotationDependentResourceConfigurator.class, 1)) .isEqualTo(KubernetesDependentResourceConfig.class); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java index e3b97afb3c..85e4af05eb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java @@ -10,9 +10,8 @@ public class ManagedWorkflowTestUtils { @SuppressWarnings("unchecked") public static DependentResourceSpec createDRS(String name, String... dependOns) { - return new DependentResourceSpec(EmptyTestDependentResource.class, - null, name, Set.of(dependOns), null, null, null, - null); + return new DependentResourceSpec(new EmptyTestDependentResource(), name, Set.of(dependOns), + null, null, null, null); } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaConfig.java new file mode 100644 index 0000000000..c43a2a060f --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaConfig.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.dependent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface SchemaConfig { + int DEFAULT_POLL_PERIOD = 500; + int DEFAULT_PORT = 3306; + + int pollPeriod() default DEFAULT_POLL_PERIOD; + + String host(); + + String user(); + + String password(); + + int port() default DEFAULT_PORT; +} 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 c262822d21..8fb8855e41 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 @@ -3,15 +3,19 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -import java.util.*; +import java.util.Base64; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.MySQLDbConfig; @@ -23,12 +27,16 @@ import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; +@SchemaConfig(pollPeriod = 700, host = "127.0.0.1", + port = SchemaDependentResource.LOCAL_PORT, + user = "root", password = "password") // NOSONAR: password is only used locally, example only public class SchemaDependentResource extends PerResourcePollingDependentResource - implements DependentResourceConfigurator, + implements AnnotationDependentResourceConfigurator, Creator, Deleter { public static final String NAME = "schema"; + public static final int LOCAL_PORT = 3307; private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); private MySQLDbConfig dbConfig; @@ -37,12 +45,29 @@ public SchemaDependentResource() { super(Schema.class); } + @Override + public Optional configuration() { + return Optional.of(new ResourcePollerConfig((int) getPollingPeriod(), dbConfig)); + } + @Override public void configureWith(ResourcePollerConfig config) { this.dbConfig = config.getMySQLDbConfig(); setPollingPeriod(config.getPollPeriod()); } + @Override + public ResourcePollerConfig configFrom(SchemaConfig annotation, + ControllerConfiguration parentConfiguration) { + if (annotation != null) { + return new ResourcePollerConfig(annotation.pollPeriod(), + new MySQLDbConfig(annotation.host(), "" + annotation.port(), + annotation.user(), annotation.password())); + } + return new ResourcePollerConfig(SchemaConfig.DEFAULT_POLL_PERIOD, + MySQLDbConfig.loadFromEnvironmentVars()); + } + @Override public Schema desired(MySQLSchema primary, Context context) { return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); 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 93a0097b6c..24e257956b 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 @@ -13,12 +13,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.ClusterDeployedOperatorExtension; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; -import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import static java.util.concurrent.TimeUnit.MINUTES; @@ -33,13 +32,12 @@ class MySQLSchemaOperatorE2E { static final Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); - static final KubernetesClient client = new DefaultKubernetesClient(); + static final KubernetesClient client = new KubernetesClientBuilder().build(); static final String MY_SQL_NS = "mysql"; private final static List infrastructure = new ArrayList<>(); public static final String TEST_RESOURCE_NAME = "mydb1"; - public static final Integer LOCAL_PORT = 3307; static { infrastructure.add( @@ -63,15 +61,10 @@ boolean isLocal() { AbstractOperatorExtension operator = isLocal() ? LocallyRunOperatorExtension.builder() - .withReconciler( - new MySQLSchemaReconciler(), - c -> c.replacingNamedDependentResourceConfig( - SchemaDependentResource.NAME, - new ResourcePollerConfig( - 700, new MySQLDbConfig("127.0.0.1", LOCAL_PORT.toString(), "root", - "password")))) + .withReconciler(new MySQLSchemaReconciler()) // configuration for schema comes from + // SchemaDependentResource annotation .withInfrastructure(infrastructure) - .withPortForward(MY_SQL_NS, "app", "mysql", 3306, LOCAL_PORT) + .withPortForward(MY_SQL_NS, "app", "mysql", 3306, SchemaDependentResource.LOCAL_PORT) .build() : ClusterDeployedOperatorExtension.builder() .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get())