Skip to content

Commit 67993de

Browse files
authored
fix: ensure manually defined CRDs are considered before generated ones (#2662)
* fix: ensure manually defined CRDs are considered before generated ones Fixes #2658 Signed-off-by: Chris Laprun <claprun@redhat.com> * fix: output more details in logs on errors / failures Signed-off-by: Chris Laprun <claprun@redhat.com> * wip: add more logging Signed-off-by: Chris Laprun <claprun@redhat.com> * fix: add missing file Signed-off-by: Chris Laprun <claprun@redhat.com> * fix: make sure generated CRDs are applied Signed-off-by: Chris Laprun <claprun@redhat.com> --------- Signed-off-by: Chris Laprun <claprun@redhat.com>
1 parent d7c72b4 commit 67993de

File tree

9 files changed

+201
-68
lines changed

9 files changed

+201
-68
lines changed

operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java

Lines changed: 79 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
import java.io.ByteArrayInputStream;
44
import java.io.FileInputStream;
5-
import java.io.FileNotFoundException;
65
import java.io.IOException;
76
import java.io.InputStream;
87
import java.nio.charset.StandardCharsets;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
910
import java.time.Duration;
1011
import java.util.ArrayList;
1112
import java.util.HashMap;
1213
import java.util.List;
1314
import java.util.Map;
1415
import java.util.function.Consumer;
1516
import java.util.function.Function;
16-
import java.util.stream.Collectors;
1717
import java.util.stream.Stream;
1818

1919
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -47,7 +47,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension {
4747
private final List<LocalPortForward> localPortForwards;
4848
private final List<Class<? extends CustomResource>> additionalCustomResourceDefinitions;
4949
private final Map<Reconciler, RegisteredController> registeredControllers;
50-
private final List<String> additionalCrds;
50+
private final Map<String, String> crdMappings;
5151

5252
private LocallyRunOperatorExtension(
5353
List<ReconcilerSpec> reconcilers,
@@ -82,7 +82,24 @@ private LocallyRunOperatorExtension(
8282
: overrider -> overrider.withKubernetesClient(kubernetesClient);
8383
this.operator = new Operator(configurationServiceOverrider);
8484
this.registeredControllers = new HashMap<>();
85-
this.additionalCrds = additionalCrds;
85+
crdMappings = getAdditionalCRDsFromFiles(additionalCrds, getKubernetesClient());
86+
}
87+
88+
static Map<String, String> getAdditionalCRDsFromFiles(Iterable<String> additionalCrds,
89+
KubernetesClient client) {
90+
Map<String, String> crdMappings = new HashMap<>();
91+
additionalCrds.forEach(p -> {
92+
try (InputStream is = new FileInputStream(p)) {
93+
client.load(is).items().stream()
94+
// only consider CRDs to avoid applying random resources to the cluster
95+
.filter(CustomResourceDefinition.class::isInstance)
96+
.map(CustomResourceDefinition.class::cast)
97+
.forEach(crd -> crdMappings.put(crd.getMetadata().getName(), p));
98+
} catch (Exception e) {
99+
throw new RuntimeException("Couldn't load CRD at " + p, e);
100+
}
101+
});
102+
return crdMappings;
86103
}
87104

88105
/**
@@ -112,25 +129,18 @@ public static void applyCrd(Class<? extends HasMetadata> resourceClass, Kubernet
112129
public static void applyCrd(String resourceTypeName, KubernetesClient client) {
113130
String path = "/META-INF/fabric8/" + resourceTypeName + "-v1.yml";
114131
try (InputStream is = LocallyRunOperatorExtension.class.getResourceAsStream(path)) {
115-
applyCrd(is, path, client);
116-
} catch (IllegalStateException e) {
117-
// rethrow directly
118-
throw e;
132+
if (is == null) {
133+
throw new IllegalStateException("Cannot find CRD at " + path);
134+
}
135+
var crdString = new String(is.readAllBytes(), StandardCharsets.UTF_8);
136+
applyCrd(crdString, path, client);
119137
} catch (IOException e) {
120138
throw new IllegalStateException("Cannot apply CRD yaml: " + path, e);
121139
}
122140
}
123141

124-
public static void applyCrd(CustomResourceDefinition crd, KubernetesClient client) {
125-
client.resource(crd).serverSideApply();
126-
}
127-
128-
private static void applyCrd(InputStream is, String path, KubernetesClient client) {
142+
private static void applyCrd(String crdString, String path, KubernetesClient client) {
129143
try {
130-
if (is == null) {
131-
throw new IllegalStateException("Cannot find CRD at " + path);
132-
}
133-
var crdString = new String(is.readAllBytes(), StandardCharsets.UTF_8);
134144
LOGGER.debug("Applying CRD: {}", crdString);
135145
final var crd = client.load(new ByteArrayInputStream(crdString.getBytes()));
136146
crd.serverSideApply();
@@ -144,14 +154,42 @@ private static void applyCrd(InputStream is, String path, KubernetesClient clien
144154
}
145155
}
146156

147-
public static List<CustomResourceDefinition> parseCrds(String path, KubernetesClient client) {
148-
try (InputStream is = new FileInputStream(path)) {
149-
return client.load(new ByteArrayInputStream(is.readAllBytes()))
150-
.items().stream().map(i -> (CustomResourceDefinition) i).collect(Collectors.toList());
151-
} catch (FileNotFoundException e) {
152-
throw new RuntimeException(e);
153-
} catch (IOException e) {
154-
throw new RuntimeException(e);
157+
/**
158+
* Applies the CRD associated with the specified custom resource, first checking if a CRD has been
159+
* manually specified using {@link Builder#withAdditionalCRD}, otherwise assuming that its CRD
160+
* should be found in the standard location as explained in
161+
* {@link LocallyRunOperatorExtension#applyCrd(String, KubernetesClient)}
162+
*
163+
* @param crClass the custom resource class for which we want to apply the CRD
164+
*/
165+
public void applyCrd(Class<? extends CustomResource> crClass) {
166+
applyCrd(ReconcilerUtils.getResourceTypeName(crClass));
167+
}
168+
169+
/**
170+
* Applies the CRD associated with the specified resource type name, first checking if a CRD has
171+
* been manually specified using {@link Builder#withAdditionalCRD}, otherwise assuming that its
172+
* CRD should be found in the standard location as explained in
173+
* {@link LocallyRunOperatorExtension#applyCrd(String, KubernetesClient)}
174+
*
175+
* @param resourceTypeName the resource type name associated with the CRD to be applied,
176+
* typically, given a resource type, its name would be obtained using
177+
* {@link ReconcilerUtils#getResourceTypeName(Class)}
178+
*/
179+
public void applyCrd(String resourceTypeName) {
180+
// first attempt to use a manually defined CRD
181+
final var pathAsString = crdMappings.get(resourceTypeName);
182+
if (pathAsString != null) {
183+
final var path = Path.of(pathAsString);
184+
try {
185+
applyCrd(Files.readString(path), pathAsString, getKubernetesClient());
186+
} catch (IOException e) {
187+
throw new IllegalStateException("Cannot open CRD file at " + path.toAbsolutePath(), e);
188+
}
189+
crdMappings.remove(resourceTypeName);
190+
} else {
191+
// if no manually defined CRD matches the resource type, apply the generated one
192+
applyCrd(resourceTypeName, getKubernetesClient());
155193
}
156194
}
157195

@@ -160,7 +198,7 @@ private Stream<Reconciler> reconcilers() {
160198
}
161199

162200
public List<Reconciler> getReconcilers() {
163-
return reconcilers().collect(Collectors.toUnmodifiableList());
201+
return reconcilers().toList();
164202
}
165203

166204
public Reconciler getFirstReconciler() {
@@ -207,7 +245,6 @@ protected void before(ExtensionContext context) {
207245
}
208246

209247
additionalCustomResourceDefinitions.forEach(this::applyCrd);
210-
Map<String, CustomResourceDefinition> unappliedCRDs = getAdditionalCRDsFromFiles();
211248
for (var ref : reconcilers) {
212249
final var config = operator.getConfigurationService().getConfigurationFor(ref.reconciler);
213250
final var oconfig = override(config);
@@ -227,49 +264,28 @@ protected void before(ExtensionContext context) {
227264
final var resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass);
228265
// only try to apply a CRD for the reconciler if it is associated to a CR
229266
if (CustomResource.class.isAssignableFrom(resourceClass)) {
230-
if (unappliedCRDs.get(resourceTypeName) != null) {
231-
applyCrd(resourceTypeName);
232-
unappliedCRDs.remove(resourceTypeName);
233-
} else {
234-
applyCrd(resourceClass);
235-
}
267+
applyCrd(resourceTypeName);
236268
}
237269

238270
// apply yet unapplied CRDs
239271
var registeredController = this.operator.register(ref.reconciler, oconfig.build());
240272
registeredControllers.put(ref.reconciler, registeredController);
241273
}
242-
unappliedCRDs.keySet().forEach(this::applyCrd);
274+
crdMappings.forEach((crdName, path) -> {
275+
final String crdString;
276+
try {
277+
crdString = Files.readString(Path.of(path));
278+
} catch (IOException e) {
279+
throw new IllegalArgumentException("Couldn't read CRD located at " + path, e);
280+
}
281+
applyCrd(crdString, path, getKubernetesClient());
282+
});
283+
crdMappings.clear();
243284

244285
LOGGER.debug("Starting the operator locally");
245286
this.operator.start();
246287
}
247288

248-
private Map<String, CustomResourceDefinition> getAdditionalCRDsFromFiles() {
249-
Map<String, CustomResourceDefinition> crdMappings = new HashMap<>();
250-
additionalCrds.forEach(p -> {
251-
var crds = parseCrds(p, getKubernetesClient());
252-
crds.forEach(c -> crdMappings.put(c.getMetadata().getName(), c));
253-
});
254-
return crdMappings;
255-
}
256-
257-
/**
258-
* Applies the CRD associated with the specified custom resource, first checking if a CRD has been
259-
* manually specified using {@link Builder#withAdditionalCRD(String)}, otherwise assuming that its
260-
* CRD should be found in the standard location as explained in
261-
* {@link LocallyRunOperatorExtension#applyCrd(String, KubernetesClient)}
262-
*
263-
* @param crClass the custom resource class for which we want to apply the CRD
264-
*/
265-
public void applyCrd(Class<? extends CustomResource> crClass) {
266-
applyCrd(ReconcilerUtils.getResourceTypeName(crClass));
267-
}
268-
269-
public void applyCrd(String resourceTypeName) {
270-
applyCrd(resourceTypeName, getKubernetesClient());
271-
}
272-
273289
@Override
274290
protected void after(ExtensionContext context) {
275291
super.after(context);
@@ -295,7 +311,6 @@ public static class Builder extends AbstractBuilder<Builder> {
295311
private final List<ReconcilerSpec> reconcilers;
296312
private final List<PortForwardSpec> portForwards;
297313
private final List<Class<? extends CustomResource>> additionalCustomResourceDefinitions;
298-
private final Map<String, String> crdMappings;
299314
private final List<String> additionalCRDs = new ArrayList<>();
300315
private KubernetesClient kubernetesClient;
301316

@@ -304,7 +319,6 @@ protected Builder() {
304319
this.reconcilers = new ArrayList<>();
305320
this.portForwards = new ArrayList<>();
306321
this.additionalCustomResourceDefinitions = new ArrayList<>();
307-
this.crdMappings = new HashMap<>();
308322
}
309323

310324
public Builder withReconciler(
@@ -359,8 +373,10 @@ public Builder withAdditionalCustomResourceDefinition(
359373
return this;
360374
}
361375

362-
public Builder withAdditionalCRD(String path) {
363-
additionalCRDs.add(path);
376+
public Builder withAdditionalCRD(String... paths) {
377+
if (paths != null) {
378+
additionalCRDs.addAll(List.of(paths));
379+
}
364380
return this;
365381
}
366382

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: externals.crd.example
5+
spec:
6+
group: crd.example
7+
names:
8+
kind: External
9+
singular: external
10+
plural: externals
11+
scope: Namespaced
12+
versions:
13+
- name: v1
14+
schema:
15+
openAPIV3Schema:
16+
properties:
17+
foo:
18+
type: "string"
19+
type: "object"
20+
served: true
21+
storage: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.javaoperatorsdk.operator.junit;
2+
3+
import java.nio.file.Path;
4+
import java.util.List;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
class LocallyRunOperatorExtensionTest {
13+
14+
@Test
15+
void getAdditionalCRDsFromFiles() {
16+
System.out.println(Path.of("").toAbsolutePath());
17+
System.out.println(Path.of("src/test/crd/test.crd").toAbsolutePath());
18+
final var crds = LocallyRunOperatorExtension.getAdditionalCRDsFromFiles(
19+
List.of("src/test/resources/crd/test.crd", "src/test/crd/test.crd"),
20+
new KubernetesClientBuilder().build());
21+
assertNotNull(crds);
22+
assertEquals(2, crds.size());
23+
assertEquals("src/test/crd/test.crd", crds.get("externals.crd.example"));
24+
assertEquals("src/test/resources/crd/test.crd", crds.get("tests.crd.example"));
25+
}
26+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: tests.crd.example
5+
spec:
6+
group: crd.example
7+
names:
8+
kind: Test
9+
singular: test
10+
plural: tests
11+
scope: Namespaced
12+
versions:
13+
- name: v1
14+
schema:
15+
openAPIV3Schema:
16+
properties:
17+
type: "object"
18+
served: true
19+
storage: true
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Configuration name="TestConfig" status="WARN">
3+
<Appenders>
4+
<Console name="Console" target="SYSTEM_OUT">
5+
<PatternLayout pattern="%d %threadId %-30c{1.} [%-5level] %msg%n%throwable"/>
6+
</Console>
7+
</Appenders>
8+
<Loggers>
9+
<Logger level="debug" name="io.javaoperatorsdk.operator" additivity="false">
10+
<AppenderRef ref="Console"/>
11+
</Logger>
12+
<Root level="info">
13+
<AppenderRef ref="Console"/>
14+
</Root>
15+
</Loggers>
16+
</Configuration>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: externals.crd.example
5+
spec:
6+
group: crd.example
7+
names:
8+
kind: External
9+
singular: external
10+
plural: externals
11+
scope: Namespaced
12+
versions:
13+
- name: v1
14+
schema:
15+
openAPIV3Schema:
16+
properties:
17+
type: "object"
18+
served: true
19+
storage: true

operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,22 @@ public class CRDMappingInTestExtensionIT {
2828
LocallyRunOperatorExtension operator =
2929
LocallyRunOperatorExtension.builder()
3030
.withReconciler(new TestReconciler())
31-
.withAdditionalCRD("src/test/resources/crd/test.crd")
31+
.withAdditionalCRD("src/test/resources/crd/test.crd", "src/test/crd/test.crd")
3232
.build();
3333

3434
@Test
3535
void correctlyAppliesManuallySpecifiedCRD() {
36-
operator.applyCrd(TestCR.class);
37-
3836
final var crdClient = client.apiextensions().v1().customResourceDefinitions();
3937
await().pollDelay(Duration.ofMillis(150))
40-
.untilAsserted(() -> assertThat(crdClient.withName("tests.crd.example").get()).isNotNull());
38+
.untilAsserted(() -> {
39+
final var actual = crdClient.withName("tests.crd.example").get();
40+
assertThat(actual).isNotNull();
41+
assertThat(actual.getSpec().getVersions().get(0).getSchema().getOpenAPIV3Schema()
42+
.getProperties().containsKey("foo")).isTrue();
43+
});
44+
await().pollDelay(Duration.ofMillis(150))
45+
.untilAsserted(
46+
() -> assertThat(crdClient.withName("externals.crd.example").get()).isNotNull());
4147
}
4248

4349
@Group("crd.example")

operator-framework/src/test/resources/crd/test.crd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ spec:
1414
schema:
1515
openAPIV3Schema:
1616
properties:
17+
foo:
18+
type: "string"
1719
type: "object"
1820
served: true
19-
storage: true
21+
storage: true

0 commit comments

Comments
 (0)