diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a615428790..e5b42dfbb9 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -56,6 +56,7 @@ io.fabric8 openshift-client + ${fabric8-client.version} @@ -98,6 +99,7 @@ io.fabric8 kubernetes-server-mock + ${fabric8-client.version} test 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 26dc4cdcca..95aa7066c0 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 @@ -45,7 +45,8 @@ public static String getResourceTypeName(Class resourceCl // return HasMetadata.getFullResourceName(resourceClass); final var group = HasMetadata.getGroup(resourceClass); final var plural = HasMetadata.getPlural(resourceClass); - return (group == null || group.isEmpty()) ? plural : plural + "." + group; + final var version = HasMetadata.getVersion(resourceClass); + return (group == null || group.isEmpty()) ? plural : plural + "." + group + "/" + version; } public static String getDefaultFinalizerName(Class resourceClass) { @@ -83,15 +84,16 @@ public static String getDefaultNameFor(Reconciler reconciler) { } public static String getDefaultNameFor(Class reconcilerClass) { - return getDefaultReconcilerName(reconcilerClass.getSimpleName()); + return getDefaultReconcilerName(reconcilerClass.getCanonicalName()); } public static String getDefaultReconcilerName(String reconcilerClassName) { - // if the name is fully qualified, extract the simple class name - final var lastDot = reconcilerClassName.lastIndexOf('.'); - if (lastDot > 0) { - reconcilerClassName = reconcilerClassName.substring(lastDot + 1); - } + // TODO: check why this logic was imlpemented in first place + // // if the name is fully qualified, extract the simple class name + // final var lastDot = reconcilerClassName.lastIndexOf('.'); + // if (lastDot > 0) { + // reconcilerClassName = reconcilerClassName.substring(lastDot + 1); + // } return reconcilerClassName.toLowerCase(Locale.ROOT); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 5e93b0d7af..065d6c8fd8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.Objects; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; @@ -10,6 +12,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; +import io.fabric8.kubernetes.internal.KubernetesDeserializer; import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; @@ -152,6 +156,13 @@ public MixedOperation, Resource> getCRClient() { return kubernetesClient.resources(configuration.getResourceClass()); } + public MixedOperation> getGenericClient() { + var context = ResourceDefinitionContext.fromResourceType(configuration.getResourceClass()); + KubernetesDeserializer.registerCustomKind(context.getVersion(), context.getKind(), + GenericKubernetesResource.class); + return kubernetesClient.genericKubernetesResources(context); + } + /** * Registers the specified controller with this operator, overriding its default configuration by * the specified one (usually created via diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 52374c5645..07cc6d1850 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -43,7 +43,7 @@ class ReconciliationDispatcher { } public ReconciliationDispatcher(Controller controller) { - this(controller, new CustomResourceFacade<>(controller.getCRClient())); + this(controller, new CustomResourceFacade(controller.getCRClient())); } public PostExecutionControl handleExecution(ExecutionScope executionScope) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java index 25c08d6af6..537a94ce90 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java @@ -2,11 +2,14 @@ import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; @@ -17,30 +20,37 @@ public class ControllerResourceCache implements ResourceCache { - private final Map> sharedIndexInformers; + private final Map> sharedIndexInformers; private final Cloner cloner; - public ControllerResourceCache(Map> sharedIndexInformers, + public ControllerResourceCache( + Map> sharedIndexInformers, Cloner cloner) { this.sharedIndexInformers = sharedIndexInformers; this.cloner = cloner; } + private Function CONVERT = + resource -> Serialization.unmarshal(Serialization.asJson(resource)); + @Override public Stream list(Predicate predicate) { return sharedIndexInformers.values().stream() - .flatMap(i -> i.getStore().list().stream().filter(predicate)); + .flatMap(i -> i.getStore().list().stream() + .map(CONVERT).filter(predicate)); } @Override public Stream list(String namespace, Predicate predicate) { if (isWatchingAllNamespaces()) { final var stream = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY).getStore().list().stream() - .filter(r -> r.getMetadata().getNamespace().equals(namespace)); + .map(CONVERT).filter(r -> r.getMetadata().getNamespace().equals(namespace)); return predicate != null ? stream.filter(predicate) : stream; } else { final var informer = sharedIndexInformers.get(namespace); - return informer != null ? informer.getStore().list().stream().filter(predicate) + return informer != null + ? informer.getStore().list().stream() + .map(CONVERT).filter(predicate) : Stream.empty(); } } @@ -59,7 +69,7 @@ public Optional get(ResourceID resourceID) { if (resource == null) { return Optional.empty(); } else { - return Optional.of(cloner.clone(resource)); + return Optional.of(cloner.clone(resource)).map(CONVERT); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 9feabf40bf..1138c53f5d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -9,12 +9,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -29,19 +31,20 @@ public class ControllerResourceEventSource extends AbstractResourceEventSource - implements ResourceEventHandler { + implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); private final Controller controller; - private final Map> sharedIndexInformers = + private final Map> sharedIndexInformers = new ConcurrentHashMap<>(); private final ResourceEventFilter filter; private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; private final ControllerResourceCache cache; + private final String CRVersion; public ControllerResourceEventSource(Controller controller) { super(controller.getConfiguration().getResourceClass()); @@ -49,7 +52,7 @@ public ControllerResourceEventSource(Controller controller) { final var configurationService = controller.getConfiguration().getConfigurationService(); var cloner = configurationService != null ? configurationService.getResourceCloner() : ConfigurationService.DEFAULT_CLONER; - this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); + this.cache = new ControllerResourceCache(sharedIndexInformers, cloner); var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), @@ -69,13 +72,18 @@ public ControllerResourceEventSource(Controller controller) { } else { filter = ResourceEventFilters.or(filters); } + + var resourceClass = controller.getConfiguration().getResourceClass(); + // TODO: check if we should use: HasMetadata.getFullResourceName(resourceClass); + this.CRVersion = + HasMetadata.getGroup(resourceClass) + "/" + HasMetadata.getVersion(resourceClass); } @Override public void start() { final var configuration = controller.getConfiguration(); final var targetNamespaces = configuration.getEffectiveNamespaces(); - final var client = controller.getCRClient(); + final var client = controller.getGenericClient(); final var labelSelector = configuration.getLabelSelector(); try { @@ -100,8 +108,9 @@ public void start() { super.start(); } - private SharedIndexInformer createAndRunInformerFor( - FilterWatchListDeletable> filteredBySelectorClient, String key) { + private SharedIndexInformer createAndRunInformerFor( + FilterWatchListDeletable filteredBySelectorClient, + String key) { var informer = filteredBySelectorClient.runnableInformer(0); informer.addEventHandler(this); sharedIndexInformers.put(key, informer); @@ -111,7 +120,7 @@ private SharedIndexInformer createAndRunInformerFor( @Override public void stop() { - for (SharedIndexInformer informer : sharedIndexInformers.values()) { + for (SharedIndexInformer informer : sharedIndexInformers.values()) { try { log.info("Stopping informer {} -> {}", controller, informer); informer.stop(); @@ -143,19 +152,49 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource } } + private String extractUnderlyingCRVersion(GenericKubernetesResource resource) { + return resource + .getMetadata() + .getManagedFields() + .get(0) + .getApiVersion(); + } + @Override - public void onAdd(T resource) { - eventReceived(ResourceAction.ADDED, resource, null); + public void onAdd(GenericKubernetesResource genericKubernetesResource) { + if (CRVersion.equals(extractUnderlyingCRVersion(genericKubernetesResource))) { + var resource = Serialization.unmarshal( + Serialization.asJson(genericKubernetesResource), this.getResourceClass()); + eventReceived(ResourceAction.ADDED, resource, null); + } } @Override - public void onUpdate(T oldCustomResource, T newCustomResource) { - eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); + public void onUpdate(GenericKubernetesResource oldResource, + GenericKubernetesResource newResource) { + if (CRVersion.equals(extractUnderlyingCRVersion(newResource))) { + var newCustomResource = Serialization.unmarshal( + Serialization.asJson(newResource), this.getResourceClass()); + + // Best effort try to deserialize the old CR with the same deserializer as the new + T oldCustomResource = null; + try { + oldCustomResource = Serialization.unmarshal( + Serialization.asJson(newResource), this.getResourceClass()); + } catch (Exception e) { + // ignored + } + eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); + } } @Override - public void onDelete(T resource, boolean b) { - eventReceived(ResourceAction.DELETED, resource, null); + public void onDelete(GenericKubernetesResource genericKubernetesResource, boolean b) { + if (CRVersion.equals(extractUnderlyingCRVersion(genericKubernetesResource))) { + var resource = Serialization.unmarshal( + Serialization.asJson(genericKubernetesResource), this.getResourceClass()); + eventReceived(ResourceAction.DELETED, resource, null); + } } public Optional get(ResourceID resourceID) { @@ -170,11 +209,11 @@ public ControllerResourceCache getResourceCache() { * @return shared informers by namespace. If custom resource is not namespace scoped use * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY */ - public Map> getInformers() { + public Map> getInformers() { return Collections.unmodifiableMap(sharedIndexInformers); } - public SharedIndexInformer getInformer(String namespace) { + public SharedIndexInformer getInformer(String namespace) { return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); } @@ -206,4 +245,5 @@ private void handleKubernetesClientException(Exception e) { public Optional getAssociated(T primary) { return cache.get(ResourceID.fromResource(primary)); } + } diff --git a/pom.xml b/pom.xml index 22bc63c36b..7ce7522983 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ https://sonarcloud.io 5.8.2 - 5.11.2 + 6.0-SNAPSHOT 1.7.33 2.17.1 4.2.0