Skip to content

Commit c0ea154

Browse files
committed
kubectl create
1 parent 6a2a60a commit c0ea154

File tree

9 files changed

+272
-28
lines changed

9 files changed

+272
-28
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323
*/
2424
public class Kubectl {
2525

26+
/**
27+
* Equivalent for `kubectl create`
28+
*
29+
* @return the kubectl create
30+
*/
31+
public static KubectlCreate create() {
32+
return new KubectlCreate();
33+
}
34+
2635
/**
2736
* Equivalent for `kubectl top`
2837
*
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.extended.kubectl;
14+
15+
import com.google.common.base.Strings;
16+
import io.kubernetes.client.Discovery;
17+
import io.kubernetes.client.common.KubernetesListObject;
18+
import io.kubernetes.client.common.KubernetesObject;
19+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
20+
import io.kubernetes.client.util.ModelMapper;
21+
import io.kubernetes.client.util.Namespaces;
22+
import io.kubernetes.client.util.generic.GenericKubernetesApi;
23+
import io.kubernetes.client.util.generic.KubernetesApiResponse;
24+
import io.kubernetes.client.util.generic.options.CreateOptions;
25+
import java.util.Optional;
26+
import java.util.Set;
27+
28+
public class KubectlCreate extends Kubectl.ApiClientBuilder<KubectlCreate>
29+
implements Kubectl.Executable<KubernetesObject> {
30+
31+
KubectlCreate() {}
32+
33+
private KubernetesObject targetObj;
34+
35+
public KubectlCreate load(KubernetesObject obj) {
36+
this.targetObj = obj;
37+
return this;
38+
}
39+
40+
@Override
41+
public KubernetesObject execute() throws KubectlException {
42+
Discovery discovery = new Discovery(apiClient);
43+
try {
44+
Set<Discovery.APIResource> apiResources = discovery.findAll();
45+
ModelMapper.refresh(apiResources);
46+
47+
ModelMapper.GroupVersionKind gvk =
48+
ModelMapper.getGroupVersionKindByClass(targetObj.getClass());
49+
Optional<Discovery.APIResource> apiResource =
50+
apiResources.stream()
51+
.filter(r -> r.getKind().equals(gvk.getKind()) && r.getGroup().equals(gvk.getGroup()))
52+
.findFirst();
53+
if (!apiResource.isPresent()) {
54+
throw new KubectlException(
55+
"Cannot recognize such kubernetes resource from api-discovery: " + gvk.toString());
56+
}
57+
GenericKubernetesApi<KubernetesObject, KubernetesListObject> api =
58+
new GenericKubernetesApi(
59+
targetObj.getClass(),
60+
KubernetesListObject.class,
61+
gvk.getGroup(),
62+
gvk.getVersion(),
63+
apiResource.get().getResourcePlural(),
64+
apiClient);
65+
if (apiResource.get().getNamespaced()) {
66+
String namespace =
67+
Strings.isNullOrEmpty(targetObj.getMetadata().getNamespace())
68+
? Namespaces.NAMESPACE_DEFAULT
69+
: targetObj.getMetadata().getNamespace();
70+
KubernetesApiResponse<KubernetesObject> response =
71+
api.create(namespace, targetObj, new CreateOptions());
72+
return response.getObject();
73+
} else {
74+
KubernetesApiResponse<KubernetesObject> response =
75+
api.create(targetObj, new CreateOptions());
76+
return response.getObject();
77+
}
78+
} catch (Throwable t) {
79+
throw new KubectlException(t);
80+
}
81+
}
82+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.extended.kubectl;
14+
15+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
16+
import static org.junit.Assert.assertNotNull;
17+
18+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
19+
import com.google.common.io.Resources;
20+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
21+
import io.kubernetes.client.openapi.ApiClient;
22+
import io.kubernetes.client.openapi.models.V1ConfigMap;
23+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
24+
import io.kubernetes.client.util.ClientBuilder;
25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Paths;
28+
import java.util.HashMap;
29+
import org.junit.Before;
30+
import org.junit.Rule;
31+
import org.junit.Test;
32+
33+
public class KubectlCreateTest {
34+
35+
private static final String DISCOVERY_API = Resources.getResource("discovery-api.json").getPath();
36+
37+
private static final String DISCOVERY_APIV1 =
38+
Resources.getResource("discovery-api-v1.json").getPath();
39+
40+
private static final String DISCOVERY_APIS =
41+
Resources.getResource("discovery-apis.json").getPath();
42+
43+
private ApiClient apiClient;
44+
45+
@Rule public WireMockRule wireMockRule = new WireMockRule(8384);
46+
47+
@Before
48+
public void setup() throws IOException {
49+
apiClient = new ClientBuilder().setBasePath("http://localhost:" + 8384).build();
50+
}
51+
52+
@Test
53+
public void testCreateConfigMap() throws KubectlException, IOException {
54+
wireMockRule.stubFor(
55+
post(urlPathEqualTo("/api/v1/namespaces/foo/configmaps"))
56+
.willReturn(
57+
aResponse()
58+
.withStatus(200)
59+
.withBody("{\"metadata\":{\"name\":\"bar\",\"namespace\":\"foo\"}}")));
60+
wireMockRule.stubFor(
61+
get(urlPathEqualTo("/api"))
62+
.willReturn(
63+
aResponse()
64+
.withStatus(200)
65+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API))))));
66+
wireMockRule.stubFor(
67+
get(urlPathEqualTo("/apis"))
68+
.willReturn(
69+
aResponse()
70+
.withStatus(200)
71+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS))))));
72+
wireMockRule.stubFor(
73+
get(urlPathEqualTo("/api/v1"))
74+
.willReturn(
75+
aResponse()
76+
.withStatus(200)
77+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1))))));
78+
79+
V1ConfigMap configMap =
80+
(V1ConfigMap)
81+
Kubectl.create()
82+
.apiClient(apiClient)
83+
.load(
84+
new V1ConfigMap()
85+
.metadata(new V1ObjectMeta().namespace("foo").name("bar"))
86+
.data(
87+
new HashMap<String, String>() {
88+
{
89+
put("key1", "value1");
90+
}
91+
}))
92+
.execute();
93+
wireMockRule.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/namespaces/foo/configmaps")));
94+
assertNotNull(configMap);
95+
}
96+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"kind":"APIResourceList","groupVersion":"v1","resources":[{"name":"bindings","singularName":"","namespaced":true,"kind":"Binding","verbs":["create"]},{"name":"componentstatuses","singularName":"","namespaced":false,"kind":"ComponentStatus","verbs":["get","list"],"shortNames":["cs"]},{"name":"configmaps","singularName":"","namespaced":true,"kind":"ConfigMap","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["cm"],"storageVersionHash":"qFsyl6wFWjQ="},{"name":"endpoints","singularName":"","namespaced":true,"kind":"Endpoints","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ep"],"storageVersionHash":"fWeeMqaN/OA="},{"name":"events","singularName":"","namespaced":true,"kind":"Event","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ev"],"storageVersionHash":"r2yiGXH7wu8="},{"name":"limitranges","singularName":"","namespaced":true,"kind":"LimitRange","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["limits"],"storageVersionHash":"EBKMFVe6cwo="},{"name":"namespaces","singularName":"","namespaced":false,"kind":"Namespace","verbs":["create","delete","get","list","patch","update","watch"],"shortNames":["ns"],"storageVersionHash":"Q3oi5N2YM8M="},{"name":"namespaces/finalize","singularName":"","namespaced":false,"kind":"Namespace","verbs":["update"]},{"name":"namespaces/status","singularName":"","namespaced":false,"kind":"Namespace","verbs":["get","patch","update"]},{"name":"nodes","singularName":"","namespaced":false,"kind":"Node","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["no"],"storageVersionHash":"XwShjMxG9Fs="},{"name":"nodes/proxy","singularName":"","namespaced":false,"kind":"NodeProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"nodes/status","singularName":"","namespaced":false,"kind":"Node","verbs":["get","patch","update"]},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"kind":"PersistentVolumeClaim","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["pvc"],"storageVersionHash":"QWTyNDq0dC4="},{"name":"persistentvolumeclaims/status","singularName":"","namespaced":true,"kind":"PersistentVolumeClaim","verbs":["get","patch","update"]},{"name":"persistentvolumes","singularName":"","namespaced":false,"kind":"PersistentVolume","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["pv"],"storageVersionHash":"HN/zwEC+JgM="},{"name":"persistentvolumes/status","singularName":"","namespaced":false,"kind":"PersistentVolume","verbs":["get","patch","update"]},{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["po"],"categories":["all"],"storageVersionHash":"xPOwRZ+Yhw8="},{"name":"pods/attach","singularName":"","namespaced":true,"kind":"PodAttachOptions","verbs":["create","get"]},{"name":"pods/binding","singularName":"","namespaced":true,"kind":"Binding","verbs":["create"]},{"name":"pods/eviction","singularName":"","namespaced":true,"group":"policy","version":"v1beta1","kind":"Eviction","verbs":["create"]},{"name":"pods/exec","singularName":"","namespaced":true,"kind":"PodExecOptions","verbs":["create","get"]},{"name":"pods/log","singularName":"","namespaced":true,"kind":"Pod","verbs":["get"]},{"name":"pods/portforward","singularName":"","namespaced":true,"kind":"PodPortForwardOptions","verbs":["create","get"]},{"name":"pods/proxy","singularName":"","namespaced":true,"kind":"PodProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"pods/status","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","patch","update"]},{"name":"podtemplates","singularName":"","namespaced":true,"kind":"PodTemplate","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"LIXB2x4IFpk="},{"name":"replicationcontrollers","singularName":"","namespaced":true,"kind":"ReplicationController","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["rc"],"categories":["all"],"storageVersionHash":"Jond2If31h0="},{"name":"replicationcontrollers/scale","singularName":"","namespaced":true,"group":"autoscaling","version":"v1","kind":"Scale","verbs":["get","patch","update"]},{"name":"replicationcontrollers/status","singularName":"","namespaced":true,"kind":"ReplicationController","verbs":["get","patch","update"]},{"name":"resourcequotas","singularName":"","namespaced":true,"kind":"ResourceQuota","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["quota"],"storageVersionHash":"8uhSgffRX6w="},{"name":"resourcequotas/status","singularName":"","namespaced":true,"kind":"ResourceQuota","verbs":["get","patch","update"]},{"name":"secrets","singularName":"","namespaced":true,"kind":"Secret","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"S6u1pOWzb84="},{"name":"serviceaccounts","singularName":"","namespaced":true,"kind":"ServiceAccount","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["sa"],"storageVersionHash":"pbx9ZvyFpBE="},{"name":"services","singularName":"","namespaced":true,"kind":"Service","verbs":["create","delete","get","list","patch","update","watch"],"shortNames":["svc"],"categories":["all"],"storageVersionHash":"0/CO1lhkEBI="},{"name":"services/proxy","singularName":"","namespaced":true,"kind":"ServiceProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"services/status","singularName":"","namespaced":true,"kind":"Service","verbs":["get","patch","update"]}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0","serverAddress":"192.168.65.3:6443"}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"kind":"APIGroupList","apiVersion":"v1","groups":[]}

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,18 @@ public static GroupVersionKind getGroupVersionKindByClass(Class<?> clazz) {
147147
* @throws ApiException the api exception
148148
*/
149149
public static void refresh(Discovery discovery) throws ApiException {
150-
// TODO(yue9944882): integration test it
151-
for (Discovery.APIResource apiResource : discovery.findAll()) {
150+
refresh(discovery.findAll());
151+
}
152+
153+
/**
154+
* Refreshes the model mapping by syncing up w/the api discovery info from the kubernetes
155+
* apiserver.
156+
*
157+
* @param apiResources the api resources
158+
* @throws ApiException the api exception
159+
*/
160+
public static void refresh(Set<Discovery.APIResource> apiResources) throws ApiException {
161+
for (Discovery.APIResource apiResource : apiResources) {
152162
for (String version : apiResource.getVersions()) {
153163
Class<?> clazz = getApiTypeClass(apiResource.getGroup(), version, apiResource.getKind());
154164
if (clazz == null) {
@@ -242,9 +252,21 @@ public GroupVersionKind(String group, String version, String kind) {
242252
this.kind = kind;
243253
}
244254

245-
private String group;
246-
private String version;
247-
private String kind;
255+
private final String group;
256+
private final String version;
257+
private final String kind;
258+
259+
public String getGroup() {
260+
return group;
261+
}
262+
263+
public String getVersion() {
264+
return version;
265+
}
266+
267+
public String getKind() {
268+
return kind;
269+
}
248270

249271
@Override
250272
public boolean equals(Object o) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.util;
14+
15+
/** Namespaces provides a set of helpers for operating namespaces. */
16+
public class Namespaces {
17+
18+
public static final String NAMESPACE_DEFAULT = "default";
19+
20+
public static final String NAMESPACE_KUBESYSTEM = "kube-system";
21+
}

util/src/main/java/io/kubernetes/client/util/generic/GenericKubernetesApi.java

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -398,34 +398,45 @@ public KubernetesApiResponse<ApiListType> list(String namespace, final ListOptio
398398
public KubernetesApiResponse<ApiType> create(ApiType object, final CreateOptions createOptions) {
399399
V1ObjectMeta objectMeta = object.getMetadata();
400400

401+
boolean isNamespaced = !Strings.isNullOrEmpty(objectMeta.getNamespace());
402+
if (isNamespaced) {
403+
return create(objectMeta.getNamespace(), object, createOptions);
404+
}
405+
401406
return executeCall(
402407
customObjectsApi.getApiClient(),
403408
apiTypeClass,
404409
() -> {
405410
// TODO(yue9944882): judge namespaced object via api discovery
406-
boolean isNamespaced = !Strings.isNullOrEmpty(objectMeta.getNamespace());
407-
if (isNamespaced) {
408-
return customObjectsApi.createNamespacedCustomObjectCall(
409-
this.apiGroup,
410-
this.apiVersion,
411-
objectMeta.getNamespace(),
412-
this.resourcePlural,
413-
object,
414-
null,
415-
createOptions.getDryRun(),
416-
createOptions.getFieldManager(),
417-
null);
418-
} else {
419-
return customObjectsApi.createClusterCustomObjectCall(
420-
this.apiGroup,
421-
this.apiVersion,
422-
this.resourcePlural,
423-
object,
424-
null,
425-
createOptions.getDryRun(),
426-
createOptions.getFieldManager(),
427-
null);
428-
}
411+
return customObjectsApi.createClusterCustomObjectCall(
412+
this.apiGroup,
413+
this.apiVersion,
414+
this.resourcePlural,
415+
object,
416+
null,
417+
createOptions.getDryRun(),
418+
createOptions.getFieldManager(),
419+
null);
420+
});
421+
}
422+
423+
public KubernetesApiResponse<ApiType> create(
424+
String namespace, ApiType object, final CreateOptions createOptions) {
425+
return executeCall(
426+
customObjectsApi.getApiClient(),
427+
apiTypeClass,
428+
() -> {
429+
// TODO(yue9944882): judge namespaced object via api discovery
430+
return customObjectsApi.createNamespacedCustomObjectCall(
431+
this.apiGroup,
432+
this.apiVersion,
433+
namespace,
434+
this.resourcePlural,
435+
object,
436+
null,
437+
createOptions.getDryRun(),
438+
createOptions.getFieldManager(),
439+
null);
429440
});
430441
}
431442

0 commit comments

Comments
 (0)