Skip to content

Commit 6812a79

Browse files
committed
feat: support additional instance CRDs in test extension
Adds withAdditionalCustomResourceDefinition(CustomResourceDefinition definition) to LocallyRunOperatorExtension#Builder. These definitions are applied at the same time as additionalCustomResourceDefinitions. The definitions are deleted according to the existing lifecycle. Why: In some cases we may wish to apply a CustomResourceDefinition that has no corresponding CRD file. For example, some frameworks publish fabric8-based api modules which contain only the java classes and no CRD files. Signed-off-by: Robert Young <robertyoungnz@gmail.com>
1 parent b7eb632 commit 6812a79

File tree

2 files changed

+150
-33
lines changed

2 files changed

+150
-33
lines changed

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

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension {
6767
private final List<PortForwardSpec> portForwards;
6868
private final List<LocalPortForward> localPortForwards;
6969
private final List<Class<? extends CustomResource>> additionalCustomResourceDefinitions;
70+
private final List<CustomResourceDefinition> additionalCustomResourceDefinitionInstances;
7071
private final Map<Reconciler, RegisteredController> registeredControllers;
7172
private final Map<String, String> crdMappings;
7273
private final Consumer<LocallyRunOperatorExtension> beforeStartHook;
@@ -76,6 +77,7 @@ private LocallyRunOperatorExtension(
7677
List<HasMetadata> infrastructure,
7778
List<PortForwardSpec> portForwards,
7879
List<Class<? extends CustomResource>> additionalCustomResourceDefinitions,
80+
List<CustomResourceDefinition> additionalCustomResourceDefinitionInstances,
7981
Duration infrastructureTimeout,
8082
boolean preserveNamespaceOnError,
8183
boolean waitForNamespaceDeletion,
@@ -101,6 +103,7 @@ private LocallyRunOperatorExtension(
101103
this.portForwards = portForwards;
102104
this.localPortForwards = new ArrayList<>(portForwards.size());
103105
this.additionalCustomResourceDefinitions = additionalCustomResourceDefinitions;
106+
this.additionalCustomResourceDefinitionInstances = additionalCustomResourceDefinitionInstances;
104107
this.beforeStartHook = beforeStartHook;
105108
configurationServiceOverrider =
106109
configurationServiceOverrider != null
@@ -172,7 +175,7 @@ private static void applyCrd(String crdString, String path, KubernetesClient cli
172175
LOGGER.debug("Applying CRD: {}", crdString);
173176
final var crd = client.load(new ByteArrayInputStream(crdString.getBytes()));
174177
crd.serverSideApply();
175-
appliedCRDs.add(new AppliedCRD(crdString, path));
178+
appliedCRDs.add(new AppliedCRD.FileCRD(crdString, path));
176179
Thread.sleep(CRD_READY_WAIT); // readiness is not applicable for CRD, just wait a little
177180
LOGGER.debug("Applied CRD with path: {}", path);
178181
} catch (InterruptedException ex) {
@@ -195,6 +198,33 @@ public void applyCrd(Class<? extends CustomResource> crClass) {
195198
applyCrd(ReconcilerUtils.getResourceTypeName(crClass));
196199
}
197200

201+
public void applyCrd(CustomResourceDefinition customResourceDefinition) {
202+
try {
203+
String resourceTypeName = customResourceDefinition.getMetadata().getName();
204+
final var pathAsString = crdMappings.get(resourceTypeName);
205+
if (pathAsString != null) {
206+
applyCrdFromMappings(pathAsString, resourceTypeName);
207+
} else {
208+
var resource = getKubernetesClient().resource(customResourceDefinition);
209+
resource.serverSideApply();
210+
Thread.sleep(CRD_READY_WAIT); // readiness is not applicable for CRD, just wait a little
211+
appliedCRDs.add(new AppliedCRD.InstanceCRD(customResourceDefinition));
212+
}
213+
} catch (Exception e) {
214+
throw new RuntimeException(e);
215+
}
216+
}
217+
218+
private void applyCrdFromMappings(String pathAsString, String resourceTypeName) {
219+
final var path = Path.of(pathAsString);
220+
try {
221+
applyCrd(Files.readString(path), pathAsString, getKubernetesClient());
222+
} catch (IOException e) {
223+
throw new IllegalStateException("Cannot open CRD file at " + path.toAbsolutePath(), e);
224+
}
225+
crdMappings.remove(resourceTypeName);
226+
}
227+
198228
/**
199229
* Applies the CRD associated with the specified resource type name, first checking if a CRD has
200230
* been manually specified using {@link Builder#withAdditionalCRD}, otherwise assuming that its
@@ -209,13 +239,7 @@ public void applyCrd(String resourceTypeName) {
209239
// first attempt to use a manually defined CRD
210240
final var pathAsString = crdMappings.get(resourceTypeName);
211241
if (pathAsString != null) {
212-
final var path = Path.of(pathAsString);
213-
try {
214-
applyCrd(Files.readString(path), pathAsString, getKubernetesClient());
215-
} catch (IOException e) {
216-
throw new IllegalStateException("Cannot open CRD file at " + path.toAbsolutePath(), e);
217-
}
218-
crdMappings.remove(resourceTypeName);
242+
applyCrdFromMappings(pathAsString, resourceTypeName);
219243
} else {
220244
// if no manually defined CRD matches the resource type, apply the generated one
221245
applyCrd(resourceTypeName, getKubernetesClient());
@@ -280,6 +304,7 @@ protected void before(ExtensionContext context) {
280304
}
281305

282306
additionalCustomResourceDefinitions.forEach(this::applyCrd);
307+
additionalCustomResourceDefinitionInstances.forEach(this::applyCrd);
283308
for (var ref : reconcilers) {
284309
final var config = operator.getConfigurationService().getConfigurationFor(ref.reconciler);
285310
final var oconfig = override(config);
@@ -361,24 +386,60 @@ private void deleteCrd(AppliedCRD appliedCRD, KubernetesClient client) {
361386
LOGGER.debug("Skipping deleting CRD because of configuration: {}", appliedCRD);
362387
return;
363388
}
364-
try {
365-
LOGGER.debug("Deleting CRD: {}", appliedCRD.crdString);
366-
final var crd = client.load(new ByteArrayInputStream(appliedCRD.crdString.getBytes()));
367-
crd.withTimeoutInMillis(CRD_DELETE_TIMEOUT).delete();
368-
LOGGER.debug("Deleted CRD with path: {}", appliedCRD.path);
369-
} catch (Exception ex) {
370-
LOGGER.warn(
371-
"Cannot delete CRD yaml: {}. You might need to delete it manually.", appliedCRD.path, ex);
372-
}
389+
appliedCRD.delete(client);
373390
}
374391

375-
private record AppliedCRD(String crdString, String path) {}
392+
private sealed interface AppliedCRD permits AppliedCRD.FileCRD, AppliedCRD.InstanceCRD {
393+
/**
394+
* Delete this CRD from the cluster
395+
*
396+
* @param client client to use for deletion
397+
*/
398+
void delete(KubernetesClient client);
399+
400+
record FileCRD(String crdString, String path) implements AppliedCRD {
401+
402+
@Override
403+
public void delete(KubernetesClient client) {
404+
try {
405+
LOGGER.debug("Deleting CRD: {}", crdString);
406+
final var crd = client.load(new ByteArrayInputStream(crdString.getBytes()));
407+
crd.withTimeoutInMillis(CRD_DELETE_TIMEOUT).delete();
408+
LOGGER.debug("Deleted CRD with path: {}", path);
409+
} catch (Exception ex) {
410+
LOGGER.warn(
411+
"Cannot delete CRD yaml: {}. You might need to delete it manually.", path, ex);
412+
}
413+
}
414+
}
415+
416+
record InstanceCRD(CustomResourceDefinition customResourceDefinition) implements AppliedCRD {
417+
418+
@Override
419+
public void delete(KubernetesClient client) {
420+
String type = customResourceDefinition.getMetadata().getName();
421+
try {
422+
LOGGER.debug("Deleting CustomResourceDefinition instance CRD: {}", type);
423+
final var crd = client.resource(customResourceDefinition);
424+
crd.withTimeoutInMillis(CRD_DELETE_TIMEOUT).delete();
425+
LOGGER.debug("Deleted CustomResourceDefinition instance CRD: {}", type);
426+
} catch (Exception ex) {
427+
LOGGER.warn(
428+
"Cannot delete CustomResourceDefinition instance CRD: {}. You might need to delete it"
429+
+ " manually.",
430+
type,
431+
ex);
432+
}
433+
}
434+
}
435+
}
376436

377437
@SuppressWarnings("rawtypes")
378438
public static class Builder extends AbstractBuilder<Builder> {
379439
private final List<ReconcilerSpec> reconcilers;
380440
private final List<PortForwardSpec> portForwards;
381441
private final List<Class<? extends CustomResource>> additionalCustomResourceDefinitions;
442+
private final List<CustomResourceDefinition> additionalCustomResourceDefinitionInstances;
382443
private final List<String> additionalCRDs = new ArrayList<>();
383444
private Consumer<LocallyRunOperatorExtension> beforeStartHook;
384445
private KubernetesClient kubernetesClient;
@@ -389,6 +450,7 @@ protected Builder() {
389450
this.reconcilers = new ArrayList<>();
390451
this.portForwards = new ArrayList<>();
391452
this.additionalCustomResourceDefinitions = new ArrayList<>();
453+
this.additionalCustomResourceDefinitionInstances = new ArrayList<>();
392454
}
393455

394456
public Builder withReconciler(
@@ -449,6 +511,11 @@ public Builder withAdditionalCustomResourceDefinition(
449511
return this;
450512
}
451513

514+
public Builder withAdditionalCustomResourceDefinition(CustomResourceDefinition definition) {
515+
additionalCustomResourceDefinitionInstances.add(definition);
516+
return this;
517+
}
518+
452519
public Builder withAdditionalCRD(String... paths) {
453520
if (paths != null) {
454521
additionalCRDs.addAll(List.of(paths));
@@ -471,6 +538,7 @@ public LocallyRunOperatorExtension build() {
471538
infrastructure,
472539
portForwards,
473540
additionalCustomResourceDefinitions,
541+
additionalCustomResourceDefinitionInstances,
474542
infrastructureTimeout,
475543
preserveNamespaceOnError,
476544
waitForNamespaceDeletion,

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

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@
1616
package io.javaoperatorsdk.operator;
1717

1818
import java.time.Duration;
19+
import java.util.Map;
1920

2021
import org.junit.jupiter.api.Test;
2122
import org.junit.jupiter.api.extension.RegisterExtension;
2223

2324
import io.fabric8.kubernetes.api.model.Namespaced;
25+
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
26+
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionBuilder;
27+
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionList;
28+
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsBuilder;
2429
import io.fabric8.kubernetes.client.CustomResource;
2530
import io.fabric8.kubernetes.client.KubernetesClient;
2631
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
32+
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
33+
import io.fabric8.kubernetes.client.dsl.Resource;
2734
import io.fabric8.kubernetes.model.annotation.Group;
2835
import io.fabric8.kubernetes.model.annotation.Kind;
2936
import io.fabric8.kubernetes.model.annotation.Version;
@@ -44,7 +51,8 @@
4451
Demonstrates how to manually specify and apply Custom Resource Definitions (CRDs) in \
4552
integration tests using the LocallyRunOperatorExtension. This test verifies that CRDs \
4653
can be loaded from specified file paths and properly registered with the Kubernetes API \
47-
server during test execution.
54+
server during test execution. It also verifies that CustomResourceDefinition instances
55+
with no corresponding file can be applied.
4856
""")
4957
public class CRDMappingInTestExtensionIT {
5058
private final KubernetesClient client = new KubernetesClientBuilder().build();
@@ -54,34 +62,75 @@ public class CRDMappingInTestExtensionIT {
5462
LocallyRunOperatorExtension.builder()
5563
.withReconciler(new TestReconciler())
5664
.withAdditionalCRD("src/test/resources/crd/test.crd", "src/test/crd/test.crd")
65+
.withAdditionalCustomResourceDefinition(testCRD())
5766
.build();
5867

68+
public static CustomResourceDefinition testCRD() {
69+
return new CustomResourceDefinitionBuilder()
70+
.editOrNewSpec()
71+
.withScope("Cluster")
72+
.withGroup("operator.javaoperatorsdk.io")
73+
.editOrNewNames()
74+
.withPlural("tests")
75+
.withSingular("test")
76+
.withKind("Test")
77+
.endNames()
78+
.addNewVersion()
79+
.withName("v1")
80+
.withServed(true)
81+
.withStorage(true)
82+
.withNewSchema()
83+
.withNewOpenAPIV3Schema()
84+
.withType("object")
85+
.withProperties(Map.of("bar", new JSONSchemaPropsBuilder().withType("string").build()))
86+
.endOpenAPIV3Schema()
87+
.endSchema()
88+
.endVersion()
89+
.and()
90+
.editOrNewMetadata()
91+
.withName("tests.operator.javaoperatorsdk.io")
92+
.and()
93+
.build();
94+
}
95+
5996
@Test
6097
void correctlyAppliesManuallySpecifiedCRD() {
6198
final var crdClient = client.apiextensions().v1().customResourceDefinitions();
99+
await()
100+
.pollDelay(Duration.ofMillis(150))
101+
.untilAsserted(() -> assertCrdApplied(crdClient, "tests.crd.example", "foo"));
62102
await()
63103
.pollDelay(Duration.ofMillis(150))
64104
.untilAsserted(
65-
() -> {
66-
final var actual = crdClient.withName("tests.crd.example").get();
67-
assertThat(actual).isNotNull();
68-
assertThat(
69-
actual
70-
.getSpec()
71-
.getVersions()
72-
.get(0)
73-
.getSchema()
74-
.getOpenAPIV3Schema()
75-
.getProperties()
76-
.containsKey("foo"))
77-
.isTrue();
78-
});
105+
() -> assertCrdApplied(crdClient, "tests.operator.javaoperatorsdk.io", "bar"));
79106
await()
80107
.pollDelay(Duration.ofMillis(150))
81108
.untilAsserted(
82109
() -> assertThat(crdClient.withName("externals.crd.example").get()).isNotNull());
83110
}
84111

112+
private static void assertCrdApplied(
113+
NonNamespaceOperation<
114+
CustomResourceDefinition,
115+
CustomResourceDefinitionList,
116+
Resource<CustomResourceDefinition>>
117+
crdClient,
118+
String s,
119+
String propertyName) {
120+
final var actual = crdClient.withName(s).get();
121+
assertThat(actual).isNotNull();
122+
assertThat(
123+
actual
124+
.getSpec()
125+
.getVersions()
126+
.get(0)
127+
.getSchema()
128+
.getOpenAPIV3Schema()
129+
.getProperties()
130+
.containsKey(propertyName))
131+
.isTrue();
132+
}
133+
85134
@Group("crd.example")
86135
@Version("v1")
87136
@Kind("Test")

0 commit comments

Comments
 (0)