Fix rollout history bug

Fix rollout history bug where the latest revision was
always shown when requesting a specific revision and
specifying an output.

Add unit and integration tests for rollout history.
This commit is contained in:
Brian Pursley 2022-07-13 18:27:05 -04:00
parent 3f630c415d
commit 693e1299a6
5 changed files with 815 additions and 14 deletions

View File

@ -18,6 +18,7 @@ package rollout
import (
"fmt"
"sort"
"github.com/spf13/cobra"
@ -153,6 +154,44 @@ func (o *RolloutHistoryOptions) Run() error {
return err
}
if o.PrintFlags.OutputFlagSpecified() {
printer, err := o.PrintFlags.ToPrinter()
if err != nil {
return err
}
return r.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
mapping := info.ResourceMapping()
historyViewer, err := o.HistoryViewer(o.RESTClientGetter, mapping)
if err != nil {
return err
}
historyInfo, err := historyViewer.GetHistory(info.Namespace, info.Name)
if err != nil {
return err
}
if o.Revision > 0 {
printer.PrintObj(historyInfo[o.Revision], o.Out)
} else {
sortedKeys := make([]int64, 0, len(historyInfo))
for k := range historyInfo {
sortedKeys = append(sortedKeys, k)
}
sort.Slice(sortedKeys, func(i, j int) bool { return sortedKeys[i] < sortedKeys[j] })
for _, k := range sortedKeys {
printer.PrintObj(historyInfo[k], o.Out)
}
}
return nil
})
}
return r.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err

View File

