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); - } }