Skip to content

Commit a5e5308

Browse files
authored
Merge pull request #1187 from yue9944882/kubectl-create
Equivalent for kubectl create
2 parents 09a9410 + 75f9d84 commit a5e5308

File tree

11 files changed

+307
-89
lines changed

11 files changed

+307
-89
lines changed

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ public static KubectlCordon uncordon() {
3333
return new KubectlCordon(false);
3434
}
3535

36+
/**
37+
* Equivalent for `kubectl create`
38+
*
39+
* @return the kubectl create
40+
*/
41+
public static KubectlCreate create() {
42+
return new KubectlCreate();
43+
}
44+
3645
/**
3746
* Equivalent for `kubectl top`
3847
*
@@ -146,8 +155,17 @@ public static interface Executable<OUTPUT> {
146155
OUTPUT execute() throws KubectlException;
147156
}
148157

149-
abstract static class ApiClientBuilder<T extends ApiClientBuilder> {
158+
abstract static class NamespacedApiClientBuilder<T extends NamespacedApiClientBuilder>
159+
extends ApiClientBuilder<T> {
160+
String namespace;
161+
162+
public T namespace(String namespace) {
163+
this.namespace = namespace;
164+
return (T) this;
165+
}
166+
}
150167

168+
abstract static class ApiClientBuilder<T extends ApiClientBuilder> {
151169
ApiClient apiClient = Configuration.getDefaultApiClient();
152170

153171
public T apiClient(ApiClient apiClient) {
@@ -158,9 +176,8 @@ public T apiClient(ApiClient apiClient) {
158176

159177
abstract static class ResourceBuilder<
160178
ApiType extends KubernetesObject, T extends ResourceBuilder<ApiType, T>>
161-
extends ApiClientBuilder<T> {
179+
extends NamespacedApiClientBuilder<T> {
162180
final Class<ApiType> apiTypeClass;
163-
String namespace;
164181
String name;
165182
String apiGroup;
166183
String apiVersion;
@@ -175,11 +192,6 @@ public T name(String name) {
175192
return (T) this;
176193
}
177194

178-
public T namespace(String namespace) {
179-
this.namespace = namespace;
180-
return (T) this;
181-
}
182-
183195
public T apiGroup(String apiGroup) {
184196
this.apiGroup = apiGroup;
185197
return (T) this;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.apimachinery.GroupVersionKind;
18+
import io.kubernetes.client.common.KubernetesListObject;
19+
import io.kubernetes.client.common.KubernetesObject;
20+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
21+
import io.kubernetes.client.openapi.ApiException;
22+
import io.kubernetes.client.util.ModelMapper;
23+
import io.kubernetes.client.util.Namespaces;
24+
import io.kubernetes.client.util.generic.GenericKubernetesApi;
25+
import io.kubernetes.client.util.generic.KubernetesApiResponse;
26+
import io.kubernetes.client.util.generic.options.CreateOptions;
27+
import java.util.Optional;
28+
import java.util.Set;
29+
30+
public class KubectlCreate extends Kubectl.NamespacedApiClientBuilder<KubectlCreate>
31+
implements Kubectl.Executable<KubernetesObject> {
32+
33+
KubectlCreate() {}
34+
35+
private KubernetesObject targetObj;
36+
37+
public KubectlCreate resource(KubernetesObject obj) {
38+
this.targetObj = obj;
39+
return this;
40+
}
41+
42+
@Override
43+
public KubernetesObject execute() throws KubectlException {
44+
Discovery discovery = new Discovery(apiClient);
45+
try {
46+
// TODO(yue9944882): caches api discovery in memory
47+
Set<Discovery.APIResource> apiResources = discovery.findAll();
48+
ModelMapper.refresh(apiResources);
49+
50+
GroupVersionKind gvk = ModelMapper.getGroupVersionKindByClass(targetObj.getClass());
51+
Optional<Discovery.APIResource> apiResource =
52+
apiResources.stream()
53+
.filter(r -> r.getKind().equals(gvk.getKind()) && r.getGroup().equals(gvk.getGroup()))
54+
.findFirst();
55+
if (!apiResource.isPresent()) {
56+
throw new KubectlException(
57+
"Cannot recognize such kubernetes resource from api-discovery: " + gvk.toString());
58+
}
59+
GenericKubernetesApi<KubernetesObject, KubernetesListObject> api =
60+
new GenericKubernetesApi(
61+
targetObj.getClass(),
62+
KubernetesListObject.class,
63+
gvk.getGroup(),
64+
gvk.getVersion(),
65+
apiResource.get().getResourcePlural(),
66+
apiClient);
67+
if (apiResource.get().getNamespaced()) {
68+
String targetNamespace =
69+
namespace != null
70+
? namespace
71+
: Strings.isNullOrEmpty(targetObj.getMetadata().getNamespace())
72+
? Namespaces.NAMESPACE_DEFAULT
73+
: targetObj.getMetadata().getNamespace();
74+
75+
KubernetesApiResponse<KubernetesObject> response =
76+
api.create(targetNamespace, targetObj, new CreateOptions());
77+
return response.getObject();
78+
} else {
79+
KubernetesApiResponse<KubernetesObject> response =
80+
api.create(targetObj, new CreateOptions());
81+
return response.getObject();
82+
}
83+
} catch (ApiException e) {
84+
throw new KubectlException(e);
85+
}
86+
}
87+
}
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+
.resource(
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/apimachinery/GroupVersionKind.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
package io.kubernetes.client.apimachinery;
1414

15+
import com.google.common.base.Objects;
16+
1517
public class GroupVersionKind {
1618

1719
private final String group;
@@ -35,4 +37,19 @@ public String getVersion() {
3537
public String getKind() {
3638
return kind;
3739
}
40+
41+
@Override
42+
public boolean equals(Object o) {
43+
if (this == o) return true;
44+
if (o == null || getClass() != o.getClass()) return false;
45+
GroupVersionKind that = (GroupVersionKind) o;
46+
return Objects.equal(group, that.group)
47+
&& Objects.equal(version, that.version)
48+
&& Objects.equal(kind, that.kind);
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return Objects.hashCode(group, version, kind);
54+
}
3855
}

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

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
*/
1313
package io.kubernetes.client.util;
1414

