Skip to content

Commit b0ea48e

Browse files
authored
Merge pull request #1128 from yue9944882/kubectl-apply
Kubectl Apply and discovery caching - server-side-apply only
2 parents 17a2012 + cdd4ce9 commit b0ea48e

File tree

14 files changed

+483
-66
lines changed

14 files changed

+483
-66
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.kubernetes.client.e2e.kubectl
2+
3+
import io.kubernetes.client.extended.kubectl.Kubectl
4+
import io.kubernetes.client.openapi.models.V1Namespace
5+
import io.kubernetes.client.openapi.models.V1ObjectMeta
6+
import io.kubernetes.client.util.ClientBuilder
7+
import spock.lang.Specification
8+
9+
class KubectlApplyTest extends Specification {
10+
11+
def "Kubectl apply namespace should work"() {
12+
given:
13+
def apiClient = ClientBuilder.defaultClient();
14+
apiClient.setDebugging(true)
15+
def appliedNamespace = Kubectl.apply()
16+
.apiClient(apiClient)
17+
.resource(new V1Namespace()
18+
.apiVersion("v1")
19+
.kind("Namespace")
20+
.metadata(new V1ObjectMeta().name("apply-foo")))
21+
.execute()
22+
expect:
23+
appliedNamespace != null
24+
}
25+
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.kubernetes.client.e2e.kubectl
2+
3+
import io.kubernetes.client.extended.kubectl.Kubectl
4+
import io.kubernetes.client.openapi.models.V1Namespace
5+
import io.kubernetes.client.openapi.models.V1ObjectMeta
6+
import io.kubernetes.client.util.ClientBuilder
7+
import spock.lang.Specification
8+
9+
class KubectlCreateTest extends Specification {
10+
11+
def "Kubectl create namespace should work"() {
12+
given:
13+
def apiClient = ClientBuilder.defaultClient();
14+
def createdNamespace = Kubectl.create()
15+
.apiClient(apiClient)
16+
.resource(new V1Namespace()
17+
.apiVersion("v1")
18+
.metadata(new V1ObjectMeta().name("create-foo")))
19+
.execute()
20+
expect:
21+
createdNamespace != null
22+
}
23+
24+
}

extended/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
<groupId>com.github.vladimir-bukhtoyarov</groupId>
4343
<artifactId>bucket4j-core</artifactId>
4444
</dependency>
45+
<dependency>
46+
<groupId>com.flipkart.zjsonpatch</groupId>
47+
<artifactId>zjsonpatch</artifactId>
48+
</dependency>
4549
<!-- test dependencies -->
4650
<dependency>
4751
<groupId>junit</groupId>

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212
*/
1313
package io.kubernetes.client.extended.kubectl;
1414

15+
import io.kubernetes.client.Discovery;
16+
import io.kubernetes.client.apimachinery.GroupVersionKind;
1517
import io.kubernetes.client.common.KubernetesObject;
1618
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
1719
import io.kubernetes.client.openapi.ApiClient;
20+
import io.kubernetes.client.openapi.ApiException;
1821
import io.kubernetes.client.openapi.Configuration;
22+
import io.kubernetes.client.util.ModelMapper;
23+
import java.util.Optional;
24+
import java.util.Set;
1925

2026
/**
2127
* Kubectl provides a set of helper functions that has the same functionalities as corresponding
@@ -46,6 +52,14 @@ public static KubectlCreate create() {
4652
return new KubectlCreate();
4753
}
4854

55+
/**
56+
* Equivalent for `kubectl apply`
57+
*
58+
* @return the kubectl create
59+
*/
60+
public static KubectlApply apply() {
61+
return new KubectlApply();
62+
}
4963
/**
5064
* Equivalent for `kubectl top`
5165
*
@@ -172,6 +186,26 @@ public T namespace(String namespace) {
172186
abstract static class ApiClientBuilder<T extends ApiClientBuilder> {
173187
ApiClient apiClient = Configuration.getDefaultApiClient();
174188

189+
protected Discovery.APIResource recognize(KubernetesObject object)
190+
throws KubectlException, ApiException {
191+
Discovery discovery = new Discovery(apiClient);
192+
193+
Set<Discovery.APIResource> apiResources = ModelMapper.refresh(discovery);
194+
195+
GroupVersionKind gvk = ModelMapper.getGroupVersionKindByClass(object.getClass());
196+
197+
Optional<Discovery.APIResource> apiResource =
198+
apiResources.stream()
199+
.filter(r -> r.getKind().equals(gvk.getKind()) && r.getGroup().equals(gvk.getGroup()))
200+
.findFirst();
201+
if (!apiResource.isPresent()) {
202+
throw new KubectlException(
203+
"Cannot recognize such kubernetes resource from api-discovery: " + gvk.toString());
204+
}
205+
206+
return apiResource.get();
207+
}
208+
175209
public T apiClient(ApiClient apiClient) {
176210
this.apiClient = apiClient;
177211
return (T) this;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.GroupVersion;
18+
import io.kubernetes.client.common.KubernetesListObject;
19+
import io.kubernetes.client.common.KubernetesObject;
20+
import io.kubernetes.client.custom.V1Patch;
21+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
22+
import io.kubernetes.client.openapi.ApiException;
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.PatchOptions;
27+
28+
public class KubectlApply extends Kubectl.NamespacedApiClientBuilder<KubectlApply>
29+
implements Kubectl.Executable<KubernetesObject> {
30+
31+
private KubernetesObject targetObj;
32+
private String fieldManager;
33+
private boolean forceConflict;
34+
35+
public static final String DEFAULT_FIELD_MANAGER = "kubernetes-java-kubectl-apply";
36+
37+
KubectlApply() {
38+
this.forceConflict = false;
39+
this.fieldManager = DEFAULT_FIELD_MANAGER;
40+
}
41+
42+
public KubectlApply fieldManager(String fieldManager) {
43+
this.fieldManager = fieldManager;
44+
return this;
45+
}
46+
47+
public KubectlApply forceConflict(boolean isForceConflict) {
48+
this.forceConflict = isForceConflict;
49+
return this;
50+
}
51+
52+
public KubectlApply resource(KubernetesObject obj) {
53+
this.targetObj = obj;
54+
return this;
55+
}
56+
57+
private void validate() throws KubectlException {
58+
if (Strings.isNullOrEmpty(fieldManager)) {
59+
throw new KubectlException("Field-manager must not be empty for server-side-apply");
60+
}
61+
}
62+
63+
@Override
64+
public KubernetesObject execute() throws KubectlException {
65+
validate();
66+
return executeServerSideApply();
67+
}
68+
69+
private KubernetesObject executeServerSideApply() throws KubectlException {
70+
try {
71+
Discovery.APIResource apiResource = recognize(this.targetObj);
72+
GroupVersion gv = GroupVersion.parse(this.targetObj);
73+
74+
GenericKubernetesApi<KubernetesObject, KubernetesListObject> api =
75+
new GenericKubernetesApi(
76+
targetObj.getClass(),
77+
KubernetesListObject.class,
78+
gv.getGroup(),
79+
gv.getVersion(),
80+
apiResource.getResourcePlural(),
81+
apiClient);
82+
83+
PatchOptions patchOptions = new PatchOptions();
84+
patchOptions.setForce(this.forceConflict);
85+
patchOptions.setFieldManager(this.fieldManager);
86+
87+
if (apiResource.getNamespaced()) {
88+
String targetNamespace =
89+
namespace != null
90+
? namespace
91+
: Strings.isNullOrEmpty(targetObj.getMetadata().getNamespace())
92+
? Namespaces.NAMESPACE_DEFAULT
93+
: targetObj.getMetadata().getNamespace();
94+
95+
KubernetesApiResponse<KubernetesObject> response =
96+
api.patch(
97+
targetNamespace,
98+
targetObj.getMetadata().getName(),
99+
V1Patch.PATCH_FORMAT_APPLY_YAML,
100+
new V1Patch(apiClient.getJSON().serialize(targetObj)),
101+
patchOptions);
102+
return response.getObject();
103+
} else {
104+
KubernetesApiResponse<KubernetesObject> response =
105+
api.patch(
106+
targetObj.getMetadata().getName(),
107+
V1Patch.PATCH_FORMAT_APPLY_YAML,
108+
new V1Patch(apiClient.getJSON().serialize(targetObj)),
109+
patchOptions);
110+
return response.getObject();
111+
}
112+
} catch (ApiException e) {
113+
throw new KubectlException(e);
114+
}
115+
}
116+
}

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

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@
1414

1515
import com.google.common.base.Strings;
1616
import io.kubernetes.client.Discovery;
17-
import io.kubernetes.client.apimachinery.GroupVersionKind;
17+
import io.kubernetes.client.apimachinery.GroupVersion;
1818
import io.kubernetes.client.common.KubernetesListObject;
1919
import io.kubernetes.client.common.KubernetesObject;
2020
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
2121
import io.kubernetes.client.openapi.ApiException;
22-
import io.kubernetes.client.util.ModelMapper;
2322
import io.kubernetes.client.util.Namespaces;
2423
import io.kubernetes.client.util.generic.GenericKubernetesApi;
2524
import io.kubernetes.client.util.generic.KubernetesApiResponse;
2625
import io.kubernetes.client.util.generic.options.CreateOptions;
27-
import java.util.Optional;
28-
import java.util.Set;
2926

3027
public class KubectlCreate extends Kubectl.NamespacedApiClientBuilder<KubectlCreate>
3128
implements Kubectl.Executable<KubernetesObject> {
@@ -41,30 +38,19 @@ public KubectlCreate resource(KubernetesObject obj) {
4138

4239
@Override
4340
public KubernetesObject execute() throws KubectlException {
44-
Discovery discovery = new Discovery(apiClient);
4541
try {
46-
// TODO(yue9944882): caches api discovery in memory
47-
Set<Discovery.APIResource> apiResources = discovery.findAll();
48-
ModelMapper.refresh(apiResources);
42+
Discovery.APIResource apiResource = recognize(this.targetObj);
43+
GroupVersion gv = GroupVersion.parse(this.targetObj);
4944

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-
}
5945
GenericKubernetesApi<KubernetesObject, KubernetesListObject> api =
6046
new GenericKubernetesApi(
6147
targetObj.getClass(),
6248
KubernetesListObject.class,
63-
gvk.getGroup(),
64-
gvk.getVersion(),
65-
apiResource.get().getResourcePlural(),
49+
gv.getGroup(),
50+
gv.getVersion(),
51+
apiResource.getResourcePlural(),
6652
apiClient);
67-
if (apiResource.get().getNamespaced()) {
53+
if (apiResource.getNamespaced()) {
6854
String targetNamespace =
6955
namespace != null
7056
? namespace
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
17+
import static org.junit.Assert.*;
18+
19+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
20+
import com.github.tomakehurst.wiremock.matching.EqualToPattern;
21+
import com.google.common.io.Resources;
22+
import io.kubernetes.client.custom.V1Patch;
23+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
24+
import io.kubernetes.client.openapi.ApiClient;
25+
import io.kubernetes.client.openapi.models.V1ConfigMap;
26+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
27+
import io.kubernetes.client.util.ClientBuilder;
28+
import java.io.IOException;
29+
import java.nio.file.Files;
30+
import java.nio.file.Paths;
31+
import java.util.HashMap;
32+
import org.junit.Before;
33+
import org.junit.Rule;
34+
import org.junit.Test;
35+
36+
public class KubectlApplyTest {
37+
38+
private static final String DISCOVERY_API = Resources.getResource("discovery-api.json").getPath();
39+
40+
private static final String DISCOVERY_APIV1 =
41+
Resources.getResource("discovery-api-v1.json").getPath();
42+
43+
private static final String DISCOVERY_APIS =
44+
Resources.getResource("discovery-apis.json").getPath();
45+
46+
private ApiClient apiClient;
47+
48+
@Rule public WireMockRule wireMockRule = new WireMockRule(8384);
49+
50+
@Before
51+
public void setup() throws IOException {
52+
apiClient = new ClientBuilder().setBasePath("http://localhost:" + 8384).build();
53+
}
54+
55+
@Test
56+
public void testApplyConfigMap() throws KubectlException, IOException {
57+
wireMockRule.stubFor(
58+
patch(urlPathEqualTo("/api/v1/namespaces/foo/configmaps/bar"))
59+
.withHeader("Content-Type", new EqualToPattern(V1Patch.PATCH_FORMAT_APPLY_YAML))
60+
.willReturn(
61+
aResponse()
62+
.withStatus(200)
63+
.withBody("{\"metadata\":{\"name\":\"bar\",\"namespace\":\"foo\"}}")));
64+
wireMockRule.stubFor(
65+
get(urlPathEqualTo("/api"))
66+
.willReturn(
67+
aResponse()
68+
.withStatus(200)
69+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API))))));
70+
wireMockRule.stubFor(
71+
get(urlPathEqualTo("/apis"))
72+
.willReturn(
73+
aResponse()
74+
.withStatus(200)
75+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS))))));
76+
wireMockRule.stubFor(
77+
get(urlPathEqualTo("/api/v1"))
78+
.willReturn(
79+
aResponse()
80+
.withStatus(200)
81+
.withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1))))));
82+
83+
V1ConfigMap configMap =
84+
(V1ConfigMap)
85+
Kubectl.apply()
86+
.apiClient(apiClient)
87+
.resource(
88+
new V1ConfigMap()
89+
.apiVersion("v1")
90+
.metadata(new V1ObjectMeta().namespace("foo").name("bar"))
91+
.data(
92+
new HashMap<String, String>() {
93+
{
94+
put("key1", "value1");
95+
}
96+
}))
97+
.execute();
98+
wireMockRule.verify(
99+
1, patchRequestedFor(urlPathEqualTo("/api/v1/namespaces/foo/configmaps/bar")));
100+
assertNotNull(configMap);
101+
}
102+
}

0 commit comments

Comments
 (0)