Skip to content

Commit bbb63a5

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

File tree

1 file changed

+330
-0
lines changed

1 file changed

+330
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
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.api.reconciler;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.function.UnaryOperator;
21+
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
25+
import io.fabric8.kubernetes.client.KubernetesClient;
26+
import io.fabric8.kubernetes.client.KubernetesClientException;
27+
import io.fabric8.kubernetes.client.dsl.MixedOperation;
28+
import io.fabric8.kubernetes.client.dsl.Resource;
29+
import io.javaoperatorsdk.operator.TestUtils;
30+
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
31+
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
32+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
33+
import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource;
34+
import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource;
35+
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
import static org.junit.jupiter.api.Assertions.*;
39+
import static org.mockito.Mockito.*;
40+
41+
class ResourceOperationsTest {
42+
43+
private static final String FINALIZER_NAME = "test.javaoperatorsdk.io/finalizer";
44+
45+
private Context<TestCustomResource> context;
46+
private KubernetesClient client;
47+
private MixedOperation mixedOperation;
48+
private Resource resourceOp;
49+
private ControllerEventSource<TestCustomResource> controllerEventSource;
50+
private ControllerConfiguration<TestCustomResource> controllerConfiguration;
51+
private ResourceOperations<TestCustomResource> resourceOperations;
52+
53+
@BeforeEach
54+
@SuppressWarnings("unchecked")
55+
void setupMocks() {
56+
context = mock(Context.class);
57+
client = mock(KubernetesClient.class);
58+
mixedOperation = mock(MixedOperation.class);
59+
resourceOp = mock(Resource.class);
60+
controllerEventSource = mock(ControllerEventSource.class);
61+
controllerConfiguration = mock(ControllerConfiguration.class);
62+
63+
var eventSourceRetriever = mock(EventSourceRetriever.class);
64+
65+
when(context.getClient()).thenReturn(client);
66+
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
67+
when(context.getControllerConfiguration()).thenReturn(controllerConfiguration);
68+
when(controllerConfiguration.getFinalizerName()).thenReturn(FINALIZER_NAME);
69+
when(eventSourceRetriever.getControllerEventSource()).thenReturn(controllerEventSource);
70+
71+
when(client.resources(TestCustomResource.class)).thenReturn(mixedOperation);
72+
when(mixedOperation.inNamespace(any())).thenReturn(mixedOperation);
73+
when(mixedOperation.withName(any())).thenReturn(resourceOp);
74+
75+
resourceOperations = new ResourceOperations<>(context);
76+
}
77+
78+
@Test
79+
void addsFinalizer() {
80+
var resource = TestUtils.testCustomResource1();
81+
resource.getMetadata().setResourceVersion("1");
82+
83+
when(context.getPrimaryResource()).thenReturn(resource);
84+
85+
// Mock successful finalizer addition
86+
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
87+
any(), any(UnaryOperator.class)))
88+
.thenAnswer(
89+
invocation -> {
90+
var res = TestUtils.testCustomResource1();
91+
res.getMetadata().setResourceVersion("2");
92+
res.addFinalizer(FINALIZER_NAME);
93+
return res;
94+
});
95+
96+
var result = resourceOperations.addFinalizer(FINALIZER_NAME);
97+
98+
assertThat(result).isNotNull();
99+
assertThat(result.hasFinalizer(FINALIZER_NAME)).isTrue();
100+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
101+
verify(controllerEventSource, times(1))
102+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
103+
}
104+
105+
@Test
106+
void addsFinalizerWithSSA() {
107+
var resource = TestUtils.testCustomResource1();
108+
resource.getMetadata().setResourceVersion("1");
109+
110+
when(context.getPrimaryResource()).thenReturn(resource);
111+
112+
// Mock successful SSA finalizer addition
113+
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
114+
any(), any(UnaryOperator.class)))
115+
.thenAnswer(
116+
invocation -> {
117+
var res = TestUtils.testCustomResource1();
118+
res.getMetadata().setResourceVersion("2");
119+
res.addFinalizer(FINALIZER_NAME);
120+
return res;
121+
});
122+
123+
var result = resourceOperations.addFinalizerWithSSA(FINALIZER_NAME);
124+
125+
assertThat(result).isNotNull();
126+
assertThat(result.hasFinalizer(FINALIZER_NAME)).isTrue();
127+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
128+
verify(controllerEventSource, times(1))
129+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
130+
}
131+
132+
@Test
133+
void removesFinalizer() {
134+
var resource = TestUtils.testCustomResource1();
135+
resource.getMetadata().setResourceVersion("1");
136+
resource.addFinalizer(FINALIZER_NAME);
137+
138+
when(context.getPrimaryResource()).thenReturn(resource);
139+
140+
// Mock successful finalizer removal
141+
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
142+
any(), any(UnaryOperator.class)))
143+
.thenAnswer(
144+
invocation -> {
145+
var res = TestUtils.testCustomResource1();
146+
res.getMetadata().setResourceVersion("2");
147+
// finalizer is removed, so don't add it
148+
return res;
149+
});
150+
151+
var result = resourceOperations.removeFinalizer(FINALIZER_NAME);
152+
153+
assertThat(result).isNotNull();
154+
assertThat(result.hasFinalizer(FINALIZER_NAME)).isFalse();
155+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
156+
verify(controllerEventSource, times(1))
157+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
158+
}
159+
160+
@Test
161+
void retriesAddingFinalizerWithoutSSA() {
162+
var resource = TestUtils.testCustomResource1();
163+
resource.getMetadata().setResourceVersion("1");
164+
165+
when(context.getPrimaryResource()).thenReturn(resource);
166+
167+
// First call throws conflict, second succeeds
168+
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
169+
any(), any(UnaryOperator.class)))
170+
.thenThrow(new KubernetesClientException("Conflict", 409, null))
171+
.thenAnswer(
172+
invocation -> {
173+
var res = TestUtils.testCustomResource1();
174+
res.getMetadata().setResourceVersion("2");
175+
res.addFinalizer(FINALIZER_NAME);
176+
return res;
177+
});
178+
179+
// Return fresh resource on retry
180+
when(resourceOp.get()).thenReturn(resource);
181+
182+
var result = resourceOperations.addFinalizer(FINALIZER_NAME);
183+
184+
assertThat(result).isNotNull();
185+
assertThat(result.hasFinalizer(FINALIZER_NAME)).isTrue();
186+
verify(controllerEventSource, times(2))
187+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
188+
verify(resourceOp, times(1)).get();
189+
}
190+
191+
@Test
192+
void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() {
193+
var resource = TestUtils.testCustomResource1();
194+
resource.getMetadata().setResourceVersion("1");
195+
resource.addFinalizer(FINALIZER_NAME);
196+
197+
when(context.getPrimaryResource()).thenReturn(resource);
198+
199+
// First call throws conflict
200+
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
201+
any(), any(UnaryOperator.class)))
202+
.thenThrow(new KubernetesClientException("Conflict", 409, null));
203+
204+
// Return null on retry (resource was deleted)
205+
when(resourceOp.get()).thenReturn(null);
206+
207+
resourceOperations.removeFinalizer(FINALIZER_NAME);
208+
209+
verify(controllerEventSource, times(1))
210+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
211+
verify(resourceOp, times(1)).get();
212+
}
213+
214+
@Test
215+
void retriesFinalizerRemovalWithFreshResource() {
216+
var originalResource = TestUtils.testCustomResource1();
217+
originalResource.getMetadata().setResourceVersion("1");
218+
originalResource.addFinalizer(FINALIZER_NAME);
219+
220+
when(context.getPrimaryResource()).thenReturn(originalResource);
221+
222+
// First call throws unprocessable (422), second succeeds
223+
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
224+
any(), any(UnaryOperator.class)))
225+
.thenThrow(new KubernetesClientException("Unprocessable", 422, null))
226+
.thenAnswer(
227+
invocation -> {
228+
var res = TestUtils.testCustomResource1();
229+
res.getMetadata().setResourceVersion("3");
230+
// finalizer should be removed
231+
return res;
232+
});
233+
234+
// Return fresh resource with newer version on retry
235+
var freshResource = TestUtils.testCustomResource1();
236+
freshResource.getMetadata().setResourceVersion("2");
237+
freshResource.addFinalizer(FINALIZER_NAME);
238+
when(resourceOp.get()).thenReturn(freshResource);
239+
240+
var result = resourceOperations.removeFinalizer(FINALIZER_NAME);
241+
242+
assertThat(result).isNotNull();
243+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("3");
244+
assertThat(result.hasFinalizer(FINALIZER_NAME)).isFalse();
245+
verify(controllerEventSource, times(2))
246+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
247+
verify(resourceOp, times(1)).get();
248+
}
249+
250+
@Test
251+
void resourcePatchWithSingleEventSource() {
252+
var resource = TestUtils.testCustomResource1();
253+
resource.getMetadata().setResourceVersion("1");
254+
255+
var updatedResource = TestUtils.testCustomResource1();
256+
updatedResource.getMetadata().setResourceVersion("2");
257+
258+
var eventSourceRetriever = mock(EventSourceRetriever.class);
259+
var managedEventSource = mock(ManagedInformerEventSource.class);
260+
261+
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
262+
when(eventSourceRetriever.getEventSourcesFor(TestCustomResource.class))
263+
.thenReturn(List.of(managedEventSource));
264+
when(managedEventSource.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class)))
265+
.thenReturn(updatedResource);
266+
267+
var result = resourceOperations.resourcePatch(context, resource, UnaryOperator.identity());
268+
269+
assertThat(result).isNotNull();
270+
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
271+
verify(managedEventSource, times(1))
272+
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
273+
}
274+
275+
@Test
276+
void resourcePatchThrowsWhenNoEventSourceFound() {
277+
var resource = TestUtils.testCustomResource1();
278+
var eventSourceRetriever = mock(EventSourceRetriever.class);
279+
280+
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
281+
when(eventSourceRetriever.getEventSourcesFor(TestCustomResource.class))
282+
.thenReturn(Collections.emptyList());
283+
284+
var exception =
285+
assertThrows(
286+
IllegalStateException.class,
287+
() -> resourceOperations.resourcePatch(context, resource, UnaryOperator.identity()));
288+
289+
assertThat(exception.getMessage()).contains("No event source found for type");
290+
}
291+
292+
@Test
293+
void resourcePatchThrowsWhenMultipleEventSourcesFound() {
294+
var resource = TestUtils.testCustomResource1();
295+
var eventSourceRetriever = mock(EventSourceRetriever.class);
296+
var eventSource1 = mock(ManagedInformerEventSource.class);
297+
var eventSource2 = mock(ManagedInformerEventSource.class);
298+
299+
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
300+
when(eventSourceRetriever.getEventSourcesFor(TestCustomResource.class))
301+
.thenReturn(List.of(eventSource1, eventSource2));
302+
303+
var exception =
304+
assertThrows(
305+
IllegalStateException.class,
306+
() -> resourceOperations.resourcePatch(context, resource, UnaryOperator.identity()));
307+
308+
assertThat(exception.getMessage()).contains("Multiple event sources found for");
309+
assertThat(exception.getMessage()).contains("please provide the target event source");
310+
}
311+
312+
@Test
313+
void resourcePatchThrowsWhenEventSourceIsNotManagedInformer() {
314+
var resource = TestUtils.testCustomResource1();
315+
var eventSourceRetriever = mock(EventSourceRetriever.class);
316+
var nonManagedEventSource = mock(EventSource.class);
317+
318+
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
319+
when(eventSourceRetriever.getEventSourcesFor(TestCustomResource.class))
320+
.thenReturn(List.of(nonManagedEventSource));
321+
322+
var exception =
323+
assertThrows(
324+
IllegalStateException.class,
325+
() -> resourceOperations.resourcePatch(context, resource, UnaryOperator.identity()));
326+
327+
assertThat(exception.getMessage()).contains("Target event source must be a subclass off");
328+
assertThat(exception.getMessage()).contains("ManagedInformerEventSource");
329+
}
330+
}

0 commit comments

Comments
 (0)