Skip to content

Commit 2f8d925

Browse files
committed
feat : support kubectl rollout history for daemonset
1 parent c26169c commit 2f8d925

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,32 @@
1616
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
1717
import io.kubernetes.client.extended.kubectl.util.deployment.DeploymentHelper;
1818
import io.kubernetes.client.openapi.ApiException;
19+
import io.kubernetes.client.openapi.JSON;
1920
import io.kubernetes.client.openapi.apis.AppsV1Api;
21+
import io.kubernetes.client.openapi.models.V1ControllerRevision;
22+
import io.kubernetes.client.openapi.models.V1ControllerRevisionList;
23+
import io.kubernetes.client.openapi.models.V1DaemonSet;
2024
import io.kubernetes.client.openapi.models.V1Deployment;
2125
import io.kubernetes.client.openapi.models.V1ObjectMeta;
26+
import io.kubernetes.client.openapi.models.V1OwnerReference;
2227
import io.kubernetes.client.openapi.models.V1PodTemplateSpec;
2328
import io.kubernetes.client.openapi.models.V1ReplicaSet;
29+
import io.kubernetes.client.util.labels.LabelSelector;
2430
import java.util.ArrayList;
2531
import java.util.HashMap;
2632
import java.util.List;
2733
import java.util.Map;
34+
import java.util.Optional;
2835

2936
public class KubectlRolloutHistory<ApiType extends KubernetesObject>
3037
extends Kubectl.ResourceBuilder<ApiType, KubectlRolloutHistory<ApiType>>
3138
implements Kubectl.Executable<ApiType> {
3239

40+
@FunctionalInterface
41+
public interface PodTemplateParser {
42+
V1PodTemplateSpec parse(V1ControllerRevision history) throws ApiException;
43+
}
44+
3345
public static final String CHANGE_CAUSE_ANNOTATION = "kubernetes.io/change-cause";
3446

3547
public V1PodTemplateSpec getTemplate() {
@@ -78,6 +90,10 @@ public ApiType execute() throws KubectlException {
7890
V1Deployment deployment = api.readNamespacedDeployment(name, namespace, null, null, null);
7991
deploymentViewHistory(deployment, api);
8092
return (ApiType) deployment;
93+
} else if (apiTypeClass.equals(V1DaemonSet.class)) {
94+
V1DaemonSet daemonSet = api.readNamespacedDaemonSet(name, namespace, null, null, null);
95+
daemonSetViewHistory(daemonSet, api);
96+
return (ApiType) daemonSet;
8197
} else {
8298
throw new KubectlException("Unsupported class for rollout history: " + apiTypeClass);
8399
}
@@ -154,8 +170,108 @@ private void deploymentViewHistory(V1Deployment deployment, AppsV1Api api) throw
154170
}
155171
}
156172

173+
private void daemonSetViewHistory(V1DaemonSet daemonSet, AppsV1Api api) throws ApiException {
174+
LabelSelector selector = LabelSelector.parse(daemonSet.getSpec().getSelector());
175+
List<V1ControllerRevision> historyList = controlledHistory(api, daemonSet, selector);
176+
parseHistory(
177+
historyList,
178+
history -> {
179+
V1DaemonSet dsOfHistory = applyDaemonSetHistory(daemonSet, history);
180+
return dsOfHistory.getSpec().getTemplate();
181+
});
182+
}
183+
184+
private V1DaemonSet applyDaemonSetHistory(V1DaemonSet ds, V1ControllerRevision history) {
185+
String dsJson = new JSON().serialize(ds);
186+
String patch = new JSON().serialize(history.getData());
187+
String patched = applyDirectivePatch(dsJson, patch);
188+
return new JSON().deserialize(patched, V1DaemonSet.class);
189+
}
190+
191+
private void parseHistory(List<V1ControllerRevision> historyList, PodTemplateParser parser)
192+
throws ApiException {
193+
Map<Long, V1ControllerRevision> historyInfo = new HashMap<>();
194+
for (V1ControllerRevision history : historyList) {
195+
historyInfo.put(history.getRevision(), history);
196+
}
197+
198+
if (revision > 0) {
199+
V1ControllerRevision history = historyInfo.get(revision);
200+
if (history == null) {
201+
throw new ApiException("unable to find the specified revision " + revision);
202+
}
203+
template = parser.parse(history);
204+
return;
205+
}
206+
207+
List<Long> revisions = new ArrayList<>(historyInfo.keySet());
208+
revisions.sort(Long::compareTo);
209+
for (Long revision : revisions) {
210+
String changeCause =
211+
historyInfo.get(revision).getMetadata().getAnnotations().get(CHANGE_CAUSE_ANNOTATION);
212+
if (changeCause == null || changeCause.isEmpty()) {
213+
changeCause = "<none>";
214+
}
215+
histories.add(new History(revision, changeCause));
216+
}
217+
}
218+
219+
// controlledHistories returns all ControllerRevisions in namespace that selected by selector and
220+
// owned by accessor
221+
private List<V1ControllerRevision> controlledHistory(
222+
AppsV1Api api, KubernetesObject accessor, LabelSelector selector) throws ApiException {
223+
List<V1ControllerRevision> result = new ArrayList<>();
224+
V1ControllerRevisionList historyList =
225+
api.listNamespacedControllerRevision(
226+
namespace, null, null, null, null, selector.toString(), null, null, null, null, null);
227+
for (V1ControllerRevision history : historyList.getItems()) {
228+
if (isControlledBy(history, accessor)) {
229+
result.add(history);
230+
}
231+
}
232+
return result;
233+
}
234+
235+
private boolean isControlledBy(KubernetesObject obj, KubernetesObject owner) {
236+
Optional<V1OwnerReference> ref =
237+
obj.getMetadata().getOwnerReferences().stream()
238+
.filter(r -> r.getController() != null && r.getController())
239+
.findAny();
240+
return ref.map(
241+
v1OwnerReference -> v1OwnerReference.getUid().equals(owner.getMetadata().getUid()))
242+
.orElse(false);
243+
}
244+
157245
// getChangeCause returns the change-cause annotation of the input object
158246
private static String getChangeCause(V1ObjectMeta meta) {
159247
return meta.getAnnotations().get(CHANGE_CAUSE_ANNOTATION);
160248
}
249+
250+
// now we just support directive in merge
251+
private static String applyDirectivePatch(String origin, String patch) {
252+
// todo StrategicPatch?
253+
Map<String, Object> patchMap = new JSON().deserialize(patch, Map.class);
254+
Map<String, Object> specMap = (Map<String, Object>) patchMap.get("spec");
255+
Map<String, Object> templateMap = (Map<String, Object>) specMap.get("template");
256+
final String directiveMarker = "$patch";
257+
Object directive = templateMap.get(directiveMarker);
258+
if (directive == null) {
259+
throw new IllegalArgumentException("not a directive patch");
260+
}
261+
if ("replace".equals(directive)) {
262+
// If the patch contains "$patch: replace", don't merge it, just use the
263+
// patch directly. Later on, we can add a single level replace that only
264+
// affects the map that the $patch is in.
265+
templateMap.remove(directiveMarker);
266+
return new JSON().serialize(patchMap);
267+
}
268+
if ("delete".equals(directive)) {
269+
// If the patch contains "$patch: delete", don't merge it, just return
270+
// an empty map.
271+
specMap.put("template", new HashMap<>());
272+
return new JSON().serialize(patchMap);
273+
}
274+
throw new IllegalArgumentException(
275+
"unknown patch type: " + directive + " in map: " + templateMap);
276+
}
161277
}

0 commit comments

Comments
 (0)