1
16
17 package polymorphichelpers
18
19 import (
20 "bytes"
21 "context"
22 "fmt"
23 "io"
24 "text/tabwriter"
25
26 appsv1 "k8s.io/api/apps/v1"
27 corev1 "k8s.io/api/core/v1"
28 "k8s.io/apimachinery/pkg/api/meta"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/labels"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/runtime/schema"
33 "k8s.io/apimachinery/pkg/util/json"
34 "k8s.io/apimachinery/pkg/util/strategicpatch"
35 "k8s.io/client-go/kubernetes"
36 clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
37 "k8s.io/klog/v2"
38 "k8s.io/kubectl/pkg/apps"
39 "k8s.io/kubectl/pkg/describe"
40 deploymentutil "k8s.io/kubectl/pkg/util/deployment"
41 sliceutil "k8s.io/kubectl/pkg/util/slice"
42 )
43
44 const (
45 ChangeCauseAnnotation = "kubernetes.io/change-cause"
46 )
47
48
49 type HistoryViewer interface {
50 ViewHistory(namespace, name string, revision int64) (string, error)
51 GetHistory(namespace, name string) (map[int64]runtime.Object, error)
52 }
53
54 type HistoryVisitor struct {
55 clientset kubernetes.Interface
56 result HistoryViewer
57 }
58
59 func (v *HistoryVisitor) VisitDeployment(elem apps.GroupKindElement) {
60 v.result = &DeploymentHistoryViewer{v.clientset}
61 }
62
63 func (v *HistoryVisitor) VisitStatefulSet(kind apps.GroupKindElement) {
64 v.result = &StatefulSetHistoryViewer{v.clientset}
65 }
66
67 func (v *HistoryVisitor) VisitDaemonSet(kind apps.GroupKindElement) {
68 v.result = &DaemonSetHistoryViewer{v.clientset}
69 }
70
71 func (v *HistoryVisitor) VisitJob(kind apps.GroupKindElement) {}
72 func (v *HistoryVisitor) VisitPod(kind apps.GroupKindElement) {}
73 func (v *HistoryVisitor) VisitReplicaSet(kind apps.GroupKindElement) {}
74 func (v *HistoryVisitor) VisitReplicationController(kind apps.GroupKindElement) {}
75 func (v *HistoryVisitor) VisitCronJob(kind apps.GroupKindElement) {}
76
77
78 func HistoryViewerFor(kind schema.GroupKind, c kubernetes.Interface) (HistoryViewer, error) {
79 elem := apps.GroupKindElement(kind)
80 visitor := &HistoryVisitor{
81 clientset: c,
82 }
83
84
85 err := elem.Accept(visitor)
86
87 if err != nil {
88 return nil, fmt.Errorf("error retrieving history for %q, %v", kind.String(), err)
89 }
90
91 if visitor.result == nil {
92 return nil, fmt.Errorf("no history viewer has been implemented for %q", kind.String())
93 }
94
95 return visitor.result, nil
96 }
97
98 type DeploymentHistoryViewer struct {
99 c kubernetes.Interface
100 }
101
102
103
104 func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
105 allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
106 if err != nil {
107 return "", err
108 }
109
110 historyInfo := make(map[int64]*corev1.PodTemplateSpec)
111 for _, rs := range allRSs {
112 v, err := deploymentutil.Revision(rs)
113 if err != nil {
114 klog.Warningf("unable to get revision from replicaset %s for deployment %s in namespace %s: %v", rs.Name, name, namespace, err)
115 continue
116 }
117 historyInfo[v] = &rs.Spec.Template
118 changeCause := getChangeCause(rs)
119 if historyInfo[v].Annotations == nil {
120 historyInfo[v].Annotations = make(map[string]string)
121 }
122 if len(changeCause) > 0 {
123 historyInfo[v].Annotations[ChangeCauseAnnotation] = changeCause
124 }
125 }
126
127 if len(historyInfo) == 0 {
128 return "No rollout history found.", nil
129 }
130
131 if revision > 0 {
132
133 template, ok := historyInfo[revision]
134 if !ok {
135 return "", fmt.Errorf("unable to find the specified revision")
136 }
137 return printTemplate(template)
138 }
139
140
141 revisions := make([]int64, 0, len(historyInfo))
142 for r := range historyInfo {
143 revisions = append(revisions, r)
144 }
145 sliceutil.SortInts64(revisions)
146
147 return tabbedString(func(out io.Writer) error {
148 fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
149 for _, r := range revisions {
150
151 changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
152 if len(changeCause) == 0 {
153 changeCause = "<none>"
154 }
155 fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
156 }
157 return nil
158 })
159 }
160
161
162 func (h *DeploymentHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
163 allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
164 if err != nil {
165 return nil, err
166 }
167
168 result := make(map[int64]runtime.Object)
169 for _, rs := range allRSs {
170 v, err := deploymentutil.Revision(rs)
171 if err != nil {
172 klog.Warningf("unable to get revision from replicaset %s for deployment %s in namespace %s: %v", rs.Name, name, namespace, err)
173 continue
174 }
175 result[v] = rs
176 }
177
178 return result, nil
179 }
180
181 func printTemplate(template *corev1.PodTemplateSpec) (string, error) {
182 buf := bytes.NewBuffer([]byte{})
183 w := describe.NewPrefixWriter(buf)
184 describe.DescribePodTemplate(template, w)
185 return buf.String(), nil
186 }
187
188 type DaemonSetHistoryViewer struct {
189 c kubernetes.Interface
190 }
191
192
193
194 func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
195 ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
196 if err != nil {
197 return "", err
198 }
199 return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
200 dsOfHistory, err := applyDaemonSetHistory(ds, history)
201 if err != nil {
202 return nil, err
203 }
204 return &dsOfHistory.Spec.Template, err
205 })
206 }
207
208
209 func (h *DaemonSetHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
210 ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
211 if err != nil {
212 return nil, err
213 }
214
215 result := make(map[int64]runtime.Object)
216 for _, h := range history {
217 applied, err := applyDaemonSetHistory(ds, h)
218 if err != nil {
219 return nil, err
220 }
221 result[h.Revision] = applied
222 }
223
224 return result, nil
225 }
226
227
228
229 func printHistory(history []*appsv1.ControllerRevision, revision int64, getPodTemplate func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error)) (string, error) {
230 historyInfo := make(map[int64]*appsv1.ControllerRevision)
231 for _, history := range history {
232
233 historyInfo[history.Revision] = history
234 }
235 if len(historyInfo) == 0 {
236 return "No rollout history found.", nil
237 }
238
239
240 if revision > 0 {
241 history, ok := historyInfo[revision]
242 if !ok {
243 return "", fmt.Errorf("unable to find the specified revision")
244 }
245 podTemplate, err := getPodTemplate(history)
246 if err != nil {
247 return "", fmt.Errorf("unable to parse history %s", history.Name)
248 }
249 return printTemplate(podTemplate)
250 }
251
252
253
254 revisions := make([]int64, 0, len(historyInfo))
255 for r := range historyInfo {
256 revisions = append(revisions, r)
257 }
258 sliceutil.SortInts64(revisions)
259
260 return tabbedString(func(out io.Writer) error {
261 fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
262 for _, r := range revisions {
263
264 changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
265 if len(changeCause) == 0 {
266 changeCause = "<none>"
267 }
268 fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
269 }
270 return nil
271 })
272 }
273
274 type StatefulSetHistoryViewer struct {
275 c kubernetes.Interface
276 }
277
278
279
280 func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
281 sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
282 if err != nil {
283 return "", err
284 }
285 return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
286 stsOfHistory, err := applyStatefulSetHistory(sts, history)
287 if err != nil {
288 return nil, err
289 }
290 return &stsOfHistory.Spec.Template, err
291 })
292 }
293
294
295 func (h *StatefulSetHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
296 sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
297 if err != nil {
298 return nil, err
299 }
300
301 result := make(map[int64]runtime.Object)
302 for _, h := range history {
303 applied, err := applyStatefulSetHistory(sts, h)
304 if err != nil {
305 return nil, err
306 }
307 result[h.Revision] = applied
308 }
309
310 return result, nil
311 }
312
313 func getDeploymentReplicaSets(apps clientappsv1.AppsV1Interface, namespace, name string) ([]*appsv1.ReplicaSet, error) {
314 deployment, err := apps.Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
315 if err != nil {
316 return nil, fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
317 }
318
319 _, oldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, apps)
320 if err != nil {
321 return nil, fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err)
322 }
323
324 if newRS == nil {
325 return oldRSs, nil
326 }
327 return append(oldRSs, newRS), nil
328 }
329
330
331
332 func controlledHistoryV1(
333 apps clientappsv1.AppsV1Interface,
334 namespace string,
335 selector labels.Selector,
336 accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
337 var result []*appsv1.ControllerRevision
338 historyList, err := apps.ControllerRevisions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
339 if err != nil {
340 return nil, err
341 }
342 for i := range historyList.Items {
343 history := historyList.Items[i]
344
345 if metav1.IsControlledBy(&history, accessor) {
346 result = append(result, &history)
347 }
348 }
349 return result, nil
350 }
351
352
353 func controlledHistory(
354 apps clientappsv1.AppsV1Interface,
355 namespace string,
356 selector labels.Selector,
357 accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
358 var result []*appsv1.ControllerRevision
359 historyList, err := apps.ControllerRevisions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
360 if err != nil {
361 return nil, err
362 }
363 for i := range historyList.Items {
364 history := historyList.Items[i]
365
366 if metav1.IsControlledBy(&history, accessor) {
367 result = append(result, &history)
368 }
369 }
370 return result, nil
371 }
372
373
374 func daemonSetHistory(
375 apps clientappsv1.AppsV1Interface,
376 namespace, name string) (*appsv1.DaemonSet, []*appsv1.ControllerRevision, error) {
377 ds, err := apps.DaemonSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
378 if err != nil {
379 return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
380 }
381 selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
382 if err != nil {
383 return nil, nil, fmt.Errorf("failed to create selector for DaemonSet %s: %v", ds.Name, err)
384 }
385 accessor, err := meta.Accessor(ds)
386 if err != nil {
387 return nil, nil, fmt.Errorf("failed to create accessor for DaemonSet %s: %v", ds.Name, err)
388 }
389 history, err := controlledHistory(apps, ds.Namespace, selector, accessor)
390 if err != nil {
391 return nil, nil, fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
392 }
393 return ds, history, nil
394 }
395
396
397 func statefulSetHistory(
398 apps clientappsv1.AppsV1Interface,
399 namespace, name string) (*appsv1.StatefulSet, []*appsv1.ControllerRevision, error) {
400 sts, err := apps.StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
401 if err != nil {
402 return nil, nil, fmt.Errorf("failed to retrieve Statefulset %s: %s", name, err.Error())
403 }
404 selector, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
405 if err != nil {
406 return nil, nil, fmt.Errorf("failed to create selector for StatefulSet %s: %s", name, err.Error())
407 }
408 accessor, err := meta.Accessor(sts)
409 if err != nil {
410 return nil, nil, fmt.Errorf("failed to obtain accessor for StatefulSet %s: %s", name, err.Error())
411 }
412 history, err := controlledHistoryV1(apps, namespace, selector, accessor)
413 if err != nil {
414 return nil, nil, fmt.Errorf("unable to find history controlled by StatefulSet %s: %v", name, err)
415 }
416 return sts, history, nil
417 }
418
419
420 func applyDaemonSetHistory(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (*appsv1.DaemonSet, error) {
421 dsBytes, err := json.Marshal(ds)
422 if err != nil {
423 return nil, err
424 }
425 patched, err := strategicpatch.StrategicMergePatch(dsBytes, history.Data.Raw, ds)
426 if err != nil {
427 return nil, err
428 }
429 result := &appsv1.DaemonSet{}
430 err = json.Unmarshal(patched, result)
431 if err != nil {
432 return nil, err
433 }
434 return result, nil
435 }
436
437
438 func applyStatefulSetHistory(sts *appsv1.StatefulSet, history *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) {
439 stsBytes, err := json.Marshal(sts)
440 if err != nil {
441 return nil, err
442 }
443 patched, err := strategicpatch.StrategicMergePatch(stsBytes, history.Data.Raw, sts)
444 if err != nil {
445 return nil, err
446 }
447 result := &appsv1.StatefulSet{}
448 err = json.Unmarshal(patched, result)
449 if err != nil {
450 return nil, err
451 }
452 return result, nil
453 }
454
455
456 func tabbedString(f func(io.Writer) error) (string, error) {
457 out := new(tabwriter.Writer)
458 buf := &bytes.Buffer{}
459 out.Init(buf, 0, 8, 2, ' ', 0)
460
461 err := f(out)
462 if err != nil {
463 return "", err
464 }
465
466 out.Flush()
467 return buf.String(), nil
468 }
469
470
471 func getChangeCause(obj runtime.Object) string {
472 accessor, err := meta.Accessor(obj)
473 if err != nil {
474 return ""
475 }
476 return accessor.GetAnnotations()[ChangeCauseAnnotation]
477 }
478
View as plain text