Skip to content

feat: generic kubernetes resource usage #2136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Dec 13, 2023
Merged
10 changes: 10 additions & 0 deletions docs/documentation/dependent-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,16 @@ also be created, one per dependent resource.
See [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java)
as a sample.


## GenericKubernetesResource based Dependent Resources

In rare circumstances resource handling where there is no class representation or just typeless handling might be needed.
Fabric8 Client provides [GenericKubernetesResource](https://github.com/fabric8io/kubernetes-client/blob/main/doc/CHEATSHEET.md#resource-typeless-api)
to support that.

For dependent resource this is supported by [GenericKubernetesDependentResource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java#L8-L8)
. See samples [here](https://github.com/java-operator-sdk/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource).

## Other Dependent Resource Features

### Caching and Event Handling in [KubernetesDependentResource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,9 @@ private static String getScope(ResourceID resourceID) {
}

private static void addGVKTags(GroupVersionKind gvk, List<Tag> tags, boolean prefixed) {
addTagOmittingOnEmptyValue(GROUP, gvk.group, tags, prefixed);
addTag(VERSION, gvk.version, tags, prefixed);
addTag(KIND, gvk.kind, tags, prefixed);
addTagOmittingOnEmptyValue(GROUP, gvk.getGroup(), tags, prefixed);
addTag(VERSION, gvk.getVersion(), tags, prefixed);
addTag(KIND, gvk.getKind(), tags, prefixed);
}

private void incrementCounter(ResourceID id, String counterName, Map<String, Object> metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Optional;
import java.util.Set;

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
import io.javaoperatorsdk.operator.ReconcilerUtils;
Expand All @@ -28,7 +29,11 @@ protected DefaultResourceConfiguration(Class<R> resourceClass,
OnUpdateFilter<? super R> onUpdateFilter, GenericFilter<? super R> genericFilter,
ItemStore<R> itemStore, Long informerListLimit) {
this.resourceClass = resourceClass;
this.resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass);
this.resourceTypeName = resourceClass.isAssignableFrom(GenericKubernetesResource.class)
// in general this is irrelevant now for secondary resources it is used just by controller
// where GenericKubernetesResource now does not apply
? GenericKubernetesResource.class.getSimpleName()
: ReconcilerUtils.getResourceTypeName(resourceClass);
this.onAddFilter = onAddFilter;
this.onUpdateFilter = onUpdateFilter;
this.genericFilter = genericFilter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static <T> void executeAndWaitForAllToComplete(Stream<T> stream,
// to find out any exceptions
f.get();
} catch (ExecutionException e) {
throw new OperatorException(e.getCause());
throw new OperatorException(e);
} catch (InterruptedException e) {
log.warn("Interrupted.", e);
Thread.currentThread().interrupt();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import java.util.Optional;
import java.util.Set;

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration;
import io.javaoperatorsdk.operator.api.config.ResourceConfiguration;
import io.javaoperatorsdk.operator.api.config.Utils;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
Expand All @@ -30,9 +32,11 @@ class DefaultInformerConfiguration<R extends HasMetadata> extends
private final SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
private final boolean followControllerNamespaceChanges;
private final OnDeleteFilter<? super R> onDeleteFilter;
private final GroupVersionKind groupVersionKind;

protected DefaultInformerConfiguration(String labelSelector,
Class<R> resourceClass,
GroupVersionKind groupVersionKind,
PrimaryToSecondaryMapper<?> primaryToSecondaryMapper,
SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper,
Set<String> namespaces, boolean followControllerNamespaceChanges,
Expand All @@ -44,7 +48,7 @@ protected DefaultInformerConfiguration(String labelSelector,
super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter,
itemStore, informerListLimit);
this.followControllerNamespaceChanges = followControllerNamespaceChanges;

this.groupVersionKind = groupVersionKind;
this.primaryToSecondaryMapper = primaryToSecondaryMapper;
this.secondaryToPrimaryMapper =
Objects.requireNonNullElse(secondaryToPrimaryMapper,
Expand Down Expand Up @@ -72,6 +76,10 @@ public Optional<OnDeleteFilter<? super R>> onDeleteFilter() {
public <P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondaryMapper() {
return (PrimaryToSecondaryMapper<P>) primaryToSecondaryMapper;
}

public Optional<GroupVersionKind> getGroupVersionKind() {
return Optional.ofNullable(groupVersionKind);
}
}

/**
Expand Down Expand Up @@ -109,14 +117,17 @@ public <P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondary

<P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondaryMapper();

Optional<GroupVersionKind> getGroupVersionKind();

@SuppressWarnings("unused")
class InformerConfigurationBuilder<R extends HasMetadata> {

private final Class<R> resourceClass;
private final GroupVersionKind groupVersionKind;
private PrimaryToSecondaryMapper<?> primaryToSecondaryMapper;
private SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
private Set<String> namespaces;
private String labelSelector;
private final Class<R> resourceClass;
private OnAddFilter<? super R> onAddFilter;
private OnUpdateFilter<? super R> onUpdateFilter;
private OnDeleteFilter<? super R> onDeleteFilter;
Expand All @@ -127,6 +138,13 @@ class InformerConfigurationBuilder<R extends HasMetadata> {

private InformerConfigurationBuilder(Class<R> resourceClass) {
this.resourceClass = resourceClass;
this.groupVersionKind = null;
}

@SuppressWarnings("unchecked")
private InformerConfigurationBuilder(GroupVersionKind groupVersionKind) {
this.resourceClass = (Class<R>) GenericKubernetesResource.class;
this.groupVersionKind = groupVersionKind;
}

public <P extends HasMetadata> InformerConfigurationBuilder<R> withPrimaryToSecondaryMapper(
Expand Down Expand Up @@ -244,7 +262,7 @@ public InformerConfigurationBuilder<R> withInformerListLimit(Long informerListLi
}

public InformerConfiguration<R> build() {
return new DefaultInformerConfiguration<>(labelSelector, resourceClass,
return new DefaultInformerConfiguration<>(labelSelector, resourceClass, groupVersionKind,
primaryToSecondaryMapper,
secondaryToPrimaryMapper,
namespaces, inheritControllerNamespacesOnChange, onAddFilter, onUpdateFilter,
Expand All @@ -257,6 +275,14 @@ static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
return new InformerConfigurationBuilder<>(resourceClass);
}

/**
* * For the case when want to use {@link GenericKubernetesResource}
*/
static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
GroupVersionKind groupVersionKind) {
return new InformerConfigurationBuilder<>(groupVersionKind);
}

/**
* Creates a configuration builder that inherits namespaces from the controller and follows
* namespaces changes.
Expand All @@ -272,6 +298,16 @@ static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
.withNamespacesInheritedFromController(eventSourceContext);
}

/**
* * For the case when want to use {@link GenericKubernetesResource}
*/
@SuppressWarnings("unchecked")
static InformerConfigurationBuilder<GenericKubernetesResource> from(
GroupVersionKind groupVersionKind, EventSourceContext<?> eventSourceContext) {
return new InformerConfigurationBuilder<GenericKubernetesResource>(groupVersionKind)
.withNamespacesInheritedFromController(eventSourceContext);
}

@SuppressWarnings("unchecked")
@Override
default Class<R> getResourceClass() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
package io.javaoperatorsdk.operator.processing;

import java.util.Objects;

import io.fabric8.kubernetes.api.model.HasMetadata;

public class GroupVersionKind {
public final String group;
public final String version;
public final String kind;
private final String group;
private final String version;
private final String kind;

GroupVersionKind(String group, String version, String kind) {
public GroupVersionKind(String apiVersion, String kind) {
this.kind = kind;
String[] groupAndVersion = apiVersion.split("/");
if (groupAndVersion.length == 1) {
this.group = null;
this.version = groupAndVersion[0];
} else {
this.group = groupAndVersion[0];
this.version = groupAndVersion[1];
}
}

public GroupVersionKind(String group, String version, String kind) {
this.group = group;
this.version = version;
this.kind = kind;
Expand All @@ -17,4 +31,45 @@ public static GroupVersionKind gvkFor(Class<? extends HasMetadata> resourceClass
return new GroupVersionKind(HasMetadata.getGroup(resourceClass),
HasMetadata.getVersion(resourceClass), HasMetadata.getKind(resourceClass));
}

public String getGroup() {
return group;
}

public String getVersion() {
return version;
}

public String getKind() {
return kind;
}

public String apiVersion() {
return group == null || group.isBlank() ? version : group + "/" + version;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
GroupVersionKind that = (GroupVersionKind) o;
return Objects.equals(group, that.group) && Objects.equals(version, that.version)
&& Objects.equals(kind, that.kind);
}

@Override
public int hashCode() {
return Objects.hash(group, version, kind);
}

@Override
public String toString() {
return "GroupVersionKind{" +
"group='" + group + '\'' +
", version='" + version + '\'' +
", kind='" + kind + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.processing.GroupVersionKind;

public class GenericKubernetesDependentResource<P extends HasMetadata>
extends KubernetesDependentResource<GenericKubernetesResource, P> {

private GroupVersionKind groupVersionKind;

public GenericKubernetesDependentResource(GroupVersionKind groupVersionKind) {
super(GenericKubernetesResource.class);
this.groupVersionKind = groupVersionKind;
}

protected InformerConfiguration.InformerConfigurationBuilder<GenericKubernetesResource> informerConfigurationBuilder() {
return InformerConfiguration.from(groupVersionKind);
}

public GroupVersionKind getGroupVersionKind() {
return groupVersionKind;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,18 @@ private void configureWith(String labelSelector, Set<String> namespaces,
namespaces = context.getControllerConfiguration().getNamespaces();
}

var ic = InformerConfiguration.from(resourceType())
var ic = informerConfigurationBuilder()
.withLabelSelector(labelSelector)
.withSecondaryToPrimaryMapper(getSecondaryToPrimaryMapper())
.withNamespaces(namespaces, inheritNamespacesOnChange)
.build();

configureWith(new InformerEventSource<>(ic, context));
}

// just to seamlessly handle GenericKubernetesDependentResource
protected InformerConfiguration.InformerConfigurationBuilder<R> informerConfigurationBuilder() {
return InformerConfiguration.from(resourceType());
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
Expand Down Expand Up @@ -88,7 +89,11 @@ public InformerEventSource(InformerConfiguration<R> configuration, KubernetesCli

public InformerEventSource(InformerConfiguration<R> configuration, KubernetesClient client,
boolean parseResourceVersions) {
super(client.resources(configuration.getResourceClass()), configuration, parseResourceVersions);
super(
configuration.getGroupVersionKind()
.map(gvk -> client.genericKubernetesResources(gvk.apiVersion(), gvk.getKind()))
.orElseGet(() -> (MixedOperation) client.resources(configuration.getResourceClass())),
configuration, parseResourceVersions);
// If there is a primary to secondary mapper there is no need for primary to secondary index.
primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper();
if (primaryToSecondaryMapper == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.informers.ExceptionHandler;
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
Expand Down Expand Up @@ -113,6 +114,9 @@ public void start() throws OperatorException {

private String versionedFullResourceName() {
final var apiTypeClass = informer.getApiTypeClass();
if (apiTypeClass.isAssignableFrom(GenericKubernetesResource.class)) {
return GenericKubernetesResource.class.getSimpleName();
}
return ReconcilerUtils.getResourceTypeNameWithVersion(apiTypeClass);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
import org.slf4j.LoggerFactory;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
import io.javaoperatorsdk.operator.OperatorException;
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
Expand All @@ -35,15 +33,15 @@ public abstract class ManagedInformerEventSource<R extends HasMetadata, P extend

private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class);
private InformerManager<R, C> cache;
private boolean parseResourceVersions;
private final boolean parseResourceVersions;
private ConfigurationService configurationService;
private C configuration;
private final C configuration;
private Map<String, Function<R, List<String>>> indexers = new HashMap<>();
protected TemporaryResourceCache<R> temporaryResourceCache;
protected MixedOperation<R, KubernetesResourceList<R>, Resource<R>> client;
protected MixedOperation client;

protected ManagedInformerEventSource(
MixedOperation<R, KubernetesResourceList<R>, Resource<R>> client, C configuration,
MixedOperation client, C configuration,
boolean parseResourceVersions) {
super(configuration.getResourceClass());
this.parseResourceVersions = parseResourceVersions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.javaoperatorsdk.operator.processing;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class GroupVersionKindTest {

@Test
void testInitFromApiVersion() {
var gvk = new GroupVersionKind("v1", "ConfigMap");
assertThat(gvk.getGroup()).isNull();
assertThat(gvk.getVersion()).isEqualTo("v1");

gvk = new GroupVersionKind("apps/v1", "Deployment");
assertThat(gvk.getGroup()).isEqualTo("apps");
assertThat(gvk.getVersion()).isEqualTo("v1");
}

}
Loading