Skip to content

Commit fb4a073

Browse files
csvirixstefank
andauthored
feat: id provider for external dependent resources (#2970)
Signed-off-by: Attila Mészáros <[email protected]> Co-authored-by: Martin Stefanko <[email protected]>
1 parent c258d7d commit fb4a073

File tree

10 files changed

+139
-41
lines changed

10 files changed

+139
-41
lines changed

docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,42 @@ as [integration tests](https://github.com/operator-framework/java-operator-sdk/t
355355
To see how bulk dependent resources interact with workflow conditions, please refer to this
356356
[integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/conidition).
357357

358+
## Dependent Resources with External Resource
359+
360+
Dependent resources are designed to manage also non-Kubernetes or external resources.
361+
To implement such dependent you can extend `AbstractExternalDependentResource` or one of its
362+
[subclasses](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external).
363+
364+
For Kubernetes resources we can have nice assumptions, like
365+
if there are multiple resources of the same type, we can select the target resource
366+
that dependent resource manages based on the name and namespace of the desired resource;
367+
or we can use a matcher based SSA in most of the cases if the resource is managed using SSA.
368+
369+
### Selecting the target resource
370+
371+
Unfortunately this is not true for external resources. So to make sure we are selecting
372+
the target resources from an event source, we provide a [mechanism](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java#L114-L138) that helps with that logic.
373+
Your POJO representing an external resource can implement [`ExternalResourceIDProvider`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java) :
374+
375+
```java
376+
377+
public interface ExternalDependentIDProvider<T> {
378+
379+
T externalResourceId();
380+
}
381+
```
382+
383+
That will provide an ID, what is used to check for equality for desired state and resources from event source caches.
384+
Not that if some reason this mechanism does not suit for you, you can simply
385+
override [`selectTargetSecondaryResource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java)
386+
method.
387+
388+
### Matching external resources
389+
390+
By default, external resources are matched using [equality](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java#L88-L92).
391+
So you can override equals of you POJO representing an external resource.
392+
As an alternative you can always override the whole `match` method to completely customize matching.
393+
358394
## External State Tracking Dependent Resources
359395

360396
It is sometimes necessary for a controller to track external (i.e. non-Kubernetes) state to

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,8 @@ public Optional<R> getSecondaryResource(P primary, Context<P> context) {
149149
* @throws IllegalStateException if more than one candidate is found, in which case some other
150150
* mechanism might be necessary to distinguish between candidate secondary resources
151151
*/
152-
protected Optional<R> selectTargetSecondaryResource(
153-
Set<R> secondaryResources, P primary, Context<P> context) {
154-
R desired = desired(primary, context);
155-
var targetResources = secondaryResources.stream().filter(r -> r.equals(desired)).toList();
156-
if (targetResources.size() > 1) {
157-
throw new IllegalStateException(
158-
"More than one secondary resource related to primary: " + targetResources);
159-
}
160-
return targetResources.isEmpty() ? Optional.empty() : Optional.of(targetResources.get(0));
161-
}
152+
protected abstract Optional<R> selectTargetSecondaryResource(
153+
Set<R> secondaryResources, P primary, Context<P> context);
162154

163155
private void throwIfNull(R desired, P primary, String descriptor) {
164156
if (desired == null) {

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.dependent;
1717

18+
import java.util.List;
19+
import java.util.Optional;
20+
import java.util.Set;
21+
1822
import io.fabric8.kubernetes.api.model.HasMetadata;
1923
import io.javaoperatorsdk.operator.api.reconciler.Context;
2024
import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller;
@@ -121,4 +125,30 @@ public void handleDeleteTargetResource(P primary, R resource, String key, Contex
121125
protected InformerEventSource getExternalStateEventSource() {
122126
return externalStateEventSource;
123127
}
128+
129+
@Override
130+
protected Optional<R> selectTargetSecondaryResource(
131+
Set<R> secondaryResources, P primary, Context<P> context) {
132+
R desired = desired(primary, context);
133+
List<R> targetResources;
134+
if (desired instanceof ExternalDependentIDProvider<?> desiredWithId) {
135+
targetResources =
136+
secondaryResources.stream()
137+
.filter(
138+
r ->
139+
((ExternalDependentIDProvider<?>) r)
140+
.externalResourceId()
141+
.equals(desiredWithId.externalResourceId()))
142+
.toList();
143+
} else {
144+
throw new IllegalStateException(
145+
"Either implement ExternalDependentIDProvider or override this "
146+
+ " (selectTargetSecondaryResource) method.");
147+
}
148+
if (targetResources.size() > 1) {
149+
throw new IllegalStateException(
150+
"More than one secondary resource related to primary: " + targetResources);
151+
}
152+
return targetResources.isEmpty() ? Optional.empty() : Optional.of(targetResources.get(0));
153+
}
124154
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collections;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.Optional;
2223
import java.util.Set;
2324

2425
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -120,6 +121,13 @@ public Result<R> match(R resource, P primary, Context<P> context) {
120121
return bulkDependentResource.match(resource, desired, primary, context);
121122
}
122123

124+
@Override
125+
protected Optional<R> selectTargetSecondaryResource(
126+
Set<R> secondaryResources, P primary, Context<P> context) {
127+
throw new IllegalStateException(
128+
"BulkDependentResource should not call selectTargetSecondaryResource.");
129+
}
130+
123131
@Override
124132
protected void onCreated(P primary, R created, Context<P> context) {
125133
asAbstractDependentResource().onCreated(primary, created, context);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.processing.dependent;
17+
18+
/**
19+
* Provides the identifier for an object that represents an external resource. This ID is used to
20+
* select target resource for a dependent resource from the resources returned by `{@link
21+
* io.javaoperatorsdk.operator.api.reconciler.Context#getSecondaryResources(Class)}`.
22+
*
23+
* @param <T>
24+
*/
25+
public interface ExternalDependentIDProvider<T> {
26+
27+
T externalResourceId();
28+
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import io.fabric8.kubernetes.api.model.apps.Deployment;
2424
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
2525
import io.fabric8.kubernetes.api.model.apps.DeploymentSpec;
26-
import io.fabric8.kubernetes.api.model.apps.DeploymentStatus;
2726
import io.fabric8.kubernetes.client.CustomResource;
2827
import io.fabric8.kubernetes.client.KubernetesClientException;
2928
import io.fabric8.kubernetes.client.http.HttpRequest;
@@ -130,29 +129,6 @@ void setsSpecCustomResourceWithReflection() {
130129
assertThat(tomcat.getSpec().getReplicas()).isEqualTo(1);
131130
}
132131

133-
@Test
134-
void setsStatusWithReflection() {
135-
Deployment deployment = new Deployment();
136-
DeploymentStatus status = new DeploymentStatus();
137-
status.setReplicas(2);
138-
139-
ReconcilerUtils.setStatus(deployment, status);
140-
141-
assertThat(deployment.getStatus().getReplicas()).isEqualTo(2);
142-
}
143-
144-
@Test
145-
void getsStatusWithReflection() {
146-
Deployment deployment = new Deployment();
147-
DeploymentStatus status = new DeploymentStatus();
148-
status.setReplicas(2);
149-
deployment.setStatus(status);
150-
151-
var res = ReconcilerUtils.getStatus(deployment);
152-
153-
assertThat(((DeploymentStatus) res).getReplicas()).isEqualTo(2);
154-
}
155-
156132
@Test
157133
void loadYamlAsBuilder() {
158134
DeploymentBuilder builder =

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.javaoperatorsdk.operator.processing.dependent;
1717

1818
import java.util.Optional;
19+
import java.util.Set;
1920

2021
import org.junit.jupiter.api.Test;
2122

@@ -99,6 +100,20 @@ public Optional<ConfigMap> getSecondaryResource(
99100
return Optional.ofNullable(secondary);
100101
}
101102

103+
@Override
104+
protected Optional<ConfigMap> selectTargetSecondaryResource(
105+
Set<ConfigMap> secondaryResources,
106+
TestCustomResource primary,
107+
Context<TestCustomResource> context) {
108+
if (secondaryResources.size() == 1) {
109+
return Optional.of(secondaryResources.iterator().next());
110+
} else if (secondaryResources.isEmpty()) {
111+
return Optional.empty();
112+
} else {
113+
throw new IllegalStateException();
114+
}
115+
}
116+
102117
@Override
103118
protected void onCreated(
104119
TestCustomResource primary, ConfigMap created, Context<TestCustomResource> context) {}

operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
class MultipleManagedExternalDependentSameTypeIT {
3030

3131
@RegisterExtension
32-
LocallyRunOperatorExtension operator =
32+
LocallyRunOperatorExtension extension =
3333
LocallyRunOperatorExtension.builder()
3434
.withReconciler(new MultipleManagedExternalDependentResourceReconciler())
3535
.build();
@@ -42,15 +42,15 @@ class MultipleManagedExternalDependentSameTypeIT {
4242

4343
@Test
4444
void handlesExternalCrudOperations() {
45-
operator.create(testResource());
45+
extension.create(testResource());
4646
assertResourceCreatedWithData(DEFAULT_SPEC_VALUE);
4747

4848
var updatedResource = testResource();
4949
updatedResource.getSpec().setValue(UPDATED_SPEC_VALUE);
50-
operator.replace(updatedResource);
50+
extension.replace(updatedResource);
5151
assertResourceCreatedWithData(UPDATED_SPEC_VALUE);
5252

53-
operator.delete(testResource());
53+
extension.delete(testResource());
5454
assertExternalResourceDeleted();
5555
}
5656

operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
import java.util.Objects;
1919

2020
import io.fabric8.kubernetes.api.model.HasMetadata;
21+
import io.javaoperatorsdk.operator.processing.dependent.ExternalDependentIDProvider;
2122
import io.javaoperatorsdk.operator.processing.event.ResourceID;
2223

23-
public class ExternalResource {
24+
public class ExternalResource implements ExternalDependentIDProvider<String> {
2425

2526
public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#";
2627

@@ -80,4 +81,9 @@ public static String toExternalResourceId(HasMetadata primary) {
8081
+ EXTERNAL_RESOURCE_NAME_DELIMITER
8182
+ primary.getMetadata().getNamespace();
8283
}
84+
85+
@Override
86+
public String externalResourceId() {
87+
return id;
88+
}
8389
}

sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import java.io.Serializable;
1919
import java.util.Objects;
2020

21-
public class Schema implements Serializable {
21+
import io.javaoperatorsdk.operator.processing.dependent.ExternalDependentIDProvider;
22+
23+
public class Schema implements Serializable, ExternalDependentIDProvider<String> {
2224

2325
private final String name;
2426
private final String characterSet;
@@ -41,7 +43,7 @@ public boolean equals(Object o) {
4143
if (this == o) return true;
4244
if (o == null || getClass() != o.getClass()) return false;
4345
Schema schema = (Schema) o;
44-
return Objects.equals(name, schema.name);
46+
return Objects.equals(name, schema.name) && Objects.equals(characterSet, schema.characterSet);
4547
}
4648

4749
@Override
@@ -53,4 +55,9 @@ public int hashCode() {
5355
public String toString() {
5456
return "Schema{" + "name='" + name + '\'' + ", characterSet='" + characterSet + '\'' + '}';
5557
}
58+
59+
@Override
60+
public String externalResourceId() {
61+
return name;
62+
}
5663
}

0 commit comments

Comments
 (0)