Skip to content

Commit dc5c922

Browse files
committed
fix: ensure manually defined CRDs are considered before generated ones
Fixes #2658 Signed-off-by: Chris Laprun <[email protected]>
1 parent d7c72b4 commit dc5c922

File tree

6 files changed

+151
-66
lines changed

6 files changed

+151
-66
lines changed

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

Lines changed: 77 additions & 61 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,40 @@ 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 path = crdMappings.get(resourceTypeName);
182+
if (path != null) {
183+
try {
184+
applyCrd(Files.readString(Path.of(path)), path, getKubernetesClient());
185+
} catch (IOException e) {
186+
throw new IllegalStateException("Cannot open CRD file at " + path, e);
187+
}
188+
} else {
189+
// if no manually defined CRD matches the resource type, apply the generated one
190+
applyCrd(resourceTypeName, getKubernetesClient());
155191
}
156192
}
157193

@@ -160,7 +196,7 @@ private Stream<Reconciler> reconcilers() {
160196
}
161197

162198
public List<Reconciler> getReconcilers() {
163-
return reconcilers().collect(Collectors.toUnmodifiableList());
199+
return reconcilers().toList();
164200
}
165201

166202
public Reconciler getFirstReconciler() {
@@ -207,7 +243,6 @@ protected void before(ExtensionContext context) {
207243
}
208244

209245
additionalCustomResourceDefinitions.forEach(this::applyCrd);
210-
Map<String, CustomResourceDefinition> unappliedCRDs = getAdditionalCRDsFromFiles();
211246
for (var ref : reconcilers) {
212247
final var config = operator.getConfigurationService().getConfigurationFor(ref.reconciler);
213248
final var oconfig = override(config);
@@ -227,49 +262,30 @@ protected void before(ExtensionContext context) {
227262
final var resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass);
228263
// only try to apply a CRD for the reconciler if it is associated to a CR
229264
if (CustomResource.class.isAssignableFrom(resourceClass)) {
230-
if (unappliedCRDs.get(resourceTypeName) != null) {
265+
if (crdMappings.get(resourceTypeName) != null) {
231266
applyCrd(resourceTypeName);
232-
unappliedCRDs.remove(resourceTypeName);
233-
} else {
234-
applyCrd(resourceClass);
267+
crdMappings.remove(resourceTypeName);
235268
}
236269
}
237270

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.javaoperatorsdk.operator.junit;
2+
3+
import java.util.List;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
8+
9+
import static org.junit.jupiter.api.Assertions.*;
10+
11+
class LocallyRunOperatorExtensionTest {
12+
13+
@Test
14+
void getAdditionalCRDsFromFiles() {
15+
final var crds = LocallyRunOperatorExtension.getAdditionalCRDsFromFiles(
16+
List.of("src/test/resources/crd/test.crd", "src/test/crd/test.crd"),
17+
new KubernetesClientBuilder().build());
18+
assertNotNull(crds);
19+
assertEquals(2, crds.size());
20+
assertEquals("src/test/crd/test.crd", crds.get("externals.crd.example"));
21+
assertEquals("src/test/resources/crd/test.crd", crds.get("tests.crd.example"));
22+
}
23+
}
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: 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)