Skip to content

Commit 17303db

Browse files
authored
Merge pull request #1126 from yue9944882/kubectl-annotate
Kubectl annotate implementation
2 parents f3a7126 + bb6e7c9 commit 17303db

File tree

4 files changed

+395
-0
lines changed

4 files changed

+395
-0
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ public static <ApiType extends KubernetesObject> KubectlLabel<ApiType> label(
4848
return new KubectlLabel<>(apiClient, apiTypeClass);
4949
}
5050

51+
/**
52+
* Equivalence for `kubectl annotate`.
53+
*
54+
* @param <ApiType> the target api type
55+
* @param apiTypeClass the api type class
56+
* @return the kubectl annotation
57+
*/
58+
public static <ApiType extends KubernetesObject> KubectlAnnotate<ApiType> annotate(
59+
Class<ApiType> apiTypeClass) {
60+
return annotate(Configuration.getDefaultApiClient(), apiTypeClass);
61+
}
62+
63+
/**
64+
* Equivalence for `kubectl annotate`.
65+
*
66+
* @param <ApiType> the target api type
67+
* @param apiClient the api client instance
68+
* @param apiTypeClass the api type class
69+
* @return the kubectl annotation
70+
*/
71+
public static <ApiType extends KubernetesObject> KubectlAnnotate<ApiType> annotate(
72+
ApiClient apiClient, Class<ApiType> apiTypeClass) {
73+
return new KubectlAnnotate<>(apiClient, apiTypeClass);
74+
}
75+
5176
/**
5277
* Equivalence for `kubectl version`.
5378
*
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 io.kubernetes.client.common.KubernetesListObject;
16+
import io.kubernetes.client.common.KubernetesObject;
17+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
18+
import io.kubernetes.client.openapi.ApiClient;
19+
import io.kubernetes.client.util.annotations.Annotations;
20+
import io.kubernetes.client.util.generic.GenericKubernetesApi;
21+
import io.kubernetes.client.util.generic.KubernetesApiResponse;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import org.apache.commons.lang.StringUtils;
25+
26+
public class KubectlAnnotate<ApiType extends KubernetesObject>
27+
extends Kubectl.ResourceBuilder<KubectlAnnotate<ApiType>>
28+
implements Kubectl.Executable<ApiType> {
29+
30+
private final ApiClient apiClient;
31+
private final Class<ApiType> apiTypeClass;
32+
private final Map<String, String> addingAnnotations;
33+
34+
private String apiGroup;
35+
private String apiVersion;
36+
private String resourceNamePlural;
37+
38+
KubectlAnnotate(ApiClient apiClient, Class<ApiType> apiTypeClass) {
39+
super(apiClient, apiTypeClass);
40+
this.addingAnnotations = new HashMap<>();
41+
this.apiTypeClass = apiTypeClass;
42+
this.apiClient = apiClient;
43+
}
44+
45+
public KubectlAnnotate<ApiType> apiGroup(String apiGroup) {
46+
this.apiGroup = apiGroup;
47+
return this;
48+
}
49+
50+
public KubectlAnnotate<ApiType> apiVersion(String apiVersion) {
51+
this.apiVersion = apiVersion;
52+
return this;
53+
}
54+
55+
public KubectlAnnotate<ApiType> resourceNamePlural(String resourceNamePlural) {
56+
this.resourceNamePlural = resourceNamePlural;
57+
return this;
58+
}
59+
60+
public KubectlAnnotate<ApiType> addAnnotation(String key, String value) {
61+
this.addingAnnotations.put(key, value);
62+
return this;
63+
}
64+
65+
private boolean isNamespaced() {
66+
return !StringUtils.isEmpty(namespace);
67+
}
68+
69+
@Override
70+
public ApiType execute() throws KubectlException {
71+
verifyArguments();
72+
GenericKubernetesApi<ApiType, KubernetesListObject> api =
73+
new GenericKubernetesApi<>(
74+
apiTypeClass,
75+
KubernetesListObject.class,
76+
apiGroup,
77+
apiVersion,
78+
resourceNamePlural,
79+
apiClient);
80+
81+
try {
82+
final KubernetesApiResponse<ApiType> getResponse;
83+
if (isNamespaced()) {
84+
getResponse = api.get(namespace, name);
85+
} else {
86+
getResponse = api.get(name);
87+
}
88+
if (!getResponse.isSuccess()) {
89+
throw new KubectlException(getResponse.getStatus());
90+
}
91+
ApiType obj = getResponse.getObject();
92+
93+
Annotations.addAnnotations(obj, addingAnnotations);
94+
95+
final KubernetesApiResponse<ApiType> updateResponse;
96+
updateResponse = api.update(obj);
97+
if (!updateResponse.isSuccess()) {
98+
throw new KubectlException(updateResponse.getStatus());
99+
}
100+
return updateResponse.getObject();
101+
} catch (Throwable t) {
102+
throw new KubectlException(t);
103+
}
104+
}
105+
106+
private void verifyArguments() throws KubectlException {
107+
if (null == apiGroup) {
108+
throw new KubectlException("missing apiGroup argument");
109+
}
110+
if (null == apiVersion) {
111+
throw new KubectlException("missing apiVersion argument");
112+
}
113+
if (null == resourceNamePlural) {
114+
throw new KubectlException("missing resourceNamePlural argument");
115+
}
116+
if (null == name) {
117+
throw new KubectlException("missing name argument");
118+
}
119+
}
120+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+
import static org.junit.Assert.assertThrows;
18+
19+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
20+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
21+
import io.kubernetes.client.openapi.ApiClient;
22+
import io.kubernetes.client.openapi.models.V1Node;
23+
import io.kubernetes.client.openapi.models.V1Pod;
24+
import io.kubernetes.client.util.ClientBuilder;
25+
import java.io.IOException;
26+
import org.junit.Before;
27+
import org.junit.Rule;
28+
import org.junit.Test;
29+
30+
public class KubectlAnnotateTest {
31+
32+
private ApiClient apiClient;
33+
34+
@Rule public WireMockRule wireMockRule = new WireMockRule(8384);
35+
36+
@Before
37+
public void setup() throws IOException {
38+
apiClient = new ClientBuilder().setBasePath("http://localhost:" + 8384).build();
39+
}
40+
41+
@Test
42+
public void testKubectlAnnotateNamespacedResourceShouldWork() throws KubectlException {
43+
wireMockRule.stubFor(
44+
get(urlPathEqualTo("/api/v1/namespaces/default/pods/foo"))
45+
.willReturn(
46+
aResponse()
47+
.withStatus(200)
48+
.withBody("{\"metadata\":{\"name\":\"foo\",\"namespace\":\"default\"}}")));
49+
wireMockRule.stubFor(
50+
put(urlPathEqualTo("/api/v1/namespaces/default/pods/foo"))
51+
.willReturn(
52+
aResponse()
53+
.withStatus(200)
54+
.withBody("{\"metadata\":{\"name\":\"foo\",\"namespace\":\"default\"}}")));
55+
V1Pod annotatedPod =
56+
Kubectl.annotate(apiClient, V1Pod.class)
57+
.apiGroup("")
58+
.apiVersion("v1")
59+
.resourceNamePlural("pods")
60+
.namespace("default")
61+
.name("foo")
62+
.addAnnotation("k1", "v1")
63+
.addAnnotation("k2", "v2")
64+
.execute();
65+
wireMockRule.verify(1, getRequestedFor(urlPathEqualTo("/api/v1/namespaces/default/pods/foo")));
66+
wireMockRule.verify(1, putRequestedFor(urlPathEqualTo("/api/v1/namespaces/default/pods/foo")));
67+
assertNotNull(annotatedPod);
68+
}
69+
70+
@Test
71+
public void testKubectlAnnotateNamespacedResourceReceiveForbiddenShouldThrowException()
72+
throws KubectlException {
73+
wireMockRule.stubFor(
74+
get(urlPathEqualTo("/api/v1/namespaces/default/pods/foo"))
75+
.willReturn(
76+
aResponse()
77+
.withStatus(200)
78+
.withBody("{\"metadata\":{\"name\":\"foo\",\"namespace\":\"default\"}}")));
79+
wireMockRule.stubFor(
80+
put(urlPathEqualTo("/api/v1/namespaces/default/pods/foo"))
81+
.willReturn(aResponse().withStatus(403).withBody("{\"metadata\":{}}")));
82+
assertThrows(
83+
KubectlException.class,
84+
() -> {
85+
Kubectl.annotate(apiClient, V1Pod.class)
86+
.apiGroup("")
87+
.apiVersion("v1")
88+
.resourceNamePlural("pods")
89+
.namespace("default")
90+
.name("foo")
91+
.addAnnotation("k1", "v1")
92+
.addAnnotation("k2", "v2")
93+
.execute();
94+
});
95+
wireMockRule.verify(1, getRequestedFor(urlPathEqualTo("/api/v1/namespaces/default/pods/foo")));
96+
wireMockRule.verify(1, putRequestedFor(urlPathEqualTo("/api/v1/namespaces/default/pods/foo")));
97+
}
98+
99+
@Test
100+
public void testKubectlAnnotateClusterResourceShouldWork() throws KubectlException {
101+
wireMockRule.stubFor(
102+
get(urlPathEqualTo("/api/v1/nodes/foo"))
103+
.willReturn(aResponse().withStatus(200).withBody("{\"metadata\":{\"name\":\"foo\"}}")));
104+
wireMockRule.stubFor(
105+
put(urlPathEqualTo("/api/v1/nodes/foo"))
106+
.willReturn(aResponse().withStatus(200).withBody("{\"metadata\":{\"name\":\"foo\"}}")));
107+
V1Node annotatedNode =
108+
Kubectl.annotate(apiClient, V1Node.class)
109+
.apiGroup("")
110+
.apiVersion("v1")
111+
.resourceNamePlural("nodes")
112+
.name("foo")
113+
.addAnnotation("k1", "v1")
114+
.addAnnotation("k2", "v2")
115+
.execute();
116+
wireMockRule.verify(1, getRequestedFor(urlPathEqualTo("/api/v1/nodes/foo")));
117+
wireMockRule.verify(1, putRequestedFor(urlPathEqualTo("/api/v1/nodes/foo")));
118+
assertNotNull(annotatedNode);
119+
}
120+
121+
@Test
122+
public void testKubectlAnnotateClusterResourceReceiveForbiddenShouldThrowException()
123+
throws KubectlException {
124+
wireMockRule.stubFor(
125+
get(urlPathEqualTo("/api/v1/nodes/foo"))
126+
.willReturn(aResponse().withStatus(200).withBody("{\"metadata\":{\"name\":\"foo\"}}")));
127+
wireMockRule.stubFor(
128+
put(urlPathEqualTo("/api/v1/nodes/foo"))
129+
.willReturn(aResponse().withStatus(403).withBody("{\"metadata\":{\"name\":\"foo\"}}")));
130+
assertThrows(
131+
KubectlException.class,
132+
() -> {
133+
Kubectl.annotate(apiClient, V1Node.class)
134+
.apiGroup("")
135+
.apiVersion("v1")
136+
.resourceNamePlural("nodes")
137+
.name("foo")
138+
.addAnnotation("k1", "v1")
139+
.addAnnotation("k2", "v2")
140+
.execute();
141+
});
142+
wireMockRule.verify(1, getRequestedFor(urlPathEqualTo("/api/v1/nodes/foo")));
143+
wireMockRule.verify(1, putRequestedFor(urlPathEqualTo("/api/v1/nodes/foo")));
144+
}
145+
146+
@Test
147+
public void testMissingArgumentsShouldFail() throws KubectlException {
148+
assertThrows(
149+
KubectlException.class,
150+
() -> {
151+
Kubectl.annotate(apiClient, V1Node.class)
152+
// .apiGroup("") # missing apiGroup
153+
.apiVersion("v1")
154+
.resourceNamePlural("nodes")
155+
.name("foo")
156+
.addAnnotation("k1", "v1")
157+
.addAnnotation("k2", "v2")
158+
.execute();
159+
});
160+
assertThrows(
161+
KubectlException.class,
162+
() -> {
163+
Kubectl.annotate(apiClient, V1Node.class)
164+
.apiGroup("")
165+
// .apiVersion("v1") # missing apiVersion
166+
.resourceNamePlural("nodes")
167+
.name("foo")
168+
.addAnnotation("k1", "v1")
169+
.addAnnotation("k2", "v2")
170+
.execute();
171+
});
172+
assertThrows(
173+
KubectlException.class,
174+
() -> {
175+
Kubectl.annotate(apiClient, V1Node.class)
176+
.apiGroup("")
177+
.apiVersion("v1")
178+
// .resourceNamePlural("nodes") # missing resourceNamePlural
179+
.name("foo")
180+
.addAnnotation("k1", "v1")
181+
.addAnnotation("k2", "v2")
182+
.execute();
183+
});
184+
assertThrows(
185+
KubectlException.class,
186+
() -> {
187+
Kubectl.annotate(apiClient, V1Node.class)
188+
.apiGroup("")
189+
.apiVersion("v1")
190+
.resourceNamePlural("nodes")
191+
// .name("foo") # missing name
192+
.addAnnotation("k1", "v1")
193+
.addAnnotation("k2", "v2")
194+
.execute();
195+
});
196+
}
197+
}

0 commit comments

Comments
 (0)