Skip to content

Commit 7f155eb

Browse files
authored
Merge pull request #1211 from brendandburns/drain
Add kubectl drain.
2 parents 0d29d35 + 2a62345 commit 7f155eb

File tree

6 files changed

+339
-1
lines changed

6 files changed

+339
-1
lines changed

examples/src/main/java/io/kubernetes/client/examples/KubectlExample.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import static io.kubernetes.client.extended.kubectl.Kubectl.apiResources;
1616
import static io.kubernetes.client.extended.kubectl.Kubectl.copy;
1717
import static io.kubernetes.client.extended.kubectl.Kubectl.cordon;
18+
import static io.kubernetes.client.extended.kubectl.Kubectl.drain;
1819
import static io.kubernetes.client.extended.kubectl.Kubectl.exec;
1920
import static io.kubernetes.client.extended.kubectl.Kubectl.label;
2021
import static io.kubernetes.client.extended.kubectl.Kubectl.log;
@@ -106,6 +107,11 @@ public static void main(String[] args)
106107
String name = null;
107108

108109
switch (verb) {
110+
case "drain":
111+
name = args[1];
112+
drain().apiClient(client).name(name).execute();
113+
System.out.println("Node drained");
114+
System.exit(0);
109115
case "cordon":
110116
name = args[1];
111117
cordon().apiClient(client).name(name).execute();

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
* kubectl commands.
2323
*/
2424
public class Kubectl {
25+
/** Equivalent for `kubectl drain` */
26+
public static KubectlDrain drain() {
27+
return new KubectlDrain();
28+
}
2529

2630
/** Equivalent for `kubectl cordon` */
2731
public static KubectlCordon cordon() {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ public class KubectlCordon extends Kubectl.ResourceAndContainerBuilder<V1Node, K
3636

3737
@Override
3838
public V1Node execute() throws KubectlException {
39+
return performCordon(new CoreV1Api(apiClient));
40+
}
41+
42+
protected V1Node performCordon(CoreV1Api api) throws KubectlException {
3943
String patch = this.cordon ? CORDON_PATCH_STR : UNCORDON_PATCH_STR;
40-
CoreV1Api api = new CoreV1Api(apiClient);
4144

4245
try {
4346
return PatchUtils.patch(
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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.extended.kubectl.exception.KubectlException;
16+
import io.kubernetes.client.openapi.ApiException;
17+
import io.kubernetes.client.openapi.apis.CoreV1Api;
18+
import io.kubernetes.client.openapi.models.V1Node;
19+
import io.kubernetes.client.openapi.models.V1OwnerReference;
20+
import io.kubernetes.client.openapi.models.V1Pod;
21+
import io.kubernetes.client.openapi.models.V1PodList;
22+
import java.io.IOException;
23+
import java.net.HttpURLConnection;
24+
import java.util.List;
25+
26+
public class KubectlDrain extends KubectlCordon {
27+
private int timeoutSeconds;
28+
private boolean force;
29+
private boolean ignoreDaemonSets;
30+
31+
KubectlDrain() {
32+
super(true);
33+
timeoutSeconds = 30;
34+
}
35+
36+
public KubectlDrain gracePeriod(int gracePeriodSeconds) {
37+
this.timeoutSeconds = gracePeriodSeconds;
38+
return this;
39+
}
40+
41+
public KubectlDrain force() {
42+
this.force = true;
43+
return this;
44+
}
45+
46+
public KubectlDrain ignoreDaemonSets() {
47+
this.ignoreDaemonSets = true;
48+
return this;
49+
}
50+
51+
@Override
52+
public V1Node execute() throws KubectlException {
53+
try {
54+
return doDrain();
55+
} catch (ApiException | IOException ex) {
56+
throw new KubectlException(ex);
57+
}
58+
}
59+
60+
private V1Node doDrain() throws KubectlException, ApiException, IOException {
61+
CoreV1Api api = new CoreV1Api(apiClient);
62+
V1Node node = performCordon(api);
63+
64+
V1PodList allPods =
65+
api.listPodForAllNamespaces(
66+
null,
67+
null,
68+
"spec.nodeName=" + node.getMetadata().getName(),
69+
null,
70+
null,
71+
null,
72+
null,
73+
null,
74+
null);
75+
76+
validatePods(allPods.getItems());
77+
78+
for (V1Pod pod : allPods.getItems()) {
79+
deletePod(api, pod.getMetadata().getName(), pod.getMetadata().getNamespace());
80+
}
81+
return node;
82+
}
83+
84+
private void validatePods(List<V1Pod> pods) throws KubectlException {
85+
// Throw if there are any unmanaged pods and force is false
86+
for (V1Pod pod : pods) {
87+
if (pod.getMetadata().getOwnerReferences() == null) continue;
88+
89+
if (!force && pod.getMetadata().getOwnerReferences().size() == 0) {
90+
throw new KubectlException("Pods unmanaged by a controller are present on the node");
91+
}
92+
// Throw if there are daemon set pods and ignore daemon set is false
93+
if (!ignoreDaemonSets) {
94+
for (V1OwnerReference ref : pod.getMetadata().getOwnerReferences()) {
95+
if (ref.getKind().equals("DaemonSet")) {
96+
throw new KubectlException("Pod managed by DaemonSet found");
97+
}
98+
}
99+
}
100+
}
101+
}
102+
103+
private void deletePod(CoreV1Api api, String name, String namespace)
104+
throws ApiException, IOException, KubectlException {
105+
api.deleteNamespacedPod(name, namespace, null, null, null, null, null, null);
106+
waitForPodDelete(api, name, namespace);
107+
}
108+
109+
private void waitForPodDelete(CoreV1Api api, String name, String namespace)
110+
throws ApiException, IOException, KubectlException {
111+
long start = System.currentTimeMillis();
112+
while (System.currentTimeMillis() - start < timeoutSeconds * 1000) {
113+
try {
114+
api.readNamespacedPod(name, namespace, null, null, null);
115+
} catch (ApiException ex) {
116+
if (ex.getCode() == HttpURLConnection.HTTP_NOT_FOUND) {
117+
return;
118+
}
119+
throw new KubectlException(ex);
120+
}
121+
}
122+
throw new KubectlException("Timed out waiting for Pod delete.");
123+
}
124+
}
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.aResponse;
16+
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
17+
import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
18+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.findUnmatchedRequests;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
22+
import static com.github.tomakehurst.wiremock.client.WireMock.patch;
23+
import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
24+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
25+
import static org.junit.Assert.assertEquals;
26+
27+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
28+
import com.google.common.io.Resources;
29+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
30+
import io.kubernetes.client.openapi.ApiClient;
31+
import io.kubernetes.client.openapi.models.V1Node;
32+
import io.kubernetes.client.util.ClientBuilder;
33+
import java.io.IOException;
34+
import java.nio.file.Files;
35+
import java.nio.file.Paths;
36+
import org.junit.Before;
37+
import org.junit.Rule;
38+
import org.junit.Test;
39+
40+
public class KubectlDrainTest {
41+
42+
private static final String POD_LIST_API = Resources.getResource("pod-list.json").getPath();
43+
44+
private ApiClient apiClient;
45+
46+
@Rule public WireMockRule wireMockRule = new WireMockRule(8384);
47+
48+
@Before
49+
public void setup() throws IOException {
50+
apiClient = new ClientBuilder().setBasePath("http://localhost:" + 8384).build();
51+
}
52+
53+
@Test
54+
public void testDrainNodeNoPods() throws KubectlException, IOException {
55+
// /api/v1/pods?fieldSelector=spec.nodeName%3Dkube3
56+
wireMockRule.stubFor(
57+
patch(urlPathEqualTo("/api/v1/nodes/node1"))
58+
.willReturn(
59+
aResponse().withStatus(200).withBody("{\"metadata\": { \"name\": \"node1\" } }")));
60+
wireMockRule.stubFor(
61+
get(urlPathEqualTo("/api/v1/pods"))
62+
.withQueryParam("fieldSelector", equalTo("spec.nodeName=node1"))
63+
.willReturn(aResponse().withStatus(200).withBody("{}")));
64+
V1Node node = new KubectlDrain().apiClient(apiClient).name("node1").execute();
65+
wireMockRule.verify(1, patchRequestedFor(urlPathEqualTo("/api/v1/nodes/node1")));
66+
wireMockRule.verify(1, getRequestedFor(urlPathEqualTo("/api/v1/pods")));
67+
assertEquals("node1", node.getMetadata().getName());
68+
}
69+
70+
@Test
71+
public void testDrainNodePods() throws KubectlException, IOException {
72+
// /api/v1/pods?fieldSelector=spec.nodeName%3Dkube3
73+
wireMockRule.stubFor(
74+
patch(urlPathEqualTo("/api/v1/nodes/node1"))
75+
.willReturn(
76+
aResponse().withStatus(200).withBody("{\"metadata\": { \"name\": \"node1\" } }")));
77+
wireMockRule.stubFor(
78+
get(urlPathEqualTo("/api/v1/pods"))
79+
.withQueryParam("fieldSelector", equalTo("spec.nodeName=node1"))
80+
.willReturn(
81+
aResponse()
82+
.withStatus(200)
83+
.withBody(new String(Files.readAllBytes(Paths.get(POD_LIST_API))))));
84+
wireMockRule.stubFor(
85+
delete(urlPathEqualTo("/api/v1/namespaces/mssql/pods/mssql-75b8b44f6b-znftp"))
86+
.willReturn(aResponse().withStatus(200).withBody("{}")));
87+
wireMockRule.stubFor(
88+
get(urlPathEqualTo("/api/v1/namespaces/mssql/pods/mssql-75b8b44f6b-znftp"))
89+
.willReturn(aResponse().withStatus(404).withBody("{}")));
90+
91+
V1Node node = new KubectlDrain().apiClient(apiClient).name("node1").execute();
92+
wireMockRule.verify(1, patchRequestedFor(urlPathEqualTo("/api/v1/nodes/node1")));
93+
wireMockRule.verify(1, getRequestedFor(urlPathEqualTo("/api/v1/pods")));
94+
wireMockRule.verify(
95+
1,
96+
deleteRequestedFor(urlPathEqualTo("/api/v1/namespaces/mssql/pods/mssql-75b8b44f6b-znftp")));
97+
wireMockRule.verify(
98+
1, getRequestedFor(urlPathEqualTo("/api/v1/namespaces/mssql/pods/mssql-75b8b44f6b-znftp")));
99+
assertEquals("node1", node.getMetadata().getName());
100+
assertEquals(0, findUnmatchedRequests().size());
101+
}
102+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{
2+
"apiVersion": "v1",
3+
"items": [
4+
{
5+
"apiVersion": "v1",
6+
"kind": "Pod",
7+
"metadata": {
8+
"name": "mssql-75b8b44f6b-znftp",
9+
"namespace": "mssql"
10+
},
11+
"spec": {
12+
"containers": [
13+
{
14+
"env": [
15+
{
16+
"name": "ACCEPT_EULA",
17+
"value": "Y"
18+
},
19+
{
20+
"name": "MSSQL_PID",
21+
"value": "Developer"
22+
}
23+
],
24+
"image": "mcr.microsoft.com/mssql/server:2019-CU5-ubuntu-18.04",
25+
"name": "mssql",
26+
"ports": [
27+
{
28+
"containerPort": 1433,
29+
"name": "mssql",
30+
"protocol": "TCP"
31+
}
32+
]
33+
}
34+
],
35+
"nodeName": "node1"
36+
},
37+
"status": {
38+
"conditions": [
39+
{
40+
"lastProbeTime": null,
41+
"lastTransitionTime": "2020-08-04T05:54:27Z",
42+
"status": "True",
43+
"type": "Initialized"
44+
},
45+
{
46+
"lastProbeTime": null,
47+
"lastTransitionTime": "2020-08-04T05:54:28Z",
48+
"status": "True",
49+
"type": "Ready"
50+
},
51+
{
52+
"lastProbeTime": null,
53+
"lastTransitionTime": "2020-08-04T05:54:28Z",
54+
"status": "True",
55+
"type": "ContainersReady"
56+
},
57+
{
58+
"lastProbeTime": null,
59+
"lastTransitionTime": "2020-08-04T05:54:27Z",
60+
"status": "True",
61+
"type": "PodScheduled"
62+
}
63+
],
64+
"containerStatuses": [
65+
{
66+
"containerID": "docker://940b61bd991a001f1c2800ccb7a3dcd23afef494fd8542adbf13e5c508b0a039",
67+
"image": "mcr.microsoft.com/mssql/server:2019-CU5-ubuntu-18.04",
68+
"imageID": "docker-pullable://mcr.microsoft.com/mssql/server@sha256:a4c896f11c73fd6eecaab1b96eb256c6bc0bdc06a79bdf836eed47ba56cdff13",
69+
"lastState": {},
70+
"name": "mssql",
71+
"ready": true,
72+
"restartCount": 0,
73+
"started": true,
74+
"state": {
75+
"running": {
76+
"startedAt": "2020-08-04T05:54:28Z"
77+
}
78+
}
79+
}
80+
],
81+
"hostIP": "10.10.0.101",
82+
"phase": "Running",
83+
"podIP": "10.244.1.65",
84+
"podIPs": [
85+
{
86+
"ip": "10.244.1.65"
87+
}
88+
],
89+
"qosClass": "Burstable",
90+
"startTime": "2020-08-04T05:54:27Z"
91+
}
92+
}
93+
],
94+
"kind": "List",
95+
"metadata": {
96+
"resourceVersion": "",
97+
"selfLink": ""
98+
}
99+
}

0 commit comments

Comments
 (0)