Skip to content

Commit c26169c

Browse files
authored
Merge pull request #1814 from NeverRaR/feat/kubectl-rollout-history-deployment
Feat/kubectl rollout history deployment
2 parents 4a8ab93 + 9f98132 commit c26169c

File tree

3 files changed

+311
-1
lines changed

3 files changed

+311
-1
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public static KubectlVersion version() {
133133
return new KubectlVersion();
134134
}
135135

136-
/*
136+
/**
137137
* Equivalent for `kubectl scale`
138138
*
139139
* @param <ApiType> the target api type
@@ -145,6 +145,18 @@ public static <ApiType extends KubernetesObject> KubectlScale<ApiType> scale(
145145
return new KubectlScale<>(apiTypeClass);
146146
}
147147

148+
/**
149+
* Equivalent for `kubectl rollout history`
150+
*
151+
* @param <ApiType> the target api type
152+
* @param apiTypeClass the api type class
153+
* @return the kubectl rollout history operator
154+
*/
155+
public static <ApiType extends KubernetesObject> KubectlRolloutHistory<ApiType> rolloutHistory(
156+
Class<ApiType> apiTypeClass) {
157+
return new KubectlRolloutHistory<>(apiTypeClass);
158+
}
159+
148160
/**
149161
* Equivalent for `kubectl exec`
150162
*
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
Copyright 2021 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.KubernetesObject;
16+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
17+
import io.kubernetes.client.extended.kubectl.util.deployment.DeploymentHelper;
18+
import io.kubernetes.client.openapi.ApiException;
19+
import io.kubernetes.client.openapi.apis.AppsV1Api;
20+
import io.kubernetes.client.openapi.models.V1Deployment;
21+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
22+
import io.kubernetes.client.openapi.models.V1PodTemplateSpec;
23+
import io.kubernetes.client.openapi.models.V1ReplicaSet;
24+
import java.util.ArrayList;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
29+
public class KubectlRolloutHistory<ApiType extends KubernetesObject>
30+
extends Kubectl.ResourceBuilder<ApiType, KubectlRolloutHistory<ApiType>>
31+
implements Kubectl.Executable<ApiType> {
32+
33+
public static final String CHANGE_CAUSE_ANNOTATION = "kubernetes.io/change-cause";
34+
35+
public V1PodTemplateSpec getTemplate() {
36+
return template;
37+
}
38+
39+
public List<History> getHistories() {
40+
return histories;
41+
}
42+
43+
public static class History {
44+
private final long revision;
45+
private final String changeCause;
46+
47+
public long getRevision() {
48+
return revision;
49+
}
50+
51+
public String getChangeCause() {
52+
return changeCause;
53+
}
54+
55+
public History(long revision, String changeCause) {
56+
this.revision = revision;
57+
this.changeCause = changeCause;
58+
}
59+
60+
@Override
61+
public String toString() {
62+
return "{revision :" + revision + ", changeCause :" + changeCause + "}";
63+
}
64+
}
65+
66+
private long revision;
67+
68+
private final List<History> histories;
69+
70+
private V1PodTemplateSpec template;
71+
72+
@Override
73+
public ApiType execute() throws KubectlException {
74+
validate();
75+
AppsV1Api api = new AppsV1Api(this.apiClient);
76+
try {
77+
if (apiTypeClass.equals(V1Deployment.class)) {
78+
V1Deployment deployment = api.readNamespacedDeployment(name, namespace, null, null, null);
79+
deploymentViewHistory(deployment, api);
80+
return (ApiType) deployment;
81+
} else {
82+
throw new KubectlException("Unsupported class for rollout history: " + apiTypeClass);
83+
}
84+
} catch (ApiException ex) {
85+
throw new KubectlException(ex);
86+
}
87+
}
88+
89+
KubectlRolloutHistory(Class<ApiType> apiTypeClass) {
90+
super(apiTypeClass);
91+
revision = 0;
92+
histories = new ArrayList<>();
93+
}
94+
95+
public KubectlRolloutHistory<ApiType> revision(int revision) {
96+
this.revision = revision;
97+
return this;
98+
}
99+
100+
private void validate() throws KubectlException {
101+
StringBuilder msg = new StringBuilder();
102+
if (name == null) {
103+
msg.append("Missing name, ");
104+
}
105+
if (namespace == null) {
106+
msg.append("Missing namespace, ");
107+
}
108+
if (revision < 0) {
109+
msg.append("revision must be a positive integer: ").append(revision);
110+
}
111+
if (msg.length() > 0) {
112+
throw new KubectlException(msg.toString());
113+
}
114+
}
115+
116+
private void deploymentViewHistory(V1Deployment deployment, AppsV1Api api) throws ApiException {
117+
List<V1ReplicaSet> allOldRSs = new ArrayList<>();
118+
List<V1ReplicaSet> oldRSs = new ArrayList<>();
119+
V1ReplicaSet newRs = DeploymentHelper.getAllReplicaSets(deployment, api, oldRSs, allOldRSs);
120+
if (newRs != null) {
121+
allOldRSs.add(newRs);
122+
}
123+
Map<Long, V1PodTemplateSpec> historyInfo = new HashMap<>();
124+
for (V1ReplicaSet rs : allOldRSs) {
125+
Long v = DeploymentHelper.revision(rs.getMetadata());
126+
historyInfo.put(v, rs.getSpec().getTemplate());
127+
String changeCause = getChangeCause(rs.getMetadata());
128+
if (historyInfo.get(v).getMetadata().getAnnotations() == null) {
129+
historyInfo.get(v).getMetadata().setAnnotations(new HashMap<>());
130+
}
131+
if (changeCause != null && changeCause.length() > 0) {
132+
historyInfo.get(v).getMetadata().getAnnotations().put(CHANGE_CAUSE_ANNOTATION, changeCause);
133+
}
134+
}
135+
136+
if (revision > 0) {
137+
// get details of a specific revision
138+
V1PodTemplateSpec template = historyInfo.get(revision);
139+
if (template == null) {
140+
throw new ApiException("unable to find the specified revision " + revision);
141+
}
142+
this.template = template;
143+
return;
144+
}
145+
List<Long> revisions = new ArrayList<>(historyInfo.keySet());
146+
revisions.sort(Long::compareTo);
147+
for (Long revision : revisions) {
148+
String changeCause =
149+
historyInfo.get(revision).getMetadata().getAnnotations().get(CHANGE_CAUSE_ANNOTATION);
150+
if (changeCause == null || changeCause.isEmpty()) {
151+
changeCause = "<none>";
152+
}
153+
histories.add(new History(revision, changeCause));
154+
}
155+
}
156+
157+
// getChangeCause returns the change-cause annotation of the input object
158+
private static String getChangeCause(V1ObjectMeta meta) {
159+
return meta.getAnnotations().get(CHANGE_CAUSE_ANNOTATION);
160+
}
161+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
Copyright 2021 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.core.WireMockConfiguration.wireMockConfig;
17+
import static org.junit.Assert.assertThrows;
18+
19+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
20+
import com.github.tomakehurst.wiremock.matching.EqualToPattern;
21+
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
22+
import io.kubernetes.client.openapi.ApiClient;
23+
import io.kubernetes.client.openapi.models.V1Deployment;
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 org.junit.Assert;
29+
import org.junit.Before;
30+
import org.junit.Rule;
31+
import org.junit.Test;
32+
33+
public class KubectlRolloutHistoryTest {
34+
35+
private ApiClient apiClient;
36+
37+
@Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
38+
39+
private static final String DEPLOYMENT =
40+
KubectlRolloutHistoryTest.class.getClassLoader().getResource("deployment.json").getPath();
41+
42+
private static final String REPLICASET_LIST =
43+
KubectlRolloutHistoryTest.class
44+
.getClassLoader()
45+
.getResource("replicaset-list.json")
46+
.getPath();
47+
48+
@Before
49+
public void setup() throws IOException {
50+
apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockRule.port()).build();
51+
}
52+
53+
@Test
54+
public void testKubectlRolloutHistoryDeploymentShouldWork() throws KubectlException, IOException {
55+
wireMockRule.stubFor(
56+
get(urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments/foo"))
57+
.willReturn(
58+
aResponse()
59+
.withStatus(200)
60+
.withBody(new String(Files.readAllBytes(Paths.get(DEPLOYMENT))))));
61+
wireMockRule.stubFor(
62+
get(urlPathEqualTo("/apis/apps/v1/namespaces/default/replicasets"))
63+
.willReturn(
64+
aResponse()
65+
.withStatus(200)
66+
.withBody(new String(Files.readAllBytes(Paths.get(REPLICASET_LIST))))));
67+
KubectlRolloutHistory<V1Deployment> rolloutHistory =
68+
Kubectl.rolloutHistory(V1Deployment.class)
69+
.apiClient(apiClient)
70+
.name("foo")
71+
.namespace("default");
72+
rolloutHistory.execute();
73+
wireMockRule.verify(
74+
1, getRequestedFor((urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments/foo"))));
75+
wireMockRule.verify(
76+
1,
77+
getRequestedFor((urlPathEqualTo("/apis/apps/v1/namespaces/default/replicasets")))
78+
.withQueryParam("labelSelector", new EqualToPattern("app = bar")));
79+
Assert.assertEquals(3, rolloutHistory.getHistories().size());
80+
}
81+
82+
@Test
83+
public void testKubectlRolloutHistoryDeploymentWithRevisionShouldWork()
84+
throws KubectlException, IOException {
85+
wireMockRule.stubFor(
86+
get(urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments/foo"))
87+
.willReturn(
88+
aResponse()
89+
.withStatus(200)
90+
.withBody(new String(Files.readAllBytes(Paths.get(DEPLOYMENT))))));
91+
wireMockRule.stubFor(
92+
get(urlPathEqualTo("/apis/apps/v1/namespaces/default/replicasets"))
93+
.willReturn(
94+
aResponse()
95+
.withStatus(200)
96+
.withBody(new String(Files.readAllBytes(Paths.get(REPLICASET_LIST))))));
97+
KubectlRolloutHistory<V1Deployment> rolloutHistory =
98+
Kubectl.rolloutHistory(V1Deployment.class)
99+
.apiClient(apiClient)
100+
.name("foo")
101+
.namespace("default")
102+
.revision(3);
103+
rolloutHistory.execute();
104+
wireMockRule.verify(
105+
1, getRequestedFor((urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments/foo"))));
106+
wireMockRule.verify(
107+
1,
108+
getRequestedFor((urlPathEqualTo("/apis/apps/v1/namespaces/default/replicasets")))
109+
.withQueryParam("labelSelector", new EqualToPattern("app = bar")));
110+
Assert.assertNotNull(rolloutHistory.getTemplate());
111+
}
112+
113+
@Test
114+
public void testKubectlRolloutHistoryWithInvalidRevisionShouldThrow() throws IOException {
115+
wireMockRule.stubFor(
116+
get(urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments/foo"))
117+
.willReturn(
118+
aResponse()
119+
.withStatus(200)
120+
.withBody(new String(Files.readAllBytes(Paths.get(DEPLOYMENT))))));
121+
wireMockRule.stubFor(
122+
get(urlPathEqualTo("/apis/apps/v1/namespaces/default/replicasets"))
123+
.willReturn(
124+
aResponse()
125+
.withStatus(200)
126+
.withBody(new String(Files.readAllBytes(Paths.get(REPLICASET_LIST))))));
127+
KubectlRolloutHistory<V1Deployment> rolloutHistory =
128+
Kubectl.rolloutHistory(V1Deployment.class)
129+
.apiClient(apiClient)
130+
.name("foo")
131+
.namespace("default")
132+
.revision(999);
133+
assertThrows(KubectlException.class, rolloutHistory::execute);
134+
rolloutHistory.revision(-1);
135+
assertThrows(KubectlException.class, rolloutHistory::execute);
136+
}
137+
}

0 commit comments

Comments
 (0)