11package io .javaoperatorsdk .operator .junit ;
22
33import java .io .ByteArrayInputStream ;
4+ import java .io .FileInputStream ;
5+ import java .io .IOException ;
46import java .io .InputStream ;
57import java .nio .charset .StandardCharsets ;
68import java .time .Duration ;
@@ -43,6 +45,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension {
4345 private final List <LocalPortForward > localPortForwards ;
4446 private final List <Class <? extends CustomResource >> additionalCustomResourceDefinitions ;
4547 private final Map <Reconciler , RegisteredController > registeredControllers ;
48+ private final Map <String , String > crdMappings ;
4649
4750 private LocallyRunOperatorExtension (
4851 List <ReconcilerSpec > reconcilers ,
@@ -56,7 +59,8 @@ private LocallyRunOperatorExtension(
5659 KubernetesClient kubernetesClient ,
5760 Consumer <ConfigurationServiceOverrider > configurationServiceOverrider ,
5861 Function <ExtensionContext , String > namespaceNameSupplier ,
59- Function <ExtensionContext , String > perClassNamespaceNameSupplier ) {
62+ Function <ExtensionContext , String > perClassNamespaceNameSupplier ,
63+ Map <String , String > crdMappings ) {
6064 super (
6165 infrastructure ,
6266 infrastructureTimeout ,
@@ -70,8 +74,13 @@ private LocallyRunOperatorExtension(
7074 this .portForwards = portForwards ;
7175 this .localPortForwards = new ArrayList <>(portForwards .size ());
7276 this .additionalCustomResourceDefinitions = additionalCustomResourceDefinitions ;
73- this .operator = new Operator (getKubernetesClient (), configurationServiceOverrider );
77+ configurationServiceOverrider = configurationServiceOverrider != null
78+ ? configurationServiceOverrider
79+ .andThen (overrider -> overrider .withKubernetesClient (kubernetesClient ))
80+ : overrider -> overrider .withKubernetesClient (kubernetesClient );
81+ this .operator = new Operator (configurationServiceOverrider );
7482 this .registeredControllers = new HashMap <>();
83+ this .crdMappings = crdMappings ;
7584 }
7685
7786 /**
@@ -83,6 +92,52 @@ public static Builder builder() {
8392 return new Builder ();
8493 }
8594
95+ public static void applyCrd (Class <? extends HasMetadata > resourceClass , KubernetesClient client ) {
96+ applyCrd (ReconcilerUtils .getResourceTypeName (resourceClass ), client );
97+ }
98+
99+ /**
100+ * Applies the CRD associated with the specified resource name to the cluster. Note that the CRD
101+ * is assumed to have been generated in this case from the Java classes and is therefore expected
102+ * to be found in the standard location with the default name for such CRDs and assumes a v1
103+ * version of the CRD spec is used. This means that, provided a given {@code resourceTypeName},
104+ * the associated CRD is expected to be found at {@code META-INF/fabric8/resourceTypeName-v1.yml}
105+ * in the project's classpath.
106+ *
107+ * @param resourceTypeName the standard resource name for CRDs i.e. {@code plural.group}
108+ * @param client the kubernetes client to use to connect to the cluster
109+ */
110+ public static void applyCrd (String resourceTypeName , KubernetesClient client ) {
111+ String path = "/META-INF/fabric8/" + resourceTypeName + "-v1.yml" ;
112+ try (InputStream is = LocallyRunOperatorExtension .class .getResourceAsStream (path )) {
113+ applyCrd (is , path , client );
114+ } catch (IllegalStateException e ) {
115+ // rethrow directly
116+ throw e ;
117+ } catch (IOException e ) {
118+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , e );
119+ }
120+ }
121+
122+ private static void applyCrd (InputStream is , String path , KubernetesClient client ) {
123+ try {
124+ if (is == null ) {
125+ throw new IllegalStateException ("Cannot find CRD at " + path );
126+ }
127+ var crdString = new String (is .readAllBytes (), StandardCharsets .UTF_8 );
128+ LOGGER .debug ("Applying CRD: {}" , crdString );
129+ final var crd = client .load (new ByteArrayInputStream (crdString .getBytes ()));
130+ crd .serverSideApply ();
131+ Thread .sleep (CRD_READY_WAIT ); // readiness is not applicable for CRD, just wait a little
132+ LOGGER .debug ("Applied CRD with path: {}" , path );
133+ } catch (InterruptedException ex ) {
134+ LOGGER .error ("Interrupted." , ex );
135+ Thread .currentThread ().interrupt ();
136+ } catch (Exception ex ) {
137+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , ex );
138+ }
139+ }
140+
86141 private Stream <Reconciler > reconcilers () {
87142 return reconcilers .stream ().map (reconcilerSpec -> reconcilerSpec .reconciler );
88143 }
@@ -134,14 +189,14 @@ protected void before(ExtensionContext context) {
134189 .withName (podName ).portForward (ref .getPort (), ref .getLocalPort ()));
135190 }
136191
137- additionalCustomResourceDefinitions
138- .forEach (cr -> applyCrd (ReconcilerUtils .getResourceTypeName (cr )));
192+ additionalCustomResourceDefinitions .forEach (this ::applyCrd );
139193
140194 for (var ref : reconcilers ) {
141195 final var config = operator .getConfigurationService ().getConfigurationFor (ref .reconciler );
142196 final var oconfig = override (config );
143197
144- if (Namespaced .class .isAssignableFrom (config .getResourceClass ())) {
198+ final var resourceClass = config .getResourceClass ();
199+ if (Namespaced .class .isAssignableFrom (resourceClass )) {
145200 oconfig .settingNamespace (namespace );
146201 }
147202
@@ -152,11 +207,17 @@ protected void before(ExtensionContext context) {
152207 ref .controllerConfigurationOverrider .accept (oconfig );
153208 }
154209
210+ final var unapplied = new HashMap <>(crdMappings );
211+ final var resourceTypeName = ReconcilerUtils .getResourceTypeName (resourceClass );
155212 // only try to apply a CRD for the reconciler if it is associated to a CR
156- if (CustomResource .class .isAssignableFrom (config .getResourceClass ())) {
157- applyCrd (config .getResourceTypeName ());
213+ if (CustomResource .class .isAssignableFrom (resourceClass )) {
214+ applyCrd (resourceTypeName );
215+ unapplied .remove (resourceTypeName );
158216 }
159217
218+ // apply yet unapplied CRDs
219+ unapplied .keySet ().forEach (this ::applyCrd );
220+
160221 var registeredController = this .operator .register (ref .reconciler , oconfig .build ());
161222 registeredControllers .put (ref .reconciler , registeredController );
162223 }
@@ -165,31 +226,28 @@ protected void before(ExtensionContext context) {
165226 this .operator .start ();
166227 }
167228
168- private void applyCrd (String resourceTypeName ) {
169- applyCrd (resourceTypeName , getKubernetesClient ());
170- }
171-
172- public static void applyCrd (Class <? extends HasMetadata > resourceClass , KubernetesClient client ) {
173- applyCrd (ReconcilerUtils .getResourceTypeName (resourceClass ), client );
229+ /**
230+ * Applies the CRD associated with the specified custom resource, first checking if a CRD has been
231+ * manually specified using {@link Builder#withCRDMapping(Class, String)}, otherwise assuming that
232+ * its CRD should be found in the standard location as explained in
233+ * {@link LocallyRunOperatorExtension#applyCrd(String, KubernetesClient)}
234+ *
235+ * @param crClass the custom resource class for which we want to apply the CRD
236+ */
237+ public void applyCrd (Class <? extends CustomResource > crClass ) {
238+ applyCrd (ReconcilerUtils .getResourceTypeName (crClass ));
174239 }
175240
176- public static void applyCrd (String resourceTypeName , KubernetesClient client ) {
177- String path = "/META-INF/fabric8/" + resourceTypeName + "-v1.yml" ;
178- try (InputStream is = LocallyRunOperatorExtension .class .getResourceAsStream (path )) {
179- if (is == null ) {
180- throw new IllegalStateException ("Cannot find CRD at " + path );
241+ public void applyCrd (String resourceTypeName ) {
242+ final var path = crdMappings .get (resourceTypeName );
243+ if (path != null ) {
244+ try (InputStream inputStream = new FileInputStream (path )) {
245+ applyCrd (inputStream , path , getKubernetesClient ());
246+ } catch (IOException e ) {
247+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , e );
181248 }
182- var crdString = new String (is .readAllBytes (), StandardCharsets .UTF_8 );
183- LOGGER .debug ("Applying CRD: {}" , crdString );
184- final var crd = client .load (new ByteArrayInputStream (crdString .getBytes ()));
185- crd .serverSideApply ();
186- Thread .sleep (CRD_READY_WAIT ); // readiness is not applicable for CRD, just wait a little
187- LOGGER .debug ("Applied CRD with path: {}" , path );
188- } catch (InterruptedException ex ) {
189- LOGGER .error ("Interrupted." , ex );
190- Thread .currentThread ().interrupt ();
191- } catch (Exception ex ) {
192- throw new IllegalStateException ("Cannot apply CRD yaml: " + path , ex );
249+ } else {
250+ applyCrd (resourceTypeName , getKubernetesClient ());
193251 }
194252 }
195253
@@ -218,13 +276,15 @@ public static class Builder extends AbstractBuilder<Builder> {
218276 private final List <ReconcilerSpec > reconcilers ;
219277 private final List <PortForwardSpec > portForwards ;
220278 private final List <Class <? extends CustomResource >> additionalCustomResourceDefinitions ;
279+ private final Map <String , String > crdMappings ;
221280 private KubernetesClient kubernetesClient ;
222281
223282 protected Builder () {
224283 super ();
225284 this .reconcilers = new ArrayList <>();
226285 this .portForwards = new ArrayList <>();
227286 this .additionalCustomResourceDefinitions = new ArrayList <>();
287+ this .crdMappings = new HashMap <>();
228288 }
229289
230290 public Builder withReconciler (
@@ -279,6 +339,16 @@ public Builder withAdditionalCustomResourceDefinition(
279339 return this ;
280340 }
281341
342+ public Builder withCRDMapping (Class <? extends CustomResource > customResourceClass ,
343+ String path ) {
344+ return withCRDMapping (ReconcilerUtils .getResourceTypeName (customResourceClass ), path );
345+ }
346+
347+ public Builder withCRDMapping (String resourceTypeName , String path ) {
348+ crdMappings .put (resourceTypeName , path );
349+ return this ;
350+ }
351+
282352 public LocallyRunOperatorExtension build () {
283353 return new LocallyRunOperatorExtension (
284354 reconcilers ,
@@ -290,7 +360,8 @@ public LocallyRunOperatorExtension build() {
290360 waitForNamespaceDeletion ,
291361 oneNamespacePerClass ,
292362 kubernetesClient ,
293- configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier );
363+ configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier ,
364+ crdMappings );
294365 }
295366 }
296367
0 commit comments