@ -0,0 +1,428 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rollout
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme"
)
type fakeHistoryViewer struct {
viewHistoryFn func(namespace, name string, revision int64) (string, error)
getHistoryFn func(namespace, name string) (map[int64]runtime.Object, error)
}
func (h *fakeHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
return h.viewHistoryFn(namespace, name, revision)
}
func (h *fakeHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
return h.getHistoryFn(namespace, name)
}
func setupFakeHistoryViewer(t *testing.T) *fakeHistoryViewer {
fhv := &fakeHistoryViewer{
viewHistoryFn: func(namespace, name string, revision int64) (string, error) {
t.Fatalf("ViewHistory mock not implemented")
return "", nil
},
getHistoryFn: func(namespace, name string) (map[int64]runtime.Object, error) {
t.Fatalf("GetHistory mock not implemented")
return nil, nil
},
}
polymorphichelpers.HistoryViewerFn = func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (polymorphichelpers.HistoryViewer, error) {
return fhv, nil
}
return fhv
}
func TestRolloutHistory(t *testing.T) {
ns := scheme.Codecs.WithoutConversion()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
tf.Client = &RolloutPauseRESTClient{
RESTClient: &fake.RESTClient{
GroupVersion: rolloutPauseGroupVersionEncoder,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test/deployments/foo" && m == "GET":
responseDeployment := &appsv1.Deployment{}
responseDeployment.Name = "foo"
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
},
}
testCases := map[string]struct {
flags map[string]string
expectedOutput string
expectedRevision int64
}{
"should display ViewHistory output for all revisions": {
expectedOutput: `deployment.apps/foo
Fake ViewHistory Output
`,
expectedRevision: int64(0),
},
"should display ViewHistory output for a single revision": {
flags: map[string]string{"revision": "2"},
expectedOutput: `deployment.apps/foo with revision #2
Fake ViewHistory Output
`,
expectedRevision: int64(2),
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
fhv := setupFakeHistoryViewer(tt)
var actualNamespace, actualName *string
var actualRevision *int64
fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) {
actualNamespace = &namespace
actualName = &name
actualRevision = &revision
return "Fake ViewHistory Output\n", nil
}
streams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdRolloutHistory(tf, streams)
for k, v := range tc.flags {
cmd.Flags().Set(k, v)
}
cmd.Run(cmd, []string{"deployment/foo"})
expectedErrorOutput := ""
if errBuf.String() != expectedErrorOutput {
tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
}
if buf.String() != tc.expectedOutput {
tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
}
expectedNamespace := "test"
if actualNamespace == nil || *actualNamespace != expectedNamespace {
tt.Fatalf("expected ViewHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace)
}
expectedName := "foo"
if actualName == nil || *actualName != expectedName {
tt.Fatalf("expected ViewHistory to have been called with name %s, but it was %v", expectedName, *actualName)
}
if actualRevision == nil {
tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was ", tc.expectedRevision)
} else if *actualRevision != tc.expectedRevision {
tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was %v", tc.expectedRevision, *actualRevision)
}
})
}
}
func TestMultipleResourceRolloutHistory(t *testing.T) {
ns := scheme.Codecs.WithoutConversion()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
tf.Client = &RolloutPauseRESTClient{
RESTClient: &fake.RESTClient{
GroupVersion: rolloutPauseGroupVersionEncoder,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test/deployments/foo" && m == "GET":
responseDeployment := &appsv1.Deployment{}
responseDeployment.Name = "foo"
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
case p == "/namespaces/test/deployments/bar" && m == "GET":
responseDeployment := &appsv1.Deployment{}
responseDeployment.Name = "bar"
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
},
}
testCases := map[string]struct {
flags map[string]string
expectedOutput string
}{
"should display ViewHistory output for all revisions": {
expectedOutput: `deployment.apps/foo
Fake ViewHistory Output
deployment.apps/bar
Fake ViewHistory Output
`,
},
"should display ViewHistory output for a single revision": {
flags: map[string]string{"revision": "2"},
expectedOutput: `deployment.apps/foo with revision #2
Fake ViewHistory Output
deployment.apps/bar with revision #2
Fake ViewHistory Output
`,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
fhv := setupFakeHistoryViewer(tt)
fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) {
return "Fake ViewHistory Output\n", nil
}
streams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdRolloutHistory(tf, streams)
for k, v := range tc.flags {
cmd.Flags().Set(k, v)
}
cmd.Run(cmd, []string{"deployment/foo", "deployment/bar"})
expectedErrorOutput := ""
if errBuf.String() != expectedErrorOutput {
tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
}
if buf.String() != tc.expectedOutput {
tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
}
})
}
}
func TestRolloutHistoryWithOutput(t *testing.T) {
ns := scheme.Codecs.WithoutConversion()
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
tf.Client = &RolloutPauseRESTClient{
RESTClient: &fake.RESTClient{
GroupVersion: rolloutPauseGroupVersionEncoder,
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test/deployments/foo" && m == "GET":
responseDeployment := &appsv1.Deployment{}
responseDeployment.Name = "foo"
body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
},
}
testCases := map[string]struct {
flags map[string]string
expectedOutput string
}{
"json": {
flags: map[string]string{"revision": "2", "output": "json"},
expectedOutput: `{
"kind": "ReplicaSet",
"apiVersion": "apps/v1",
"metadata": {
"name": "rev2",
"creationTimestamp": null
},
"spec": {
"selector": null,
"template": {
"metadata": {
"creationTimestamp": null
},
"spec": {
"containers": null
}
}
},
"status": {
"replicas": 0
}
}
`,
},
"yaml": {
flags: map[string]string{"revision": "2", "output": "yaml"},
expectedOutput: `apiVersion: apps/v1
kind: ReplicaSet
metadata:
creationTimestamp: null
name: rev2
spec:
selector: null
template:
metadata:
creationTimestamp: null
spec:
containers: null
status:
replicas: 0
`,
},
"yaml all revisions": {
flags: map[string]string{"output": "yaml"},
expectedOutput: `apiVersion: apps/v1
kind: ReplicaSet
metadata:
creationTimestamp: null
name: rev1
spec:
selector: null
template:
metadata:
creationTimestamp: null
spec:
containers: null
status:
replicas: 0
---
apiVersion: apps/v1
kind: ReplicaSet
metadata:
creationTimestamp: null
name: rev2
spec:
selector: null
template:
metadata:
creationTimestamp: null
spec:
containers: null
status:
replicas: 0
`,
},
"name": {
flags: map[string]string{"output": "name"},
expectedOutput: `replicaset.apps/rev1
replicaset.apps/rev2
`,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
fhv := setupFakeHistoryViewer(t)
var actualNamespace, actualName *string
fhv.getHistoryFn = func(namespace, name string) (map[int64]runtime.Object, error) {
actualNamespace = &namespace
actualName = &name
return map[int64]runtime.Object{
1: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev1"}},
2: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev2"}},
}, nil
}
streams, _, buf, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdRolloutHistory(tf, streams)
for k, v := range tc.flags {
cmd.Flags().Set(k, v)
}
cmd.Run(cmd, []string{"deployment/foo"})
expectedErrorOutput := ""
if errBuf.String() != expectedErrorOutput {
t.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
}
if buf.String() != tc.expectedOutput {
t.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
}
expectedNamespace := "test"
if actualNamespace == nil || *actualNamespace != expectedNamespace {
t.Fatalf("expected GetHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace)
}
expectedName := "foo"
if actualName == nil || *actualName != expectedName {
t.Fatalf("expected GetHistory to have been called with name %s, but it was %v", expectedName, *actualName)
}
})
}
}
func TestValidate(t *testing.T) {
opts := RolloutHistoryOptions{
Revision: 0,
Resources: []string{"deployment/foo"},
}
if err := opts.Validate(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
opts.Revision = -1
expectedError := "revision must be a positive integer: -1"
if err := opts.Validate(); err == nil {
t.Fatalf("unexpected non error")
} else if err.Error() != expectedError {
t.Fatalf("expected error %s, but got %s", expectedError, err.Error())
}
opts.Revision = 0
opts.Resources = []string{}
expectedError = "required resource not specified"
if err := opts.Validate(); err == nil {
t.Fatalf("unexpected non error")
} else if err.Error() != expectedError {
t.Fatalf("expected error %s, but got %s", expectedError, err.Error())
}
}

View File

@ -25,7 +25,6 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
@ -35,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/kubernetes"
clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
"k8s.io/klog/v2"
"k8s.io/kubectl/pkg/apps"
"k8s.io/kubectl/pkg/describe"
deploymentutil "k8s.io/kubectl/pkg/util/deployment"
@ -48,6 +48,7 @@ const (
// HistoryViewer provides an interface for resources have historical information.
type HistoryViewer interface {
ViewHistory(namespace, name string, revision int64) (string, error)
GetHistory(namespace, name string) (map[int64]runtime.Object, error)
}
type HistoryVisitor struct {
@ -101,24 +102,16 @@ type DeploymentHistoryViewer struct {
// ViewHistory returns a revision-to-replicaset map as the revision history of a deployment
// TODO: this should be a describer
func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
versionedAppsClient := h.c.AppsV1()
deployment, err := versionedAppsClient.Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
if err != nil {
return "", fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
}
_, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, versionedAppsClient)
if err != nil {
return "", fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err)
}
allRSs := allOldRSs
if newRS != nil {
allRSs = append(allRSs, newRS)
return "", err
}
historyInfo := make(map[int64]*corev1.PodTemplateSpec)
for _, rs := range allRSs {
v, err := deploymentutil.Revision(rs)
if err != nil {
klog.Warningf("unable to get revision from replicaset %s for deployment %s in namespace %s: %v", rs.Name, name, namespace, err)
continue
}
historyInfo[v] = &rs.Spec.Template
@ -165,6 +158,26 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i
})
}
// GetHistory returns the ReplicaSet revisions associated with a Deployment
func (h *DeploymentHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
if err != nil {
return nil, err
}
result := make(map[int64]runtime.Object)
for _, rs := range allRSs {
v, err := deploymentutil.Revision(rs)
if err != nil {
klog.Warningf("unable to get revision from replicaset %s for deployment %s in namespace %s: %v", rs.Name, name, namespace, err)
continue
}
result[v] = rs
}
return result, nil
}
func printTemplate(template *corev1.PodTemplateSpec) (string, error) {
buf := bytes.NewBuffer([]byte{})
w := describe.NewPrefixWriter(buf)
@ -192,6 +205,25 @@ func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision in
})
}
// GetHistory returns the revisions associated with a DaemonSet
func (h *DaemonSetHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
if err != nil {
return nil, err
}
result := make(map[int64]runtime.Object)
for _, h := range history {
applied, err := applyDaemonSetHistory(ds, h)
if err != nil {
return nil, err
}
result[h.Revision] = applied
}
return result, nil
}
// 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) {
@ -259,6 +291,42 @@ func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision
})
}
// GetHistory returns the revisions associated with a StatefulSet
func (h *StatefulSetHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
if err != nil {
return nil, err
}
result := make(map[int64]runtime.Object)
for _, h := range history {
applied, err := applyStatefulSetHistory(sts, h)
if err != nil {
return nil, err
}
result[h.Revision] = applied
}
return result, nil
}
func getDeploymentReplicaSets(apps clientappsv1.AppsV1Interface, namespace, name string) ([]*appsv1.ReplicaSet, error) {
deployment, err := apps.Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
}
_, oldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, apps)
if err != nil {
return nil, fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err)
}
if newRS == nil {
return oldRSs, nil
}
return append(oldRSs, newRS), nil
}
// controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
// TODO: Rename this to controllerHistory when other controllers have been upgraded
func controlledHistoryV1(

View File

@ -18,6 +18,7 @@ package polymorphichelpers
import (
"context"
"fmt"
"reflect"
"testing"
@ -27,6 +28,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/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/client-go/kubernetes/fake"
)
@ -52,6 +54,140 @@ func TestHistoryViewerFor(t *testing.T) {
}
}
func TestViewDeploymentHistory(t *testing.T) {
trueVar := true
replicas := int32(1)
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "moons",
Namespace: "default",
UID: "fc7e66ad-eacc-4413-8277-e22276eacce6",
Labels: map[string]string{"foo": "bar"},
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Replicas: &replicas,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "test",
Image: fmt.Sprintf("foo:1"),
}}},
},
},
}
fakeClientSet := fake.NewSimpleClientset(deployment)
replicaSets := map[int64]*appsv1.ReplicaSet{}
var i int64
for i = 1; i < 5; i++ {
rs := &appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("moons-%d", i),
Namespace: "default",
UID: types.UID(fmt.Sprintf("00000000-0000-0000-0000-00000000000%d", i)),
Labels: map[string]string{"foo": "bar"},
OwnerReferences: []metav1.OwnerReference{{"apps/v1", "Deployment", deployment.Name, deployment.UID, &trueVar, nil}},
Annotations: map[string]string{
"deployment.kubernetes.io/revision": fmt.Sprintf("%d", i),
},
},
Spec: appsv1.ReplicaSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Replicas: &replicas,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "test",
Image: fmt.Sprintf("foo:%d", i),
}}},
},
},
}
if i == 3 {
rs.ObjectMeta.Annotations[ChangeCauseAnnotation] = "foo change cause"
} else if i == 4 {
rs.ObjectMeta.Annotations[ChangeCauseAnnotation] = "bar change cause"
}
fakeClientSet.AppsV1().ReplicaSets("default").Create(context.TODO(), rs, metav1.CreateOptions{})
replicaSets[i] = rs
}
viewer := DeploymentHistoryViewer{fakeClientSet}
t.Run("should show revisions list if the revision is not specified", func(t *testing.T) {
result, err := viewer.ViewHistory("default", "moons", 0)
if err != nil {
t.Fatalf("error getting history for Deployment moons: %v", err)
}
expected := `REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 foo change cause
4 bar change cause
`
if result != expected {
t.Fatalf("unexpected output (%v was expected but got %v)", expected, result)
}
})
t.Run("should describe a single revision", func(t *testing.T) {
result, err := viewer.ViewHistory("default", "moons", 3)
if err != nil {
t.Fatalf("error getting history for Deployment moons: %v", err)
}
expected := `Pod Template:
Labels: foo=bar
Annotations: kubernetes.io/change-cause: foo change cause
Containers:
test:
Image: foo:3
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
`
if result != expected {
t.Fatalf("unexpected output (%v was expected but got %v)", expected, result)
}
})
t.Run("should get history", func(t *testing.T) {
result, err := viewer.GetHistory("default", "moons")
if err != nil {
t.Fatalf("error getting history for Deployment moons: %v", err)
}
if len(result) != 4 {
t.Fatalf("unexpected history length (expected 4, got %d", len(result))
}
for i = 1; i < 4; i++ {
actual, found := result[i]
if !found {
t.Fatalf("revision %d not found in history", i)
}
expected := replicaSets[i]
if !reflect.DeepEqual(expected, actual) {
t.Errorf("history does not match. expected %+v, got %+v", expected, actual)
}
}
})
}
func TestViewHistory(t *testing.T) {
t.Run("for statefulSet", func(t *testing.T) {
@ -138,6 +274,25 @@ func TestViewHistory(t *testing.T) {
}
})
t.Run("should get history", func(t *testing.T) {
result, err := sts.GetHistory("default", "moons")
if err != nil {
t.Fatalf("error getting history for StatefulSet moons: %v", err)
}
if len(result) != 1 {
t.Fatalf("unexpected history length (expected 1, got %d", len(result))
}
actual, found := result[1]
if !found {
t.Fatalf("revision 1 not found in history")
}
expected := ssStub
if !reflect.DeepEqual(expected, actual) {
t.Errorf("history does not match. expected %+v, got %+v", expected, actual)
}
})
})
t.Run("for daemonSet", func(t *testing.T) {
@ -188,7 +343,7 @@ func TestViewHistory(t *testing.T) {
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)
t.Fatalf("error getting ViewHistory for DaemonSet moons: %v", err)
}
expected := `REVISION CHANGE-CAUSE
@ -203,7 +358,7 @@ func TestViewHistory(t *testing.T) {
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)
t.Fatalf("error getting ViewHistory for DaemonSet moons: %v", err)
}
expected := `Pod Template:
@ -223,6 +378,25 @@ func TestViewHistory(t *testing.T) {
}
})
t.Run("should get history", func(t *testing.T) {
result, err := daemonSetHistoryViewer.GetHistory("default", "moons")
if err != nil {
t.Fatalf("error getting history for DaemonSet moons: %v", err)
}
if len(result) != 1 {
t.Fatalf("unexpected history length (expected 1, got %d", len(result))
}
actual, found := result[1]
if !found {
t.Fatalf("revision 1 not found in history")
}
expected := daemonSetStub
if !reflect.DeepEqual(expected, actual) {
t.Errorf("history does not match. expected %+v, got %+v", expected, actual)
}
})
})
}

