1
1
package io .javaoperatorsdk .operator .junit ;
2
2
3
3
import java .io .ByteArrayInputStream ;
4
+ import java .io .FileInputStream ;
5
+ import java .io .IOException ;
4
6
import java .io .InputStream ;
5
7
import java .nio .charset .StandardCharsets ;
6
8
import java .time .Duration ;
@@ -43,6 +45,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension {
43
45
private final List <LocalPortForward > localPortForwards ;
44
46
private final List <Class <? extends CustomResource >> additionalCustomResourceDefinitions ;
45
47
private final Map <Reconciler , RegisteredController > registeredControllers ;
48
+ private final Map <Class <? extends CustomResource >, String > crdMappings ;
46
49
47
50
private LocallyRunOperatorExtension (
48
51
List <ReconcilerSpec > reconcilers ,
@@ -56,7 +59,8 @@ private LocallyRunOperatorExtension(
56
59
KubernetesClient kubernetesClient ,
57
60
Consumer <ConfigurationServiceOverrider > configurationServiceOverrider ,
58
61
Function <ExtensionContext , String > namespaceNameSupplier ,
59
- Function <ExtensionContext , String > perClassNamespaceNameSupplier ) {
62
+ Function <ExtensionContext , String > perClassNamespaceNameSupplier ,
63
+ Map <Class <? extends CustomResource >, String > crdMappings ) {
60
64
super (
61
65
infrastructure ,
62
66
infrastructureTimeout ,
@@ -70,8 +74,12 @@ private LocallyRunOperatorExtension(
70
74
this .portForwards = portForwards ;
71
75
this .localPortForwards = new ArrayList <>(portForwards .size ());
72
76
this .additionalCustomResourceDefinitions = additionalCustomResourceDefinitions ;
73
- this .operator = new Operator (getKubernetesClient (), configurationServiceOverrider );
77
+ configurationServiceOverrider = configurationServiceOverrider != null ?
78
+ configurationServiceOverrider .andThen (overrider -> overrider .withKubernetesClient (kubernetesClient )) :
79
+ overrider -> overrider .withKubernetesClient (kubernetesClient );
80
+ this .operator = new Operator (configurationServiceOverrider );
74
81
this .registeredControllers = new HashMap <>();
82
+ this .crdMappings = crdMappings ;
75
83
}
76
84
77
85
/**
@@ -83,6 +91,52 @@ public static Builder builder() {
83
91
return new Builder ();
84
92
}
85
93
94
+ public static void applyCrd (Class <? extends HasMetadata > resourceClass , KubernetesClient client ) {
95
+ applyCrd (ReconcilerUtils .getResourceTypeName (resourceClass ), client );
96
+ }
97
+
98
+ /**
99
+ * Applies the CRD associated with the specified resource name to the cluster. Note that the CRD
100
+ * is assumed to have been generated in this case from the Java classes and is therefore expected
101
+ * to be found in the standard location with the default name for such CRDs and assumes a v1
102
+ * version of the CRD spec is used. This means that, provided a given {@code resourceTypeName},
103
+ * the associated CRD is expected to be found at {@code META-INF/fabric8/resourceTypeName-v1.yml}
104
+ * in the project's classpath.
105
+ *
106
+ * @param resourceTypeName the standard resource name for CRDs i.e. {@code plural.group}
107
+ * @param client the kubernetes client to use to connect to the cluster
108
+ */
109
+ public static void applyCrd (String resourceTypeName , KubernetesClient client ) {
110
+ String path = "/META-INF/fabric8/" + resourceTypeName + "-v1.yml" ;
111
+ try (InputStream is = LocallyRunOperatorExtension .class .getResourceAsStream (path )) {
112
+ applyCrd (is , path , client );
113
+ } catch (IllegalStateException e ) {
114
+ // rethrow directly
115
+ throw e ;
116
+ } catch (IOException e ) {
117
+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , e );
118
+ }
119
+ }
120
+
121
+ private static void applyCrd (InputStream is , String path , KubernetesClient client ) {
122
+ try {
123
+ if (is == null ) {
124
+ throw new IllegalStateException ("Cannot find CRD at " + path );
125
+ }
126
+ var crdString = new String (is .readAllBytes (), StandardCharsets .UTF_8 );
127
+ LOGGER .debug ("Applying CRD: {}" , crdString );
128
+ final var crd = client .load (new ByteArrayInputStream (crdString .getBytes ()));
129
+ crd .serverSideApply ();
130
+ Thread .sleep (CRD_READY_WAIT ); // readiness is not applicable for CRD, just wait a little
131
+ LOGGER .debug ("Applied CRD with path: {}" , path );
132
+ } catch (InterruptedException ex ) {
133
+ LOGGER .error ("Interrupted." , ex );
134
+ Thread .currentThread ().interrupt ();
135
+ } catch (Exception ex ) {
136
+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , ex );
137
+ }
138
+ }
139
+
86
140
private Stream <Reconciler > reconcilers () {
87
141
return reconcilers .stream ().map (reconcilerSpec -> reconcilerSpec .reconciler );
88
142
}
@@ -134,14 +188,14 @@ protected void before(ExtensionContext context) {
134
188
.withName (podName ).portForward (ref .getPort (), ref .getLocalPort ()));
135
189
}
136
190
137
- additionalCustomResourceDefinitions
138
- .forEach (cr -> applyCrd (ReconcilerUtils .getResourceTypeName (cr )));
191
+ additionalCustomResourceDefinitions .forEach (this ::applyCrd );
139
192
140
193
for (var ref : reconcilers ) {
141
194
final var config = operator .getConfigurationService ().getConfigurationFor (ref .reconciler );
142
195
final var oconfig = override (config );
143
196
144
- if (Namespaced .class .isAssignableFrom (config .getResourceClass ())) {
197
+ final var resourceClass = config .getResourceClass ();
198
+ if (Namespaced .class .isAssignableFrom (resourceClass )) {
145
199
oconfig .settingNamespace (namespace );
146
200
}
147
201
@@ -153,8 +207,8 @@ protected void before(ExtensionContext context) {
153
207
}
154
208
155
209
// 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 () );
210
+ if (CustomResource .class .isAssignableFrom (resourceClass )) {
211
+ applyCrd (resourceClass );
158
212
}
159
213
160
214
var registeredController = this .operator .register (ref .reconciler , oconfig .build ());
@@ -165,31 +219,24 @@ protected void before(ExtensionContext context) {
165
219
this .operator .start ();
166
220
}
167
221
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 );
222
+ /**
223
+ * Applies the CRD associated with the specified custom resource, first checking if a CRD has been
224
+ * manually specified using {@link Builder#withCRDMapping(Class, String)}, otherwise assuming that
225
+ * its CRD should be found in the standard location as explained in
226
+ * {@link LocallyRunOperatorExtension#applyCrd(String, KubernetesClient)}
227
+ *
228
+ * @param crClass the custom resource class for which we want to apply the CRD
229
+ */
230
+ public void applyCrd (Class <? extends CustomResource > crClass ) {
231
+ final var path = crdMappings .get (crClass );
232
+ if (path != null ) {
233
+ try (InputStream inputStream = new FileInputStream (path )) {
234
+ applyCrd (inputStream , path , getKubernetesClient ());
235
+ } catch (IOException e ) {
236
+ throw new IllegalStateException ("Cannot apply CRD yaml: " + path , e );
181
237
}
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 );
238
+ } else {
239
+ applyCrd (crClass , getKubernetesClient ());
193
240
}
194
241
}
195
242
@@ -218,13 +265,15 @@ public static class Builder extends AbstractBuilder<Builder> {
218
265
private final List <ReconcilerSpec > reconcilers ;
219
266
private final List <PortForwardSpec > portForwards ;
220
267
private final List <Class <? extends CustomResource >> additionalCustomResourceDefinitions ;
268
+ private final Map <Class <? extends CustomResource >, String > crdMappings ;
221
269
private KubernetesClient kubernetesClient ;
222
270
223
271
protected Builder () {
224
272
super ();
225
273
this .reconcilers = new ArrayList <>();
226
274
this .portForwards = new ArrayList <>();
227
275
this .additionalCustomResourceDefinitions = new ArrayList <>();
276
+ this .crdMappings = new HashMap <>();
228
277
}
229
278
230
279
public Builder withReconciler (
@@ -279,6 +328,12 @@ public Builder withAdditionalCustomResourceDefinition(
279
328
return this ;
280
329
}
281
330
331
+ public Builder withCRDMapping (Class <? extends CustomResource > customResourceClass ,
332
+ String path ) {
333
+ crdMappings .put (customResourceClass , path );
334
+ return this ;
335
+ }
336
+
282
337
public LocallyRunOperatorExtension build () {
283
338
return new LocallyRunOperatorExtension (
284
339
reconcilers ,
@@ -290,7 +345,8 @@ public LocallyRunOperatorExtension build() {
290
345
waitForNamespaceDeletion ,
291
346
oneNamespacePerClass ,
292
347
kubernetesClient ,
293
- configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier );
348
+ configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier ,
349
+ crdMappings );
294
350
}
295
351
}
296
352
0 commit comments