Skip to content

Commit d58f7fb

Browse files
committed
kubectl annotate implementation
1 parent 6b43ab6 commit d58f7fb

File tree

4 files changed

+407
-0
lines changed

4 files changed

+407
-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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
implements Kubectl.Executable<ApiType> {
28+
29+
private final ApiClient apiClient;
30+
private final Class<ApiType> apiTypeClass;
31+
private final Map<String, String> addingAnnotations;
32+
33+
private String apiGroup;
34+
private String apiVersion;
35+
private String resourceNamePlural;
36+
37+
private String namespace;
38+
private String name;
39+
40+
KubectlAnnotate(ApiClient apiClient, Class<ApiType> apiTypeClass) {
41+
this.addingAnnotations = new HashMap<>();
42+
this.apiTypeClass = apiTypeClass;
43+
this.apiClient = apiClient;
44+
}
45+
46+
public KubectlAnnotate<ApiType> apiGroup(String apiGroup) {
47+
this.apiGroup = apiGroup;
48+
return this;
49+
}
50+
51+
public KubectlAnnotate<ApiType> apiVersion(String apiVersion) {
52+
this.apiVersion = apiVersion;
53+
return this;
54+
}
55+
56+
public KubectlAnnotate<ApiType> resourceNamePlural(String resourceNamePlural) {
57+
this.resourceNamePlural = resourceNamePlural;
58+
return this;
59+
}
60+
61+
public KubectlAnnotate<ApiType> namespace(String namespace) {
62+
this.namespace = namespace;
63+
return this;
64+
}
65+
66+
public KubectlAnnotate<ApiType> name(String name) {
67+
this.name = name;
68+
return this;
69+
}
70+
71+
public KubectlAnnotate<ApiType> addAnnotation(String key, String value) {
72+
this.addingAnnotations.put(key, value);
73+
return this;
74+
}
75+
76+
private boolean isNamespaced() {
77+
return !StringUtils.isEmpty(namespace);
78+
}
79+
80+
@Override
81+
public ApiType execute() throws KubectlException {
82+
verifyArguments();
83+
GenericKubernetesApi<ApiType, KubernetesListObject> api =
84+
new GenericKubernetesApi<>(
85+
apiTypeClass,
86+
KubernetesListObject.class,
87+
apiGroup,
88+
apiVersion,
89+
resourceNamePlural,
90+
apiClient);
91+
92+
try {
93+
final KubernetesApiResponse<ApiType> getResponse;
94+
if (isNamespaced()) {
95+
getResponse = api.get(namespace, name);
96+
} else {
97+
getResponse = api.get(name);
98+
}
99+
if (!getResponse.isSuccess()) {
100+
throw new KubectlException(getResponse.getStatus());
101+
}
102+
ApiType obj = getResponse.getObject();
103+
104+
Annotations.addAnnotations(obj, addingAnnotations);
105+
106+
final KubernetesApiResponse<ApiType> updateResponse;
107+
updateResponse = api.update(obj);
108+
if (!updateResponse.isSuccess()) {
109+
throw new KubectlException(updateResponse.getStatus());
110+
}
111+
return updateResponse.getObject();
112+
} catch (Throwable t) {
113+
throw new KubectlException(t);
114+
}
115+
}
116+
117+
private void verifyArguments() throws KubectlException {
118+
if (null == apiGroup) {
119+
throw new KubectlException("missing apiGroup argument");
120+
}
121+
if (null == apiVersion) {
122+
throw new KubectlException("missing apiVersion argument");
123+
}
124+
if (null == resourceNamePlural) {
125+
throw new KubectlException("missing resourceNamePlural argument");
126+
}
127+
if (null == name) {
128+
throw new KubectlException("missing name argument");
129+
}
130+
}
131+
}
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)