diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/BUILD b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/BUILD index 37ff8eba97a..0b4d9f91a12 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/BUILD @@ -95,6 +95,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library", "//staging/src/k8s.io/client-go/testing:go_default_library", diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history.go index 0d00f4ab009..139cc8f8130 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history.go @@ -183,6 +183,18 @@ func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision in if err != nil { return "", err } + return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) { + dsOfHistory, err := applyDaemonSetHistory(ds, history) + if err != nil { + return nil, err + } + return &dsOfHistory.Spec.Template, err + }) +} + +// printHistory returns the podTemplate of the given revision if it is non-zero +// else returns the overall revisions +func printHistory(history []*appsv1.ControllerRevision, revision int64, getPodTemplate func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error)) (string, error) { historyInfo := make(map[int64]*appsv1.ControllerRevision) for _, history := range history { // TODO: for now we assume revisions don't overlap, we may need to handle it @@ -198,11 +210,11 @@ func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision in if !ok { return "", fmt.Errorf("unable to find the specified revision") } - dsOfHistory, err := applyDaemonSetHistory(ds, history) + podTemplate, err := getPodTemplate(history) if err != nil { return "", fmt.Errorf("unable to parse history %s", history.Name) } - return printTemplate(&dsOfHistory.Spec.Template) + return printTemplate(podTemplate) } // Print an overview of all Revisions @@ -233,28 +245,17 @@ type StatefulSetHistoryViewer struct { // ViewHistory returns a list of the revision history of a statefulset // TODO: this should be a describer -// TODO: needs to implement detailed revision view func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { - _, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name) + sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name) if err != nil { return "", err } - - if len(history) <= 0 { - return "No rollout history found.", nil - } - revisions := make([]int64, 0, len(history)) - for _, revision := range history { - revisions = append(revisions, revision.Revision) - } - sliceutil.SortInts64(revisions) - - return tabbedString(func(out io.Writer) error { - fmt.Fprintf(out, "REVISION\n") - for _, r := range revisions { - fmt.Fprintf(out, "%d\n", r) + return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) { + stsOfHistory, err := applyStatefulSetHistory(sts, history) + if err != nil { + return nil, err } - return nil + return &stsOfHistory.Spec.Template, err }) } @@ -365,6 +366,24 @@ func applyDaemonSetHistory(ds *appsv1.DaemonSet, history *appsv1.ControllerRevis return result, nil } +// applyStatefulSetHistory returns a specific revision of StatefulSet by applying the given history to a copy of the given StatefulSet +func applyStatefulSetHistory(sts *appsv1.StatefulSet, history *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) { + stsBytes, err := json.Marshal(sts) + if err != nil { + return nil, err + } + patched, err := strategicpatch.StrategicMergePatch(stsBytes, history.Data.Raw, sts) + if err != nil { + return nil, err + } + result := &appsv1.StatefulSet{} + err = json.Unmarshal(patched, result) + if err != nil { + return nil, err + } + return result, nil +} + // TODO: copied here until this becomes a describer func tabbedString(f func(io.Writer) error) (string, error) { out := new(tabwriter.Writer) diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history_test.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history_test.go index 8b2af5704f9..13b204c6c30 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history_test.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/history_test.go @@ -27,6 +27,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/client-go/kubernetes/fake" ) @@ -53,60 +54,176 @@ func TestHistoryViewerFor(t *testing.T) { func TestViewHistory(t *testing.T) { - var ( - trueVar = true - replicas = int32(1) + t.Run("for statefulSet", func(t *testing.T) { + var ( + trueVar = true + replicas = int32(1) - podStub = corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "test", Image: "nginx"}}}, + podStub = corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "test", Image: "nginx"}}}, + } + + ssStub = &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "moons", + Namespace: "default", + UID: "1993", + Labels: map[string]string{"foo": "bar"}, + }, + Spec: appsv1.StatefulSetSpec{Selector: &metav1.LabelSelector{MatchLabels: podStub.ObjectMeta.Labels}, Replicas: &replicas, Template: podStub}, + } + ) + stsRawData, err := json.Marshal(ssStub) + if err != nil { + t.Fatalf("error creating sts raw data: %v", err) } - - ssStub = &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "moons", - Namespace: "default", - UID: "1993", - Labels: map[string]string{"foo": "bar"}, - }, - Spec: appsv1.StatefulSetSpec{Selector: &metav1.LabelSelector{MatchLabels: podStub.ObjectMeta.Labels}, Replicas: &replicas, Template: podStub}, - } - - ssStub1 = &appsv1.ControllerRevision{ + ssStub1 := &appsv1.ControllerRevision{ ObjectMeta: metav1.ObjectMeta{ Name: "moons", Namespace: "default", Labels: map[string]string{"foo": "bar"}, OwnerReferences: []metav1.OwnerReference{{"apps/v1", "StatefulSet", "moons", "1993", &trueVar, nil}}, }, + Data: runtime.RawExtension{Raw: stsRawData}, TypeMeta: metav1.TypeMeta{Kind: "StatefulSet", APIVersion: "apps/v1"}, Revision: 1, } - ) - fakeClientSet := fake.NewSimpleClientset(ssStub) - _, err := fakeClientSet.AppsV1().ControllerRevisions("default").Create(context.TODO(), ssStub1, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("create controllerRevisions error %v occurred ", err) - } + fakeClientSet := fake.NewSimpleClientset(ssStub) + _, err = fakeClientSet.AppsV1().ControllerRevisions("default").Create(context.TODO(), ssStub1, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("create controllerRevisions error %v occurred ", err) + } - var sts = &StatefulSetHistoryViewer{ - fakeClientSet, - } + var sts = &StatefulSetHistoryViewer{ + fakeClientSet, + } - result, err := sts.ViewHistory("default", "moons", 1) - if err != nil { - t.Fatalf("error getting ViewHistory for a StatefulSets moons: %v", err) - } + t.Run("should show revisions list if the revision is not specified", func(t *testing.T) { + result, err := sts.ViewHistory("default", "moons", 0) + if err != nil { + t.Fatalf("error getting ViewHistory for a StatefulSets moons: %v", err) + } - expected := `REVISION -1 + expected := `REVISION CHANGE-CAUSE +1 ` - if result != expected { - t.Fatalf("unexpected output (%v was expected but got %v)", expected, result) - } + if result != expected { + t.Fatalf("unexpected output (%v was expected but got %v)", expected, result) + } + }) + t.Run("should describe the revision if revision is specified", func(t *testing.T) { + result, err := sts.ViewHistory("default", "moons", 1) + if err != nil { + t.Fatalf("error getting ViewHistory for a StatefulSets moons: %v", err) + } + + expected := `Pod Template: + Labels: foo=bar + Containers: + test: + Image: nginx + Port: + Host Port: + Environment: + Mounts: + Volumes: +` + + if result != expected { + t.Fatalf("unexpected output (%v was expected but got %v)", expected, result) + } + }) + + }) + + t.Run("for daemonSet", func(t *testing.T) { + var ( + trueVar = true + podStub = corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "test", Image: "nginx"}}}, + } + + daemonSetStub = &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "moons", + Namespace: "default", + UID: "1993", + Labels: map[string]string{"foo": "bar"}, + }, + Spec: appsv1.DaemonSetSpec{Selector: &metav1.LabelSelector{MatchLabels: podStub.ObjectMeta.Labels}, Template: podStub}, + } + ) + + daemonSetRaw, err := json.Marshal(daemonSetStub) + if err != nil { + t.Fatalf("error creating sts raw data: %v", err) + } + daemonSetControllerRevision := &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "moons", + Namespace: "default", + Labels: map[string]string{"foo": "bar"}, + OwnerReferences: []metav1.OwnerReference{{"apps/v1", "DaemonSet", "moons", "1993", &trueVar, nil}}, + }, + Data: runtime.RawExtension{Raw: daemonSetRaw}, + TypeMeta: metav1.TypeMeta{Kind: "StatefulSet", APIVersion: "apps/v1"}, + Revision: 1, + } + + fakeClientSet := fake.NewSimpleClientset(daemonSetStub) + _, err = fakeClientSet.AppsV1().ControllerRevisions("default").Create(context.TODO(), daemonSetControllerRevision, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("create controllerRevisions error %v occurred ", err) + } + + var daemonSetHistoryViewer = &DaemonSetHistoryViewer{ + fakeClientSet, + } + + t.Run("should show revisions list if the revision is not specified", func(t *testing.T) { + result, err := daemonSetHistoryViewer.ViewHistory("default", "moons", 0) + if err != nil { + t.Fatalf("error getting ViewHistory for a StatefulSets moons: %v", err) + } + + expected := `REVISION CHANGE-CAUSE +1 +` + + if result != expected { + t.Fatalf("unexpected output (%v was expected but got %v)", expected, result) + } + }) + + t.Run("should describe the revision if revision is specified", func(t *testing.T) { + result, err := daemonSetHistoryViewer.ViewHistory("default", "moons", 1) + if err != nil { + t.Fatalf("error getting ViewHistory for a StatefulSets moons: %v", err) + } + + expected := `Pod Template: + Labels: foo=bar + Containers: + test: + Image: nginx + Port: + Host Port: + Environment: + Mounts: + Volumes: +` + + if result != expected { + t.Fatalf("unexpected output (%v was expected but got %v)", expected, result) + } + }) + + }) } func TestApplyDaemonSetHistory(t *testing.T) {