15-
import com.google.common.base.Objects;
1615
import com.google.common.collect.ImmutableMap;
1716
import com.google.common.reflect.ClassPath;
1817
import io.kubernetes.client.Discovery;
18+
import io.kubernetes.client.apimachinery.GroupVersionKind;
1919
import io.kubernetes.client.common.KubernetesListObject;
2020
import io.kubernetes.client.common.KubernetesObject;
2121
import io.kubernetes.client.openapi.ApiException;
@@ -142,12 +142,26 @@ public static GroupVersionKind getGroupVersionKindByClass(Class<?> clazz) {
142142
* Refreshes the model mapping by syncing up w/the api discovery info from the kubernetes
143143
* apiserver.
144144
*
145+
* <p>Note: if model mappings can be incomplete if this method is never called.
146+
*
145147
* @param discovery the discovery
146148
* @throws ApiException the api exception
147149
*/
148150
public static void refresh(Discovery discovery) throws ApiException {
149-
// TODO(yue9944882): integration test it
150-
for (Discovery.APIResource apiResource : discovery.findAll()) {
151+
refresh(discovery.findAll());
152+
}
153+
154+
/**
155+
* Refreshes the model mapping by syncing up w/the api discovery info from the kubernetes
156+
* apiserver.
157+
*
158+
* <p>Note: if model mappings can be incomplete if this method is never called.
159+
*
160+
* @param apiResources the api resources
161+
* @throws ApiException the api exception
162+
*/
163+
public static void refresh(Set<Discovery.APIResource> apiResources) {
164+
for (Discovery.APIResource apiResource : apiResources) {
151165
for (String version : apiResource.getVersions()) {
152166
Class<?> clazz = getApiTypeClass(apiResource.getGroup(), version, apiResource.getKind());
153167
if (clazz == null) {
@@ -232,47 +246,4 @@ private static Pair<String, String> getApiVersion(String name) {
232246
.findFirst()
233247
.orElse(new MutablePair(null, name));
234248
}
235-
236-
public static class GroupVersionKind {
237-
238-
public GroupVersionKind(String group, String version, String kind) {
239-
this.group = group;
240-
this.version = version;
241-
this.kind = kind;
242-
}
243-
244-
private String group;
245-
private String version;
246-
private String kind;
247-
248-
@Override
249-
public boolean equals(Object o) {
250-
if (this == o) return true;
251-
if (o == null || getClass() != o.getClass()) return false;
252-
GroupVersionKind that = (GroupVersionKind) o;
253-
return Objects.equal(group, that.group)
254-
&& Objects.equal(version, that.version)
255-
&& Objects.equal(kind, that.kind);
256-
}
257-
258-
@Override
259-
public int hashCode() {
260-
return Objects.hashCode(group, version, kind);
261-
}
262-
263-
@Override
264-
public String toString() {
265-
return "GroupVersionKind{"
266-
+ "group='"
267-
+ group
268-
+ '\''
269-
+ ", version='"
270-
+ version
271-
+ '\''
272-
+ ", kind='"
273-
+ kind
274-
+ '\''
275-
+ '}';
276-
}
277-
}
278249
}
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+
}

0 commit comments

Comments
 (0)