View File

@ -83,6 +83,23 @@ run_daemonset_history_tests() {
kube::test::wait_object_assert daemonset "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_DAEMONSET_R2_2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{${container_len:?}}}{{end}}" "2"
kube::test::wait_object_assert controllerrevisions "{{range.items}}{{${annotations_field:?}}}:{{end}}" ".*rollingupdate-daemonset-rv2.yaml --record.*"
# Get rollout history
output_message=$(kubectl rollout history daemonset)
kube::test::if_has_string "${output_message}" "daemonset.apps/bind"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "1 kubectl apply"
kube::test::if_has_string "${output_message}" "2 kubectl apply"
# Get rollout history for a single revision
output_message=$(kubectl rollout history daemonset --revision=1)
kube::test::if_has_string "${output_message}" "daemonset.apps/bind with revision #1"
kube::test::if_has_string "${output_message}" "Pod Template:"
kube::test::if_has_string "${output_message}" "${IMAGE_PAUSE_V2}"
# Get rollout history for a different single revision
output_message=$(kubectl rollout history daemonset --revision=2)
kube::test::if_has_string "${output_message}" "daemonset.apps/bind with revision #2"
kube::test::if_has_string "${output_message}" "Pod Template:"
kube::test::if_has_string "${output_message}" "${IMAGE_DAEMONSET_R2}"
kube::test::if_has_string "${output_message}" "${IMAGE_DAEMONSET_R2_2}"
# Rollback to revision 1 with dry-run - should be no-op
kubectl rollout undo daemonset --dry-run=client "${kube_flags[@]:?}"
kubectl rollout undo daemonset --dry-run=server "${kube_flags[@]:?}"
@ -93,6 +110,12 @@ run_daemonset_history_tests() {
kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]:?}"
kube::test::wait_object_assert daemonset "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_PAUSE_V2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{${container_len:?}}}{{end}}" "1"
# Get rollout history
output_message=$(kubectl rollout history daemonset)
kube::test::if_has_string "${output_message}" "daemonset.apps/bind"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "2 kubectl apply"
kube::test::if_has_string "${output_message}" "3 kubectl apply"
# Rollback to revision 1000000 - should fail
output_message=$(! kubectl rollout undo daemonset --to-revision=1000000 "${kube_flags[@]:?}" 2>&1)
kube::test::if_has_string "${output_message}" "unable to find specified revision"
@ -103,6 +126,12 @@ run_daemonset_history_tests() {
kube::test::wait_object_assert daemonset "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
kube::test::wait_object_assert daemonset "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_DAEMONSET_R2_2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{${container_len:?}}}{{end}}" "2"
# Get rollout history
output_message=$(kubectl rollout history daemonset)
kube::test::if_has_string "${output_message}" "daemonset.apps/bind"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "3 kubectl apply"
kube::test::if_has_string "${output_message}" "4 kubectl apply"
# Clean up
kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]:?}"
@ -440,6 +469,40 @@ run_deployment_tests() {
kubectl delete configmap test-set-env-config "${kube_flags[@]:?}"
kubectl delete secret test-set-env-secret "${kube_flags[@]:?}"
### Get rollout history
# Pre-condition: no deployment exists
kube::test::get_object_assert deployment "{{range.items}}{{${id_field:?}}}:{{end}}" ''
# Create a deployment
kubectl create -f hack/testdata/deployment-multicontainer.yaml "${kube_flags[@]:?}"
kube::test::get_object_assert deployment "{{range.items}}{{${id_field:?}}}:{{end}}" 'nginx-deployment:'
kube::test::get_object_assert deployment "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_DEPLOYMENT_R1}:"
kube::test::get_object_assert deployment "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_PERL}:"
# Set the deployment's image
kubectl set image deployment nginx-deployment nginx="${IMAGE_DEPLOYMENT_R2}" "${kube_flags[@]:?}"
kube::test::get_object_assert deployment "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_DEPLOYMENT_R2}:"
kube::test::get_object_assert deployment "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_PERL}:"
# Get rollout history
output_message=$(kubectl rollout history deployment nginx-deployment)
kube::test::if_has_string "${output_message}" "deployment.apps/nginx-deployment"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "1 <none>"
kube::test::if_has_string "${output_message}" "2 <none>"
kube::test::if_has_not_string "${output_message}" "3 <none>"
# Get rollout history for a single revision
output_message=$(kubectl rollout history deployment nginx-deployment --revision=1)
kube::test::if_has_string "${output_message}" "deployment.apps/nginx-deployment with revision #1"
kube::test::if_has_string "${output_message}" "Pod Template:"
kube::test::if_has_string "${output_message}" "${IMAGE_DEPLOYMENT_R1}"
kube::test::if_has_string "${output_message}" "${IMAGE_PERL}"
# Get rollout history for a different single revision
output_message=$(kubectl rollout history deployment nginx-deployment --revision=2)
kube::test::if_has_string "${output_message}" "deployment.apps/nginx-deployment with revision #2"
kube::test::if_has_string "${output_message}" "Pod Template:"
kube::test::if_has_string "${output_message}" "${IMAGE_DEPLOYMENT_R2}"
kube::test::if_has_string "${output_message}" "${IMAGE_PERL}"
# Clean up
kubectl delete deployment nginx-deployment "${kube_flags[@]:?}"
set +o nounset
set +o errexit
}
@ -468,6 +531,23 @@ run_statefulset_history_tests() {
kube::test::wait_object_assert statefulset "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_PAUSE_V2}:"
kube::test::get_object_assert statefulset "{{range.items}}{{${container_len:?}}}{{end}}" "2"
kube::test::wait_object_assert controllerrevisions "{{range.items}}{{${annotations_field:?}}}:{{end}}" ".*rollingupdate-statefulset-rv2.yaml --record.*"
# Get rollout history
output_message=$(kubectl rollout history statefulset)
kube::test::if_has_string "${output_message}" "statefulset.apps/nginx"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "1 kubectl apply"
kube::test::if_has_string "${output_message}" "2 kubectl apply"
# Get rollout history for a single revision
output_message=$(kubectl rollout history statefulset --revision=1)
kube::test::if_has_string "${output_message}" "statefulset.apps/nginx with revision #1"
kube::test::if_has_string "${output_message}" "Pod Template:"
kube::test::if_has_string "${output_message}" "${IMAGE_STATEFULSET_R1}"
# Get rollout history for a different single revision
output_message=$(kubectl rollout history statefulset --revision=2)
kube::test::if_has_string "${output_message}" "statefulset.apps/nginx with revision #2"
kube::test::if_has_string "${output_message}" "Pod Template:"
kube::test::if_has_string "${output_message}" "${IMAGE_STATEFULSET_R2}"
kube::test::if_has_string "${output_message}" "${IMAGE_PAUSE_V2}"
# Rollback to revision 1 with dry-run - should be no-op
kubectl rollout undo statefulset --dry-run=client "${kube_flags[@]:?}"
kubectl rollout undo statefulset --dry-run=server "${kube_flags[@]:?}"
@ -478,6 +558,12 @@ run_statefulset_history_tests() {
kubectl rollout undo statefulset --to-revision=1 "${kube_flags[@]:?}"
kube::test::wait_object_assert statefulset "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_STATEFULSET_R1}:"
kube::test::get_object_assert statefulset "{{range.items}}{{${container_len:?}}}{{end}}" "1"
# Get rollout history
output_message=$(kubectl rollout history statefulset)
kube::test::if_has_string "${output_message}" "statefulset.apps/nginx"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "2 kubectl apply"
kube::test::if_has_string "${output_message}" "3 kubectl apply"
# Rollback to revision 1000000 - should fail
output_message=$(! kubectl rollout undo statefulset --to-revision=1000000 "${kube_flags[@]:?}" 2>&1)
kube::test::if_has_string "${output_message}" "unable to find specified revision"
@ -488,6 +574,12 @@ run_statefulset_history_tests() {
kube::test::wait_object_assert statefulset "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_STATEFULSET_R2}:"
kube::test::wait_object_assert statefulset "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_PAUSE_V2}:"
kube::test::get_object_assert statefulset "{{range.items}}{{${container_len:?}}}{{end}}" "2"
# Get rollout history
output_message=$(kubectl rollout history statefulset)
kube::test::if_has_string "${output_message}" "statefulset.apps/nginx"
kube::test::if_has_string "${output_message}" "REVISION CHANGE-CAUSE"
kube::test::if_has_string "${output_message}" "3 kubectl apply"
kube::test::if_has_string "${output_message}" "4 kubectl apply"
# Clean up - delete newest configuration
kubectl delete -f hack/testdata/rollingupdate-statefulset-rv2.yaml "${kube_flags[@]:?}"
# Post-condition: no pods from statefulset controller