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 <Class <? extends CustomResource >, 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 <Class <? extends CustomResource >, 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
@@ -153,8 +208,8 @@ protected void before(ExtensionContext context) {
153208 }
154209
155210 // 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 () );
211+ if (CustomResource .class .isAssignableFrom (resourceClass )) {
212+ applyCrd (resourceClass );
158213 }
159214
160215 var registeredController = this .operator .register (ref .reconciler , oconfig .build ());
@@ -165,31 +220,24 @@ protected void before(ExtensionContext context) {
165220 this .operator .start ();
166221 }
167222
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 );
174- }
175-
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 );
223+ /**
224+ * Applies the CRD associated with the specified custom resource, first checking if a CRD has been
225+ * manually specified using {@link Builder#withCRDMapping(Class, String)}, otherwise assuming that
226+ * its CRD should be found in the standard location as explained in
227+ * {@link LocallyRunOperatorExtension#applyCrd(String, KubernetesClient)}
228+ *
229+ * @param crClass the custom resource class for which we want to apply the CRD
230+ */
231+ public void applyCrd (Class <? extends CustomResource > crClass ) {
232+ final var path = crdMappings .get (crClass );
233+ if (path != null ) {
234+ try (InputStream inputStream = new FileInputStream (path )) {
235+ applyCrd (inputStream , path , getKubernetesClient ());
236+ } catch (IOException e ) {
237+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , e );
181238 }
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 );
239+ } else {
240+ applyCrd (crClass , getKubernetesClient ());
193241 }
194242 }
195243
@@ -218,13 +266,15 @@ public static class Builder extends AbstractBuilder<Builder> {
218266 private final List <ReconcilerSpec > reconcilers ;
219267 private final List <PortForwardSpec > portForwards ;
220268 private final List <Class <? extends CustomResource >> additionalCustomResourceDefinitions ;
269+ private final Map <Class <? extends CustomResource >, String > crdMappings ;
221270 private KubernetesClient kubernetesClient ;
222271
223272 protected Builder () {
224273 super ();
225274 this .reconcilers = new ArrayList <>();
226275 this .portForwards = new ArrayList <>();
227276 this .additionalCustomResourceDefinitions = new ArrayList <>();
277+ this .crdMappings = new HashMap <>();
228278 }
229279
230280 public Builder withReconciler (
@@ -279,6 +329,12 @@ public Builder withAdditionalCustomResourceDefinition(
279329 return this ;
280330 }
281331
332+ public Builder withCRDMapping (Class <? extends CustomResource > customResourceClass ,
333+ String path ) {
334+ crdMappings .put (customResourceClass , path );
335+ return this ;
336+ }
337+
282338 public LocallyRunOperatorExtension build () {
283339 return new LocallyRunOperatorExtension (
284340 reconcilers ,
@@ -290,7 +346,8 @@ public LocallyRunOperatorExtension build() {
290346 waitForNamespaceDeletion ,
291347 oneNamespacePerClass ,
292348 kubernetesClient ,
293- configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier );
349+ configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier ,
350+ crdMappings );
294351 }
295352 }
296353
0 commit comments