Skip to content

Commit 1a1529c

Browse files
authored
Merge pull request #2321 from brendandburns/auth_config
Add support for dynamic objects to kubectl apply.
2 parents 16ecd07 + 5107625 commit 1a1529c

File tree

4 files changed

+132
-18
lines changed

4 files changed

+132
-18
lines changed

extended/src/main/java/io/kubernetes/client/extended/kubectl/Kubectl.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package io.kubernetes.client.extended.kubectl;
1414

1515
import io.kubernetes.client.Discovery;
16+
import io.kubernetes.client.apimachinery.GroupVersionKind;
1617
import io.kubernetes.client.apimachinery.GroupVersionResource;
1718
import io.kubernetes.client.common.KubernetesListObject;
1819
import io.kubernetes.client.common.KubernetesObject;
@@ -22,6 +23,8 @@
2223
import io.kubernetes.client.openapi.Configuration;
2324
import io.kubernetes.client.util.ModelMapper;
2425
import io.kubernetes.client.util.generic.GenericKubernetesApi;
26+
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesListObject;
27+
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
2528

2629
/**
2730
* Kubectl provides a set of helper functions that has the same functionalities as corresponding
@@ -308,6 +311,34 @@ protected GenericKubernetesApi<ApiType, KubernetesListObject> getGenericApi()
308311
throws KubectlException {
309312
return getGenericApi(apiTypeClass, KubernetesListObject.class);
310313
}
314+
315+
protected boolean isNamespaced(KubernetesObject obj) {
316+
if (obj instanceof DynamicKubernetesObject) {
317+
// This is slightly hacky, but for now CRD objects are all namespaced.
318+
return true;
319+
} else {
320+
return ModelMapper.isNamespaced(obj.getClass());
321+
}
322+
}
323+
324+
protected GenericKubernetesApi<? extends KubernetesObject, ? extends KubernetesListObject>
325+
getGenericApi(KubernetesObject targetObj) throws KubectlException {
326+
if (targetObj instanceof DynamicKubernetesObject) {
327+
DynamicKubernetesObject obj = (DynamicKubernetesObject) targetObj;
328+
GroupVersionKind gvk =
329+
ModelMapper.groupVersionKindFromApiVersionAndKind(obj.getApiVersion(), obj.getKind());
330+
Discovery.APIResource rsrc = ModelMapper.findApiResourceByGroupVersionKind(gvk);
331+
String plural = rsrc == null ? obj.getKind() + "s" /* hack! */ : rsrc.getResourcePlural();
332+
return new GenericKubernetesApi<DynamicKubernetesObject, DynamicKubernetesListObject>(
333+
DynamicKubernetesObject.class,
334+
DynamicKubernetesListObject.class,
335+
gvk.getGroup(),
336+
gvk.getVersion(),
337+
plural,
338+
apiClient);
339+
}
340+
return getGenericApi();
341+
}
311342
}
312343

313344
abstract static class ResourceAndContainerBuilder<

extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlApply.java

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import io.kubernetes.client.custom.V1Patch;
1818
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
1919
import io.kubernetes.client.openapi.ApiException;
20-
import io.kubernetes.client.util.ModelMapper;
2120
import io.kubernetes.client.util.Namespaces;
2221
import io.kubernetes.client.util.Strings;
2322
import io.kubernetes.client.util.generic.GenericKubernetesApi;
@@ -70,13 +69,17 @@ public ApiType execute() throws KubectlException {
7069
private ApiType executeServerSideApply() throws KubectlException {
7170
refreshDiscovery();
7271

73-
GenericKubernetesApi<ApiType, KubernetesListObject> api = getGenericApi();
72+
GenericKubernetesApi<? extends KubernetesObject, ? extends KubernetesListObject> api =
73+
getGenericApi(this.targetObj);
74+
if (api == null) {
75+
api = getGenericApi();
76+
}
7477

7578
PatchOptions patchOptions = new PatchOptions();
7679
patchOptions.setForce(this.forceConflict);
7780
patchOptions.setFieldManager(this.fieldManager);
7881

79-
if (ModelMapper.isNamespaced(this.targetObj.getClass())) {
82+
if (isNamespaced(this.targetObj)) {
8083
String targetNamespace =
8184
namespace != null
8285
? namespace
@@ -86,26 +89,28 @@ private ApiType executeServerSideApply() throws KubectlException {
8689

8790
KubernetesApiResponse<KubernetesObject> response = null;
8891
try {
89-
return api.patch(
90-
targetNamespace,
91-
targetObj.getMetadata().getName(),
92-
V1Patch.PATCH_FORMAT_APPLY_YAML,
93-
new V1Patch(apiClient.getJSON().serialize(targetObj)),
94-
patchOptions)
95-
.throwsApiException()
96-
.getObject();
92+
return (ApiType)
93+
api.patch(
94+
targetNamespace,
95+
targetObj.getMetadata().getName(),
96+
V1Patch.PATCH_FORMAT_APPLY_YAML,
97+
new V1Patch(apiClient.getJSON().serialize(targetObj)),
98+
patchOptions)
99+
.throwsApiException()
100+
.getObject();
97101
} catch (ApiException e) {
98102
throw new KubectlException(e);
99103
}
100104
} else {
101105
try {
102-
return api.patch(
103-
targetObj.getMetadata().getName(),
104-
V1Patch.PATCH_FORMAT_APPLY_YAML,
105-
new V1Patch(apiClient.getJSON().serialize(targetObj)),
106-
patchOptions)
107-
.throwsApiException()
108-
.getObject();
106+
return (ApiType)
107+
api.patch(
108+
targetObj.getMetadata().getName(),
109+
V1Patch.PATCH_FORMAT_APPLY_YAML,
110+
new V1Patch(apiClient.getJSON().serialize(targetObj)),
111+
patchOptions)
112+
.throwsApiException()
113+
.getObject();
109114
} catch (ApiException e) {
110115
throw new KubectlException(e);
111116
}

extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlApplyTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121

2222
import com.github.tomakehurst.wiremock.junit.WireMockRule;
2323
import com.github.tomakehurst.wiremock.matching.EqualToPattern;
24+
import com.google.gson.JsonObject;
2425
import io.kubernetes.client.custom.V1Patch;
2526
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
2627
import io.kubernetes.client.openapi.ApiClient;
2728
import io.kubernetes.client.openapi.models.V1ConfigMap;
2829
import io.kubernetes.client.openapi.models.V1ObjectMeta;
2930
import io.kubernetes.client.util.ClientBuilder;
31+
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
3032
import java.io.IOException;
3133
import java.nio.file.Files;
3234
import java.nio.file.Paths;
@@ -110,4 +112,48 @@ public void testApplyConfigMap() throws KubectlException, IOException {
110112
1, patchRequestedFor(urlPathEqualTo("/api/v1/namespaces/foo/configmaps/bar")));
111113
assertNotNull(configMap);
112114
}
115+
116+
@Test
117+
public void testApplyDynamic() throws KubectlException, IOException {
118+
JsonObject json = new JsonObject();
119+
json.addProperty("kind", "bar");
120+
json.addProperty("apiVersion", "example.com/v1");
121+
JsonObject meta = new JsonObject();
122+
meta.addProperty("name", "something");
123+
meta.addProperty("namespace", "foo");
124+
json.add("metadata", meta);
125+
126+
DynamicKubernetesObject obj = new DynamicKubernetesObject(json);
127+
wireMockRule.stubFor(
128+
patch(urlPathEqualTo("/apis/example.com/v1/namespaces/foo/bars/something"))
129+
.withHeader("Content-Type", new EqualToPattern(V1Patch.PATCH_FORMAT_APPLY_YAML))
130+
.willReturn(
131+
aResponse()
132+
.withStatus(200)
133+
.withBody("{\"metadata\":{\"name\":\"something\",\"namespace\":\"bar\"}}")));
134+
wireMockRule.stubFor(
135+
get(urlPathEqualTo("/api"))
136+
.willReturn(
137+
aResponse()
138+
.withStatus(200)
139+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API))))));
140+
wireMockRule.stubFor(
141+
get(urlPathEqualTo("/apis"))
142+
.willReturn(
143+
aResponse()
144+
.withStatus(200)
145+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS))))));
146+
wireMockRule.stubFor(
147+
get(urlPathEqualTo("/api/v1"))
148+
.willReturn(
149+
aResponse()
150+
.withStatus(200)
151+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1))))));
152+
153+
DynamicKubernetesObject out =
154+
Kubectl.apply(DynamicKubernetesObject.class).apiClient(apiClient).resource(obj).execute();
155+
wireMockRule.verify(
156+
1, patchRequestedFor(urlPathEqualTo("/apis/example.com/v1/namespaces/foo/bars/something")));
157+
assertNotNull(out);
158+
}
113159
}

util/src/main/java/io/kubernetes/client/util/ModelMapper.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,38 @@ public static Boolean isNamespaced(Class<?> clazz) {
322322
return isNamespacedByClasses.get(clazz);
323323
}
324324

325+
/**
326+
* Find GroupVersionKind from fields in a Kubernetes Resource
327+
*
328+
* @param apiVersion The apiVersion field from the object
329+
* @param kind The kind for the object
330+
* @return the GroupVersionKind
331+
*/
332+
public static GroupVersionKind groupVersionKindFromApiVersionAndKind(
333+
String apiVersion, String kind) {
334+
int ix = apiVersion.indexOf("/");
335+
String group = ix == -1 ? "" : apiVersion.substring(0, ix);
336+
String version = apiVersion.substring(ix + 1);
337+
return new GroupVersionKind(group, version, kind);
338+
}
339+
340+
public static Discovery.APIResource findApiResourceByGroupVersionKind(GroupVersionKind gvk) {
341+
// TODO: Create another hash map to make this lookup fast? For now I don't think it matters, but
342+
// it's definitely slow.
343+
for (Discovery.APIResource apiResource : lastAPIDiscovery) {
344+
if (!apiResource.getGroup().equals(gvk.getGroup())
345+
|| !apiResource.getKind().equals(gvk.getKind())) {
346+
continue;
347+
}
348+
for (String version : apiResource.getVersions()) {
349+
if (version.equals(gvk.getVersion())) {
350+
return apiResource;
351+
}
352+
}
353+
}
354+
return null;
355+
}
356+
325357
private static void initApiGroupMap() {
326358
preBuiltApiGroups.put("Admissionregistration", "admissionregistration.k8s.io");
327359
preBuiltApiGroups.put("Apiextensions", "apiextensions.k8s.io");

0 commit comments

Comments
 (0)