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,13 @@ 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
79
+ .andThen (overrider -> overrider .withKubernetesClient (kubernetesClient ))
80
+ : overrider -> overrider .withKubernetesClient (kubernetesClient );
81
+ this .operator = new Operator (configurationServiceOverrider );
74
82
this .registeredControllers = new HashMap <>();
83
+ this .crdMappings = crdMappings ;
75
84
}
76
85
77
86
/**
@@ -83,6 +92,52 @@ public static Builder builder() {
83
92
return new Builder ();
84
93
}
85
94
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
+
86
141
private Stream <Reconciler > reconcilers () {
87
142
return reconcilers .stream ().map (reconcilerSpec -> reconcilerSpec .reconciler );
88
143
}
@@ -134,14 +189,14 @@ protected void before(ExtensionContext context) {
134
189
.withName (podName ).portForward (ref .getPort (), ref .getLocalPort ()));
135
190
}
136
191
137
- additionalCustomResourceDefinitions
138
- .forEach (cr -> applyCrd (ReconcilerUtils .getResourceTypeName (cr )));
192
+ additionalCustomResourceDefinitions .forEach (this ::applyCrd );
139
193
140
194
for (var ref : reconcilers ) {
141
195
final var config = operator .getConfigurationService ().getConfigurationFor (ref .reconciler );
142
196
final var oconfig = override (config );
143
197
144
- if (Namespaced .class .isAssignableFrom (config .getResourceClass ())) {
198
+ final var resourceClass = config .getResourceClass ();
199
+ if (Namespaced .class .isAssignableFrom (resourceClass )) {
145
200
oconfig .settingNamespace (namespace );
146
201
}
147
202
@@ -153,8 +208,8 @@ protected void before(ExtensionContext context) {
153
208
}
154
209
155
210
// 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 );
158
213
}
159
214
160
215
var registeredController = this .operator .register (ref .reconciler , oconfig .build ());
@@ -165,31 +220,24 @@ protected void before(ExtensionContext context) {
165
220
this .operator .start ();
166
221
}
167
222
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 );
181
238
}
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 ());
193
241
}
194
242
}
195
243
@@ -218,13 +266,15 @@ public static class Builder extends AbstractBuilder<Builder> {
218
266
private final List <ReconcilerSpec > reconcilers ;
219
267
private final List <PortForwardSpec > portForwards ;
220
268
private final List <Class <? extends CustomResource >> additionalCustomResourceDefinitions ;
269
+ private final Map <Class <? extends CustomResource >, String > crdMappings ;
221
270
private KubernetesClient kubernetesClient ;
222
271
223
272
protected Builder () {
224
273
super ();
225
274
this .reconcilers = new ArrayList <>();
226
275
this .portForwards = new ArrayList <>();
227
276
this .additionalCustomResourceDefinitions = new ArrayList <>();
277
+ this .crdMappings = new HashMap <>();
228
278
}
229
279
230
280
public Builder withReconciler (
@@ -279,6 +329,12 @@ public Builder withAdditionalCustomResourceDefinition(
279
329
return this ;
280
330
}
281
331
332
+ public Builder withCRDMapping (Class <? extends CustomResource > customResourceClass ,
333
+ String path ) {
334
+ crdMappings .put (customResourceClass , path );
335
+ return this ;
336
+ }
337
+
282
338
public LocallyRunOperatorExtension build () {
283
339
return new LocallyRunOperatorExtension (
284
340
reconcilers ,
@@ -290,7 +346,8 @@ public LocallyRunOperatorExtension build() {
290
346
waitForNamespaceDeletion ,
291
347
oneNamespacePerClass ,
292
348
kubernetesClient ,
293
- configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier );
349
+ configurationServiceOverrider , namespaceNameSupplier , perClassNamespaceNameSupplier ,
350
+ crdMappings );
294
351
}
295
352
}
296
353
0 commit comments