Skip to content

Commit 58bd66c

Browse files
committed
feat: generic kubernetes resource usage (#2136)
Signed-off-by: Attila Mészáros <csviri@gmail.com>
1 parent 1d7370c commit 58bd66c

File tree

26 files changed

+632
-20
lines changed

26 files changed

+632
-20
lines changed

docs/documentation/dependent-resources.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,16 @@ also be created, one per dependent resource.
483483
See [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java)
484484
as a sample.
485485

486+
487+
## GenericKubernetesResource based Dependent Resources
488+
489+
In rare circumstances resource handling where there is no class representation or just typeless handling might be needed.
490+
Fabric8 Client provides [GenericKubernetesResource](https://github.com/fabric8io/kubernetes-client/blob/main/doc/CHEATSHEET.md#resource-typeless-api)
491+
to support that.
492+
493+
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)
494+
. See samples [here](https://github.com/java-operator-sdk/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource).
495+
486496
## Other Dependent Resource Features
487497

488498
### 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)

micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,9 @@ private static String getScope(ResourceID resourceID) {
291291
}
292292

293293
private static void addGVKTags(GroupVersionKind gvk, List<Tag> tags, boolean prefixed) {
294-
addTagOmittingOnEmptyValue(GROUP, gvk.group, tags, prefixed);
295-
addTag(VERSION, gvk.version, tags, prefixed);
296-
addTag(KIND, gvk.kind, tags, prefixed);
294+
addTagOmittingOnEmptyValue(GROUP, gvk.getGroup(), tags, prefixed);
295+
addTag(VERSION, gvk.getVersion(), tags, prefixed);
296+
addTag(KIND, gvk.getKind(), tags, prefixed);
297297
}
298298

299299
private void incrementCounter(ResourceID id, String counterName, Map<String, Object> metadata,

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Optional;
44
import java.util.Set;
55

6+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
67
import io.fabric8.kubernetes.api.model.HasMetadata;
78
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
89
import io.javaoperatorsdk.operator.ReconcilerUtils;
@@ -28,7 +29,11 @@ protected DefaultResourceConfiguration(Class<R> resourceClass,
2829
OnUpdateFilter<? super R> onUpdateFilter, GenericFilter<? super R> genericFilter,
2930
ItemStore<R> itemStore, Long informerListLimit) {
3031
this.resourceClass = resourceClass;
31-
this.resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass);
32+
this.resourceTypeName = resourceClass.isAssignableFrom(GenericKubernetesResource.class)
33+
// in general this is irrelevant now for secondary resources it is used just by controller
34+
// where GenericKubernetesResource now does not apply
35+
? GenericKubernetesResource.class.getSimpleName()
36+
: ReconcilerUtils.getResourceTypeName(resourceClass);
3237
this.onAddFilter = onAddFilter;
3338
this.onUpdateFilter = onUpdateFilter;
3439
this.genericFilter = genericFilter;

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public static <T> void executeAndWaitForAllToComplete(Stream<T> stream,
7878
// to find out any exceptions
7979
f.get();
8080
} catch (ExecutionException e) {
81-
throw new OperatorException(e.getCause());
81+
throw new OperatorException(e);
8282
} catch (InterruptedException e) {
8383
log.warn("Interrupted.", e);
8484
Thread.currentThread().interrupt();

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import java.util.Optional;
55
import java.util.Set;
66

7+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
78
import io.fabric8.kubernetes.api.model.HasMetadata;
89
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
910
import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration;
1011
import io.javaoperatorsdk.operator.api.config.ResourceConfiguration;
1112
import io.javaoperatorsdk.operator.api.config.Utils;
1213
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
14+
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
1315
import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper;
1416
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
1517
import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
@@ -30,9 +32,11 @@ class DefaultInformerConfiguration<R extends HasMetadata> extends
3032
private final SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
3133
private final boolean followControllerNamespaceChanges;
3234
private final OnDeleteFilter<? super R> onDeleteFilter;
35+
private final GroupVersionKind groupVersionKind;
3336

3437
protected DefaultInformerConfiguration(String labelSelector,
3538
Class<R> resourceClass,
39+
GroupVersionKind groupVersionKind,
3640
PrimaryToSecondaryMapper<?> primaryToSecondaryMapper,
3741
SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper,
3842
Set<String> namespaces, boolean followControllerNamespaceChanges,
@@ -44,7 +48,7 @@ protected DefaultInformerConfiguration(String labelSelector,
4448
super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter,
4549
itemStore, informerListLimit);
4650
this.followControllerNamespaceChanges = followControllerNamespaceChanges;
47-
51+
this.groupVersionKind = groupVersionKind;
4852
this.primaryToSecondaryMapper = primaryToSecondaryMapper;
4953
this.secondaryToPrimaryMapper =
5054
Objects.requireNonNullElse(secondaryToPrimaryMapper,
@@ -72,6 +76,10 @@ public Optional<OnDeleteFilter<? super R>> onDeleteFilter() {
7276
public <P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondaryMapper() {
7377
return (PrimaryToSecondaryMapper<P>) primaryToSecondaryMapper;
7478
}
79+
80+
public Optional<GroupVersionKind> getGroupVersionKind() {
81+
return Optional.ofNullable(groupVersionKind);
82+
}
7583
}
7684

7785
/**
@@ -109,14 +117,17 @@ public <P extends HasMetadata> PrimaryToSecondaryMapper<P> getPrimaryToSecondary
109117

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

120+
Optional<GroupVersionKind> getGroupVersionKind();
121+
112122
@SuppressWarnings("unused")
113123
class InformerConfigurationBuilder<R extends HasMetadata> {
114124

125+
private final Class<R> resourceClass;
126+
private final GroupVersionKind groupVersionKind;
115127
private PrimaryToSecondaryMapper<?> primaryToSecondaryMapper;
116128
private SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
117129
private Set<String> namespaces;
118130
private String labelSelector;
119-
private final Class<R> resourceClass;
120131
private OnAddFilter<? super R> onAddFilter;
121132
private OnUpdateFilter<? super R> onUpdateFilter;
122133
private OnDeleteFilter<? super R> onDeleteFilter;
@@ -127,6 +138,13 @@ class InformerConfigurationBuilder<R extends HasMetadata> {
127138

128139
private InformerConfigurationBuilder(Class<R> resourceClass) {
129140
this.resourceClass = resourceClass;
141+
this.groupVersionKind = null;
142+
}
143+
144+
@SuppressWarnings("unchecked")
145+
private InformerConfigurationBuilder(GroupVersionKind groupVersionKind) {
146+
this.resourceClass = (Class<R>) GenericKubernetesResource.class;
147+
this.groupVersionKind = groupVersionKind;
130148
}
131149

132150
public <P extends HasMetadata> InformerConfigurationBuilder<R> withPrimaryToSecondaryMapper(
@@ -244,7 +262,7 @@ public InformerConfigurationBuilder<R> withInformerListLimit(Long informerListLi
244262
}
245263

246264
public InformerConfiguration<R> build() {
247-
return new DefaultInformerConfiguration<>(labelSelector, resourceClass,
265+
return new DefaultInformerConfiguration<>(labelSelector, resourceClass, groupVersionKind,
248266
primaryToSecondaryMapper,
249267
secondaryToPrimaryMapper,
250268
namespaces, inheritControllerNamespacesOnChange, onAddFilter, onUpdateFilter,
@@ -257,6 +275,14 @@ static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
257275
return new InformerConfigurationBuilder<>(resourceClass);
258276
}
259277

278+
/**
279+
* * For the case when want to use {@link GenericKubernetesResource}
280+
*/
281+
static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
282+
GroupVersionKind groupVersionKind) {
283+
return new InformerConfigurationBuilder<>(groupVersionKind);
284+
}
285+
260286
/**
261287
* Creates a configuration builder that inherits namespaces from the controller and follows
262288
* namespaces changes.
@@ -272,6 +298,16 @@ static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
272298
.withNamespacesInheritedFromController(eventSourceContext);
273299
}
274300

301+
/**
302+
* * For the case when want to use {@link GenericKubernetesResource}
303+
*/
304+
@SuppressWarnings("unchecked")
305+
static InformerConfigurationBuilder<GenericKubernetesResource> from(
306+
GroupVersionKind groupVersionKind, EventSourceContext<?> eventSourceContext) {
307+
return new InformerConfigurationBuilder<GenericKubernetesResource>(groupVersionKind)
308+
.withNamespacesInheritedFromController(eventSourceContext);
309+
}
310+
275311
@SuppressWarnings("unchecked")
276312
@Override
277313
default Class<R> getResourceClass() {
Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
package io.javaoperatorsdk.operator.processing;
22

3+
import java.util.Objects;
4+
35
import io.fabric8.kubernetes.api.model.HasMetadata;
46

57
public class GroupVersionKind {
6-
public final String group;
7-
public final String version;
8-
public final String kind;
8+
private final String group;
9+
private final String version;
10+
private final String kind;
911

10-
GroupVersionKind(String group, String version, String kind) {
12+
public GroupVersionKind(String apiVersion, String kind) {
13+
this.kind = kind;
14+
String[] groupAndVersion = apiVersion.split("/");
15+
if (groupAndVersion.length == 1) {
16+
this.group = null;
17+
this.version = groupAndVersion[0];
18+
} else {
19+
this.group = groupAndVersion[0];
20+
this.version = groupAndVersion[1];
21+
}
22+
}
23+
24+
public GroupVersionKind(String group, String version, String kind) {
1125
this.group = group;
1226
this.version = version;
1327
this.kind = kind;
@@ -17,4 +31,45 @@ public static GroupVersionKind gvkFor(Class<? extends HasMetadata> resourceClass
1731
return new GroupVersionKind(HasMetadata.getGroup(resourceClass),
1832
HasMetadata.getVersion(resourceClass), HasMetadata.getKind(resourceClass));
1933
}
34+
35+
public String getGroup() {
36+
return group;
37+
}
38+
39+
public String getVersion() {
40+
return version;
41+
}
42+
43+
public String getKind() {
44+
return kind;
45+
}
46+
47+
public String apiVersion() {
48+
return group == null || group.isBlank() ? version : group + "/" + version;
49+
}
50+
51+
@Override
52+
public boolean equals(Object o) {
53+
if (this == o)
54+
return true;
55+
if (o == null || getClass() != o.getClass())
56+
return false;
57+
GroupVersionKind that = (GroupVersionKind) o;
58+
return Objects.equals(group, that.group) && Objects.equals(version, that.version)
59+
&& Objects.equals(kind, that.kind);
60+
}
61+
62+
@Override
63+
public int hashCode() {
64+
return Objects.hash(group, version, kind);
65+
}
66+
67+
@Override
68+
public String toString() {
69+
return "GroupVersionKind{" +
70+
"group='" + group + '\'' +
71+
", version='" + version + '\'' +
72+
", kind='" + kind + '\'' +
73+
'}';
74+
}
2075
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
2+
3+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
4+
import io.fabric8.kubernetes.api.model.HasMetadata;
5+
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
6+
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
7+
8+
public class GenericKubernetesDependentResource<P extends HasMetadata>
9+
extends KubernetesDependentResource<GenericKubernetesResource, P> {
10+
11+
private GroupVersionKind groupVersionKind;
12+
13+
public GenericKubernetesDependentResource(GroupVersionKind groupVersionKind) {
14+
super(GenericKubernetesResource.class);
15+
this.groupVersionKind = groupVersionKind;
16+
}
17+
18+
protected InformerConfiguration.InformerConfigurationBuilder<GenericKubernetesResource> informerConfigurationBuilder() {
19+
return InformerConfiguration.from(groupVersionKind);
20+
}
21+
22+
public GroupVersionKind getGroupVersionKind() {
23+
return groupVersionKind;
24+
}
25+
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,18 @@ private void configureWith(String labelSelector, Set<String> namespaces,
6767
namespaces = context.getControllerConfiguration().getNamespaces();
6868
}
6969

70-
var ic = InformerConfiguration.from(resourceType())
70+
var ic = informerConfigurationBuilder()
7171
.withLabelSelector(labelSelector)
7272
.withSecondaryToPrimaryMapper(getSecondaryToPrimaryMapper())
7373
.withNamespaces(namespaces, inheritNamespacesOnChange)
7474
.build();
7575

7676
configureWith(new InformerEventSource<>(ic, context));
77+
}
7778

79+
// just to seamlessly handle GenericKubernetesDependentResource
80+
protected InformerConfiguration.InformerConfigurationBuilder<R> informerConfigurationBuilder() {
81+
return InformerConfiguration.from(resourceType());
7882
}
7983

8084
@SuppressWarnings("unchecked")

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

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

8990
public InformerEventSource(InformerConfiguration<R> configuration, KubernetesClient client,
9091
boolean parseResourceVersions) {
91-
super(client.resources(configuration.getResourceClass()), configuration, parseResourceVersions);
92+
super(
93+
configuration.getGroupVersionKind()
94+
.map(gvk -> client.genericKubernetesResources(gvk.apiVersion(), gvk.getKind()))
95+
.orElseGet(() -> (MixedOperation) client.resources(configuration.getResourceClass())),
96+
configuration, parseResourceVersions);
9297
// If there is a primary to secondary mapper there is no need for primary to secondary index.
9398
primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper();
9499
if (primaryToSecondaryMapper == null) {

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.slf4j.Logger;
1414
import org.slf4j.LoggerFactory;
1515

16+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
1617
import io.fabric8.kubernetes.api.model.HasMetadata;
1718
import io.fabric8.kubernetes.client.informers.ExceptionHandler;
1819
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
@@ -113,6 +114,9 @@ public void start() throws OperatorException {
113114

114115
private String versionedFullResourceName() {
115116
final var apiTypeClass = informer.getApiTypeClass();
117+
if (apiTypeClass.isAssignableFrom(GenericKubernetesResource.class)) {
118+
return GenericKubernetesResource.class.getSimpleName();
119+
}
116120
return ReconcilerUtils.getResourceTypeNameWithVersion(apiTypeClass);
117121
}
118122

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
import org.slf4j.LoggerFactory;
1010

1111
import io.fabric8.kubernetes.api.model.HasMetadata;
12-
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
1312
import io.fabric8.kubernetes.client.dsl.MixedOperation;
14-
import io.fabric8.kubernetes.client.dsl.Resource;
1513
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
1614
import io.javaoperatorsdk.operator.OperatorException;
1715
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
@@ -35,15 +33,15 @@ public abstract class ManagedInformerEventSource<R extends HasMetadata, P extend
3533

3634
private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class);
3735
private InformerManager<R, C> cache;
38-
private boolean parseResourceVersions;
36+
private final boolean parseResourceVersions;
3937
private ConfigurationService configurationService;
40-
private C configuration;
38+
private final C configuration;
4139
private Map<String, Function<R, List<String>>> indexers = new HashMap<>();
4240
protected TemporaryResourceCache<R> temporaryResourceCache;
43-
protected MixedOperation<R, KubernetesResourceList<R>, Resource<R>> client;
41+
protected MixedOperation client;
4442

4543
protected ManagedInformerEventSource(
46-
MixedOperation<R, KubernetesResourceList<R>, Resource<R>> client, C configuration,
44+
MixedOperation client, C configuration,
4745
boolean parseResourceVersions) {
4846
super(configuration.getResourceClass());
4947
this.parseResourceVersions = parseResourceVersions;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.javaoperatorsdk.operator.processing;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
import static org.junit.jupiter.api.Assertions.*;
7+
8+
class GroupVersionKindTest {
9+
10+
@Test
11+
void testInitFromApiVersion() {
12+
var gvk = new GroupVersionKind("v1", "ConfigMap");
13+
assertThat(gvk.getGroup()).isNull();
14+
assertThat(gvk.getVersion()).isEqualTo("v1");
15+
16+
gvk = new GroupVersionKind("apps/v1", "Deployment");
17+
assertThat(gvk.getGroup()).isEqualTo("apps");
18+
assertThat(gvk.getVersion()).isEqualTo("v1");
19+
}
20+
21+
}

0 commit comments

Comments
 (0)