Skip to content

Commit b7bdd97

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent b964031 commit b7bdd97

6 files changed

Lines changed: 310 additions & 7 deletions

File tree

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ public P jsonPatchPrimaryStatus(
570570
return resourcePatch(
571571
desired,
572572
actualResource,
573-
r -> context.getClient().resource(actualResource).editStatus(unaryOperator),
573+
r -> context.getClient().resource(actualResource).status().edit(unaryOperator),
574574
context.eventSourceRetriever().getControllerEventSource(),
575575
options);
576576
}
@@ -769,13 +769,15 @@ public <R extends HasMetadata> R resourcePatch(
769769
if (matches) {
770770
return actualResource;
771771
}
772-
boolean optimisticLocking = desiredResource.getMetadata().getResourceVersion() != null;
772+
var targetBaseResource = desiredResource != null ? desiredResource : actualResource;
773+
774+
boolean optimisticLocking = targetBaseResource.getMetadata().getResourceVersion() != null;
773775

774776
if (options.getMode() == Mode.CACHE_ONLY
775777
|| (options.getMode() == Mode.FILTER_IF_OPTIMISTIC_LOCKING && !optimisticLocking)) {
776-
return ies.updateAndCacheResource(desiredResource, updateOperation);
778+
return ies.updateAndCacheResource(targetBaseResource, updateOperation);
777779
} else {
778-
return ies.eventFilteringUpdateAndCacheResource(desiredResource, updateOperation);
780+
return ies.eventFilteringUpdateAndCacheResource(targetBaseResource, updateOperation);
779781
}
780782
}
781783

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,15 +398,14 @@ private R editStatus(Context<R> context, R resource, R originalResource) {
398398
String resourceVersion = resource.getMetadata().getResourceVersion();
399399
// the cached resource should not be changed in any circumstances
400400
// that can lead to all kinds of race conditions.
401-
// TODO review
402401
R clonedOriginal = cloner.clone(originalResource);
403402
try {
404403
clonedOriginal.getMetadata().setResourceVersion(null);
405404
resource.getMetadata().setResourceVersion(null);
406405
return context
407406
.resourceOperations()
408407
.jsonPatchPrimaryStatus(
409-
originalResource,
408+
clonedOriginal,
410409
r -> {
411410
ReconcilerUtilsInternal.setStatus(r, ReconcilerUtilsInternal.getStatus(resource));
412411
return r;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.baseapi.resourceoperations;
17+
18+
import io.fabric8.kubernetes.api.model.Namespaced;
19+
import io.fabric8.kubernetes.client.CustomResource;
20+
import io.fabric8.kubernetes.model.annotation.Group;
21+
import io.fabric8.kubernetes.model.annotation.ShortNames;
22+
import io.fabric8.kubernetes.model.annotation.Version;
23+
24+
@Group("sample.javaoperatorsdk")
25+
@Version("v1")
26+
@ShortNames("secropres")
27+
public class SecondaryResourceOperationsCustomResource
28+
extends CustomResource<ResourceOperationsSpec, ResourceOperationsStatus>
29+
implements Namespaced {}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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.baseapi.resourceoperations;
17+
18+
import java.time.Duration;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
23+
import io.fabric8.kubernetes.api.model.ConfigMap;
24+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
25+
import io.javaoperatorsdk.operator.baseapi.resourceoperations.SecondaryResourceOperationsReconciler.Operation;
26+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
27+
28+
import static io.javaoperatorsdk.operator.baseapi.resourceoperations.SecondaryResourceOperationsReconciler.APPLIED_VALUE;
29+
import static io.javaoperatorsdk.operator.baseapi.resourceoperations.SecondaryResourceOperationsReconciler.CREATE_VALUE;
30+
import static io.javaoperatorsdk.operator.baseapi.resourceoperations.SecondaryResourceOperationsReconciler.DATA_KEY;
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.awaitility.Awaitility.await;
33+
34+
/**
35+
* Integration test for the secondary-resource variants of {@link
36+
* io.javaoperatorsdk.operator.api.reconciler.ResourceOperations} (the overloads taking an {@code
37+
* InformerEventSource}). For each operation it asserts:
38+
*
39+
* <ul>
40+
* <li>the managed {@code ConfigMap} secondary is created/updated to the expected value, and
41+
* <li>the write on the secondary is filtered as an own event, so the controller converges and
42+
* does not loop on its own secondary write.
43+
* </ul>
44+
*/
45+
class SecondaryResourceOperationsIT {
46+
47+
static final String RESOURCE_NAME = "test-resource";
48+
49+
SecondaryResourceOperationsReconciler reconciler = new SecondaryResourceOperationsReconciler();
50+
51+
@RegisterExtension
52+
LocallyRunOperatorExtension extension =
53+
LocallyRunOperatorExtension.builder().withReconciler(reconciler).build();
54+
55+
@Test
56+
void create() {
57+
reconciler.setOperation(Operation.CREATE);
58+
extension.create(testResource());
59+
awaitConfigMapValue(CREATE_VALUE);
60+
assertConvergesWithoutLooping(Operation.CREATE);
61+
}
62+
63+
@Test
64+
void serverSideApply() {
65+
assertAppliedOperation(Operation.SSA);
66+
}
67+
68+
@Test
69+
void update() {
70+
assertAppliedOperation(Operation.UPDATE);
71+
}
72+
73+
@Test
74+
void jsonPatch() {
75+
assertAppliedOperation(Operation.JSON_PATCH);
76+
}
77+
78+
@Test
79+
void jsonMergePatch() {
80+
assertAppliedOperation(Operation.JSON_MERGE_PATCH);
81+
}
82+
83+
private void assertAppliedOperation(Operation operation) {
84+
reconciler.setOperation(operation);
85+
extension.create(testResource());
86+
awaitConfigMapValue(APPLIED_VALUE);
87+
assertConvergesWithoutLooping(operation);
88+
}
89+
90+
private void awaitConfigMapValue(String expected) {
91+
await()
92+
.atMost(Duration.ofSeconds(30))
93+
.untilAsserted(
94+
() -> {
95+
var cm = extension.get(ConfigMap.class, RESOURCE_NAME);
96+
assertThat(cm).isNotNull();
97+
assertThat(cm.getData()).containsEntry(DATA_KEY, expected);
98+
});
99+
}
100+
101+
private void assertConvergesWithoutLooping(Operation operation) {
102+
await()
103+
.during(Duration.ofSeconds(2))
104+
.atMost(Duration.ofSeconds(5))
105+
.untilAsserted(
106+
() ->
107+
assertThat(reconciler.numberOfExecutions.get())
108+
.as(
109+
"operation %s should converge without looping on its own secondary write",
110+
operation)
111+
.isLessThanOrEqualTo(4));
112+
}
113+
114+
SecondaryResourceOperationsCustomResource testResource() {
115+
var r = new SecondaryResourceOperationsCustomResource();
116+
r.setMetadata(new ObjectMetaBuilder().withName(RESOURCE_NAME).build());
117+
r.setSpec(new ResourceOperationsSpec().setValue("initial"));
118+
return r;
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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.baseapi.resourceoperations;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.concurrent.atomic.AtomicInteger;
21+
22+
import io.fabric8.kubernetes.api.model.ConfigMap;
23+
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
24+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
25+
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
26+
import io.javaoperatorsdk.operator.api.reconciler.Context;
27+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
28+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
29+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
30+
import io.javaoperatorsdk.operator.api.reconciler.ResourceOperations.Options;
31+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
32+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
33+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
34+
35+
/**
36+
* Exercises the secondary-resource variants of {@link
37+
* io.javaoperatorsdk.operator.api.reconciler.ResourceOperations} - the overloads that take an
38+
* {@link InformerEventSource} so the written secondary is cached (and its own event filtered)
39+
* against that source. On every reconciliation it ensures a {@code ConfigMap} secondary exists with
40+
* the value dictated by the selected {@link Operation}, using {@code context.resourceOperations()}.
41+
*
42+
* <p>The operations are applied idempotently: the secondary is only written when missing or when
43+
* its data differs from the desired value. Together with own event filtering this keeps the
44+
* controller from looping on the secondary writes it performs itself.
45+
*/
46+
@ControllerConfiguration
47+
public class SecondaryResourceOperationsReconciler
48+
implements Reconciler<SecondaryResourceOperationsCustomResource> {
49+
50+
public static final String DATA_KEY = "value";
51+
public static final String CREATE_VALUE = "created";
52+
public static final String APPLIED_VALUE = "applied";
53+
54+
public enum Operation {
55+
CREATE,
56+
SSA,
57+
UPDATE,
58+
JSON_PATCH,
59+
JSON_MERGE_PATCH
60+
}
61+
62+
private volatile Operation operation;
63+
final AtomicInteger numberOfExecutions = new AtomicInteger();
64+
65+
private InformerEventSource<ConfigMap, SecondaryResourceOperationsCustomResource>
66+
configMapEventSource;
67+
68+
@Override
69+
public UpdateControl<SecondaryResourceOperationsCustomResource> reconcile(
70+
SecondaryResourceOperationsCustomResource resource,
71+
Context<SecondaryResourceOperationsCustomResource> context) {
72+
numberOfExecutions.incrementAndGet();
73+
var ops = context.resourceOperations();
74+
var actual = context.getSecondaryResource(ConfigMap.class).orElse(null);
75+
76+
if (operation == Operation.CREATE) {
77+
if (actual == null) {
78+
ops.create(
79+
desiredConfigMap(resource, CREATE_VALUE), configMapEventSource, Options.cacheOnly());
80+
}
81+
return UpdateControl.noUpdate();
82+
}
83+
84+
// for the update/patch variants the secondary must exist first
85+
if (actual == null) {
86+
ops.create(
87+
desiredConfigMap(resource, CREATE_VALUE), configMapEventSource, Options.cacheOnly());
88+
return UpdateControl.noUpdate();
89+
}
90+
91+
// idempotency guard: only write when the secondary does not yet hold the desired value
92+
if (APPLIED_VALUE.equals(actual.getData().get(DATA_KEY))) {
93+
return UpdateControl.noUpdate();
94+
}
95+
96+
switch (operation) {
97+
case SSA -> {
98+
var desired = desiredConfigMap(resource, APPLIED_VALUE);
99+
desired.getMetadata().setResourceVersion(null);
100+
ops.serverSideApply(desired, configMapEventSource, Options.forceFilterEvents());
101+
}
102+
case UPDATE -> {
103+
actual.getData().put(DATA_KEY, APPLIED_VALUE);
104+
ops.update(actual, configMapEventSource, Options.filterIfOptimisticLocking());
105+
}
106+
case JSON_PATCH ->
107+
ops.jsonPatch(
108+
actual,
109+
cm -> {
110+
cm.getData().put(DATA_KEY, APPLIED_VALUE);
111+
return cm;
112+
},
113+
configMapEventSource,
114+
Options.filterIfOptimisticLocking());
115+
case JSON_MERGE_PATCH -> {
116+
var desired = desiredConfigMap(resource, APPLIED_VALUE);
117+
ops.jsonMergePatch(desired, configMapEventSource, Options.filterIfOptimisticLocking());
118+
}
119+
default -> throw new IllegalStateException("Unexpected operation: " + operation);
120+
}
121+
return UpdateControl.noUpdate();
122+
}
123+
124+
@Override
125+
public List<EventSource<?, SecondaryResourceOperationsCustomResource>> prepareEventSources(
126+
EventSourceContext<SecondaryResourceOperationsCustomResource> context) {
127+
configMapEventSource =
128+
new InformerEventSource<>(
129+
InformerEventSourceConfiguration.from(
130+
ConfigMap.class, SecondaryResourceOperationsCustomResource.class)
131+
.build(),
132+
context);
133+
return List.of(configMapEventSource);
134+
}
135+
136+
private static ConfigMap desiredConfigMap(
137+
SecondaryResourceOperationsCustomResource primary, String value) {
138+
var cm =
139+
new ConfigMapBuilder()
140+
.withMetadata(
141+
new ObjectMetaBuilder()
142+
.withName(primary.getMetadata().getName())
143+
.withNamespace(primary.getMetadata().getNamespace())
144+
.build())
145+
.withData(Map.of(DATA_KEY, value))
146+
.build();
147+
cm.addOwnerReference(primary);
148+
return cm;
149+
}
150+
151+
public void setOperation(Operation operation) {
152+
this.operation = operation;
153+
}
154+
}

sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ private <T extends HasMetadata> T createOrUpdate(
133133
desired.getMetadata().getName(),
134134
desired.getMetadata().getNamespace());
135135
context.resourceOperations().serverSideApply(desired);
136-
137136
}
138137
return previous;
139138
}

0 commit comments

Comments
 (0)