0
# Custom Resource Support
1
2
Support for Custom Resource Definitions and Custom Resources with automatic API discovery and full CRUD operations.
3
4
## Capabilities
5
6
### CustomResourceAware Interface
7
8
Interface for components that support custom resource operations.
9
10
```java { .api }
11
/**
12
* Interface for components that support custom resource operations
13
* Enables registration and handling of custom resource definitions
14
*/
15
public interface CustomResourceAware {
16
/**
17
* Register a custom resource definition context
18
* Enables the mock server to handle requests for this custom resource type
19
* @param rdc CustomResourceDefinitionContext defining the custom resource
20
*/
21
void expectCustomResource(CustomResourceDefinitionContext rdc);
22
}
23
```
24
25
### CustomResourceDefinitionProcessor
26
27
Processes custom resource definitions and handles API discovery for custom resources.
28
29
```java { .api }
30
/**
31
* Processes custom resource definitions and handles API metadata
32
* Manages CRD registration and API resource list generation
33
*/
34
public class CustomResourceDefinitionProcessor {
35
36
/**
37
* Add a custom resource definition context
38
* Registers the CRD for API discovery and resource handling
39
* @param context CustomResourceDefinitionContext to register
40
*/
41
public void addCrdContext(CustomResourceDefinitionContext context);
42
43
/**
44
* Get API resources for a given path
45
* Returns APIResourceList for API group/version endpoints
46
* @param path API path (e.g., "/apis/example.com/v1")
47
* @return JSON string of APIResourceList or null if not applicable
48
*/
49
public String getApiResources(String path);
50
51
/**
52
* Check if status subresource is enabled for a resource
53
* @param pathValues Map containing resource path information
54
* @return true if status subresource is enabled
55
*/
56
public boolean isStatusSubresourceEnabledForResource(Map<String, String> pathValues);
57
58
/**
59
* Process CRD-related events
60
* Handles CRD creation, updates, and deletion
61
* @param path Request path
62
* @param crdString CRD resource JSON string
63
* @param delete Whether the CRD was deleted
64
*/
65
public void process(String path, String crdString, boolean delete);
66
67
/**
68
* Get CRD context by API coordinates
69
* @param api API group name
70
* @param version API version
71
* @param plural Resource plural name
72
* @return Optional containing CRD context if found
73
*/
74
public Optional<CustomResourceDefinitionContext> getCrdContext(String api, String version, String plural);
75
76
/**
77
* Find CRD by kind
78
* @param api API group name
79
* @param version API version
80
* @param kind Resource kind name
81
* @return Optional containing CRD context if found
82
*/
83
public Optional<CustomResourceDefinitionContext> findCrd(String api, String version, String kind);
84
85
/**
86
* Remove a CRD context from the processor
87
* @param context CRD context to remove
88
*/
89
public void removeCrdContext(CustomResourceDefinitionContext context);
90
91
/**
92
* Reset the processor state
93
* Clears all registered custom resource definitions
94
*/
95
public void reset();
96
}
97
```
98
99
### Custom Resource Registration
100
101
Methods for registering custom resources with the mock server.
102
103
**Basic Registration:**
104
105
```java
106
@EnableKubernetesMockClient(crud = true)
107
class CustomResourceTest {
108
KubernetesMockServer server;
109
KubernetesClient client;
110
111
@Test
112
void testCustomResourceRegistration() {
113
// Define custom resource context
114
CustomResourceDefinitionContext crdContext = new CustomResourceDefinitionContext.Builder()
115
.withGroup("example.com")
116
.withVersion("v1")
117
.withKind("MyResource")
118
.withPlural("myresources")
119
.withNamespaceScoped(true)
120
.build();
121
122
// Register with server
123
server.expectCustomResource(crdContext);
124
125
// Create generic client for custom resource
126
GenericKubernetesResource customResource = new GenericKubernetesResourceBuilder()
127
.withNewMetadata()
128
.withName("my-custom-resource")
129
.withNamespace("default")
130
.endMetadata()
131
.withApiVersion("example.com/v1")
132
.withKind("MyResource")
133
.addToAdditionalProperties("spec", Map.of("field1", "value1"))
134
.build();
135
136
// Use generic client
137
Resource<GenericKubernetesResource> resource = client
138
.genericKubernetesResources(crdContext)
139
.inNamespace("default")
140
.resource(customResource);
141
142
GenericKubernetesResource created = resource.create();
143
assertEquals("my-custom-resource", created.getMetadata().getName());
144
}
145
}
146
```
147
148
**Advanced Custom Resource Features:**
149
150
```java
151
@Test
152
void testCustomResourceWithStatus() {
153
// CRD with status subresource enabled
154
CustomResourceDefinitionContext crdContext = new CustomResourceDefinitionContext.Builder()
155
.withGroup("example.com")
156
.withVersion("v1")
157
.withKind("MyApp")
158
.withPlural("myapps")
159
.withNamespaceScoped(true)
160
.withStatusSubresource(true) // Enable status subresource
161
.build();
162
163
server.expectCustomResource(crdContext);
164
165
// Create custom resource
166
GenericKubernetesResource app = new GenericKubernetesResourceBuilder()
167
.withNewMetadata()
168
.withName("my-app")
169
.withNamespace("default")
170
.endMetadata()
171
.withApiVersion("example.com/v1")
172
.withKind("MyApp")
173
.addToAdditionalProperties("spec", Map.of(
174
"replicas", 3,
175
"image", "nginx:latest"
176
))
177
.build();
178
179
GenericKubernetesResource created = client
180
.genericKubernetesResources(crdContext)
181
.inNamespace("default")
182
.resource(app)
183
.create();
184
185
// Update status subresource
186
Map<String, Object> status = Map.of(
187
"readyReplicas", 3,
188
"phase", "Running"
189
);
190
created.setAdditionalProperty("status", status);
191
192
GenericKubernetesResource updated = client
193
.genericKubernetesResources(crdContext)
194
.inNamespace("default")
195
.withName("my-app")
196
.updateStatus(created);
197
198
assertEquals("Running",
199
((Map<String, Object>) updated.getAdditionalProperties().get("status")).get("phase"));
200
}
201
```
202
203
### API Discovery Integration
204
205
Custom resources integrate with Kubernetes API discovery mechanisms.
206
207
```java
208
@Test
209
void testCustomResourceApiDiscovery() {
210
// Register multiple custom resources in same API group
211
CustomResourceDefinitionContext app = new CustomResourceDefinitionContext.Builder()
212
.withGroup("example.com")
213
.withVersion("v1")
214
.withKind("MyApp")
215
.withPlural("myapps")
216
.withNamespaceScoped(true)
217
.build();
218
219
CustomResourceDefinitionContext config = new CustomResourceDefinitionContext.Builder()
220
.withGroup("example.com")
221
.withVersion("v1")
222
.withKind("MyConfig")
223
.withPlural("myconfigs")
224
.withNamespaceScoped(false) // Cluster-scoped
225
.build();
226
227
server.expectCustomResource(app);
228
server.expectCustomResource(config);
229
230
// API discovery should work
231
APIResourceList resources = client.apiextensions().apiResources("example.com/v1");
232
assertNotNull(resources);
233
234
// Should contain both custom resources
235
List<String> resourceNames = resources.getResources().stream()
236
.map(APIResource::getName)
237
.collect(Collectors.toList());
238
239
assertTrue(resourceNames.contains("myapps"));
240
assertTrue(resourceNames.contains("myconfigs"));
241
242
// Check namespace scoping
243
APIResource appResource = resources.getResources().stream()
244
.filter(r -> "myapps".equals(r.getName()))
245
.findFirst()
246
.orElseThrow();
247
assertTrue(appResource.getNamespaced());
248
249
APIResource configResource = resources.getResources().stream()
250
.filter(r -> "myconfigs".equals(r.getName()))
251
.findFirst()
252
.orElseThrow();
253
assertFalse(configResource.getNamespaced());
254
}
255
```
256
257
### Custom Resource Definition Management
258
259
Handling of CRD resources themselves within the mock server.
260
261
```java
262
@Test
263
void testCrdManagement() {
264
// Create a CRD resource
265
CustomResourceDefinition crd = new CustomResourceDefinitionBuilder()
266
.withNewMetadata()
267
.withName("myapps.example.com")
268
.endMetadata()
269
.withNewSpec()
270
.withGroup("example.com")
271
.withNewNames()
272
.withKind("MyApp")
273
.withPlural("myapps")
274
.withSingular("myapp")
275
.endNames()
276
.withScope("Namespaced")
277
.addNewVersion()
278
.withName("v1")
279
.withServed(true)
280
.withStorage(true)
281
.withNewSchema()
282
.withNewOpenAPIV3Schema()
283
.withType("object")
284
.addToProperties("spec", new JSONSchemaPropsBuilder()
285
.withType("object")
286
.addToProperties("replicas", new JSONSchemaPropsBuilder()
287
.withType("integer")
288
.build())
289
.build())
290
.endOpenAPIV3Schema()
291
.endSchema()
292
.endVersion()
293
.endSpec()
294
.build();
295
296
// Create CRD in the cluster
297
CustomResourceDefinition created = client.apiextensions().v1()
298
.customResourceDefinitions()
299
.resource(crd)
300
.create();
301
302
assertNotNull(created);
303
assertEquals("myapps.example.com", created.getMetadata().getName());
304
305
// The CRD should automatically enable the custom resource
306
// (In CRUD mode, CRDs are automatically processed)
307
308
// Now we can create instances of the custom resource
309
GenericKubernetesResource myApp = new GenericKubernetesResourceBuilder()
310
.withNewMetadata()
311
.withName("test-app")
312
.withNamespace("default")
313
.endMetadata()
314
.withApiVersion("example.com/v1")
315
.withKind("MyApp")
316
.addToAdditionalProperties("spec", Map.of("replicas", 5))
317
.build();
318
319
// Use the automatically registered custom resource
320
CustomResourceDefinitionContext context = CustomResourceDefinitionContext.fromCrd(created);
321
GenericKubernetesResource createdApp = client
322
.genericKubernetesResources(context)
323
.inNamespace("default")
324
.resource(myApp)
325
.create();
326
327
assertEquals("test-app", createdApp.getMetadata().getName());
328
assertEquals(5, ((Map<String, Object>) createdApp.getAdditionalProperties().get("spec")).get("replicas"));
329
}
330
```
331
332
### Cluster and Namespace Scoped Resources
333
334
Support for both cluster-scoped and namespace-scoped custom resources.
335
336
```java
337
@Test
338
void testClusterScopedCustomResource() {
339
// Define cluster-scoped custom resource
340
CustomResourceDefinitionContext globalConfig = new CustomResourceDefinitionContext.Builder()
341
.withGroup("config.example.com")
342
.withVersion("v1")
343
.withKind("GlobalConfig")
344
.withPlural("globalconfigs")
345
.withNamespaceScoped(false) // Cluster-scoped
346
.build();
347
348
server.expectCustomResource(globalConfig);
349
350
// Create cluster-scoped resource (no namespace)
351
GenericKubernetesResource config = new GenericKubernetesResourceBuilder()
352
.withNewMetadata()
353
.withName("cluster-config")
354
// No namespace for cluster-scoped resources
355
.endMetadata()
356
.withApiVersion("config.example.com/v1")
357
.withKind("GlobalConfig")
358
.addToAdditionalProperties("data", Map.of(
359
"globalSetting", "enabled",
360
"maxConnections", 1000
361
))
362
.build();
363
364
// Use without namespace
365
GenericKubernetesResource created = client
366
.genericKubernetesResources(globalConfig)
367
.resource(config)
368
.create();
369
370
assertEquals("cluster-config", created.getMetadata().getName());
371
assertNull(created.getMetadata().getNamespace());
372
373
// List cluster-scoped resources
374
List<GenericKubernetesResource> configs = client
375
.genericKubernetesResources(globalConfig)
376
.list()
377
.getItems();
378
379
assertEquals(1, configs.size());
380
}
381
382
@Test
383
void testNamespaceScopedCustomResource() {
384
// Define namespace-scoped custom resource
385
CustomResourceDefinitionContext appConfig = new CustomResourceDefinitionContext.Builder()
386
.withGroup("config.example.com")
387
.withVersion("v1")
388
.withKind("AppConfig")
389
.withPlural("appconfigs")
390
.withNamespaceScoped(true) // Namespace-scoped
391
.build();
392
393
server.expectCustomResource(appConfig);
394
395
// Create namespace-scoped resources in different namespaces
396
GenericKubernetesResource config1 = new GenericKubernetesResourceBuilder()
397
.withNewMetadata()
398
.withName("app-config")
399
.withNamespace("namespace1")
400
.endMetadata()
401
.withApiVersion("config.example.com/v1")
402
.withKind("AppConfig")
403
.build();
404
405
GenericKubernetesResource config2 = new GenericKubernetesResourceBuilder()
406
.withNewMetadata()
407
.withName("app-config")
408
.withNamespace("namespace2")
409
.endMetadata()
410
.withApiVersion("config.example.com/v1")
411
.withKind("AppConfig")
412
.build();
413
414
// Create in different namespaces
415
client.genericKubernetesResources(appConfig).inNamespace("namespace1").resource(config1).create();
416
client.genericKubernetesResources(appConfig).inNamespace("namespace2").resource(config2).create();
417
418
// List per namespace
419
List<GenericKubernetesResource> ns1Configs = client
420
.genericKubernetesResources(appConfig)
421
.inNamespace("namespace1")
422
.list()
423
.getItems();
424
assertEquals(1, ns1Configs.size());
425
426
// List across all namespaces
427
List<GenericKubernetesResource> allConfigs = client
428
.genericKubernetesResources(appConfig)
429
.inAnyNamespace()
430
.list()
431
.getItems();
432
assertEquals(2, allConfigs.size());
433
}
434
```