mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	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:
		@@ -18,6 +18,7 @@ package rollout
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,6 +154,44 @@ func (o *RolloutHistoryOptions) Run() error {
 | 
				
			|||||||
		return err
 | 
							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 {
 | 
						return r.Visit(func(info *resource.Info, err error) error {
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -25,7 +25,6 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	appsv1 "k8s.io/api/apps/v1"
 | 
						appsv1 "k8s.io/api/apps/v1"
 | 
				
			||||||
	corev1 "k8s.io/api/core/v1"
 | 
						corev1 "k8s.io/api/core/v1"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
						"k8s.io/apimachinery/pkg/api/meta"
 | 
				
			||||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/labels"
 | 
						"k8s.io/apimachinery/pkg/labels"
 | 
				
			||||||
@@ -35,6 +34,7 @@ import (
 | 
				
			|||||||
	"k8s.io/apimachinery/pkg/util/strategicpatch"
 | 
						"k8s.io/apimachinery/pkg/util/strategicpatch"
 | 
				
			||||||
	"k8s.io/client-go/kubernetes"
 | 
						"k8s.io/client-go/kubernetes"
 | 
				
			||||||
	clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
 | 
						clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
 | 
				
			||||||
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
	"k8s.io/kubectl/pkg/apps"
 | 
						"k8s.io/kubectl/pkg/apps"
 | 
				
			||||||
	"k8s.io/kubectl/pkg/describe"
 | 
						"k8s.io/kubectl/pkg/describe"
 | 
				
			||||||
	deploymentutil "k8s.io/kubectl/pkg/util/deployment"
 | 
						deploymentutil "k8s.io/kubectl/pkg/util/deployment"
 | 
				
			||||||
@@ -48,6 +48,7 @@ const (
 | 
				
			|||||||
// HistoryViewer provides an interface for resources have historical information.
 | 
					// HistoryViewer provides an interface for resources have historical information.
 | 
				
			||||||
type HistoryViewer interface {
 | 
					type HistoryViewer interface {
 | 
				
			||||||
	ViewHistory(namespace, name string, revision int64) (string, error)
 | 
						ViewHistory(namespace, name string, revision int64) (string, error)
 | 
				
			||||||
 | 
						GetHistory(namespace, name string) (map[int64]runtime.Object, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type HistoryVisitor struct {
 | 
					type HistoryVisitor struct {
 | 
				
			||||||
@@ -101,24 +102,16 @@ type DeploymentHistoryViewer struct {
 | 
				
			|||||||
// ViewHistory returns a revision-to-replicaset map as the revision history of a deployment
 | 
					// ViewHistory returns a revision-to-replicaset map as the revision history of a deployment
 | 
				
			||||||
// TODO: this should be a describer
 | 
					// TODO: this should be a describer
 | 
				
			||||||
func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
 | 
					func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
 | 
				
			||||||
	versionedAppsClient := h.c.AppsV1()
 | 
						allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
 | 
				
			||||||
	deployment, err := versionedAppsClient.Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
 | 
							return "", 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)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	historyInfo := make(map[int64]*corev1.PodTemplateSpec)
 | 
						historyInfo := make(map[int64]*corev1.PodTemplateSpec)
 | 
				
			||||||
	for _, rs := range allRSs {
 | 
						for _, rs := range allRSs {
 | 
				
			||||||
		v, err := deploymentutil.Revision(rs)
 | 
							v, err := deploymentutil.Revision(rs)
 | 
				
			||||||
		if err != nil {
 | 
							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
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		historyInfo[v] = &rs.Spec.Template
 | 
							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) {
 | 
					func printTemplate(template *corev1.PodTemplateSpec) (string, error) {
 | 
				
			||||||
	buf := bytes.NewBuffer([]byte{})
 | 
						buf := bytes.NewBuffer([]byte{})
 | 
				
			||||||
	w := describe.NewPrefixWriter(buf)
 | 
						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
 | 
					// printHistory returns the podTemplate of the given revision if it is non-zero
 | 
				
			||||||
// else returns the overall revisions
 | 
					// else returns the overall revisions
 | 
				
			||||||
func printHistory(history []*appsv1.ControllerRevision, revision int64, getPodTemplate func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error)) (string, error) {
 | 
					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
 | 
					// 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
 | 
					// TODO: Rename this to controllerHistory when other controllers have been upgraded
 | 
				
			||||||
func controlledHistoryV1(
 | 
					func controlledHistoryV1(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ package polymorphichelpers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,6 +28,7 @@ import (
 | 
				
			|||||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
						"k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/types"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/json"
 | 
						"k8s.io/apimachinery/pkg/util/json"
 | 
				
			||||||
	"k8s.io/client-go/kubernetes/fake"
 | 
						"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) {
 | 
					func TestViewHistory(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("for statefulSet", func(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) {
 | 
						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) {
 | 
							t.Run("should show revisions list if the revision is not specified", func(t *testing.T) {
 | 
				
			||||||
			result, err := daemonSetHistoryViewer.ViewHistory("default", "moons", 0)
 | 
								result, err := daemonSetHistoryViewer.ViewHistory("default", "moons", 0)
 | 
				
			||||||
			if err != nil {
 | 
								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
 | 
								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) {
 | 
							t.Run("should describe the revision if revision is specified", func(t *testing.T) {
 | 
				
			||||||
			result, err := daemonSetHistoryViewer.ViewHistory("default", "moons", 1)
 | 
								result, err := daemonSetHistoryViewer.ViewHistory("default", "moons", 1)
 | 
				
			||||||
			if err != nil {
 | 
								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:
 | 
								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)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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::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::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.*"
 | 
					  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
 | 
					  # 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=client "${kube_flags[@]:?}"
 | 
				
			||||||
  kubectl rollout undo daemonset --dry-run=server "${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[@]:?}"
 | 
					  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::wait_object_assert daemonset "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_PAUSE_V2}:"
 | 
				
			||||||
  kube::test::get_object_assert daemonset "{{range.items}}{{${container_len:?}}}{{end}}" "1"
 | 
					  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
 | 
					  # Rollback to revision 1000000 - should fail
 | 
				
			||||||
  output_message=$(! kubectl rollout undo daemonset --to-revision=1000000 "${kube_flags[@]:?}" 2>&1)
 | 
					  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"
 | 
					  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_field0:?}}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
 | 
				
			||||||
  kube::test::wait_object_assert daemonset "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_DAEMONSET_R2_2}:"
 | 
					  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::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
 | 
					  # Clean up
 | 
				
			||||||
  kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]:?}"
 | 
					  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 configmap test-set-env-config "${kube_flags[@]:?}"
 | 
				
			||||||
  kubectl delete secret test-set-env-secret "${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 nounset
 | 
				
			||||||
  set +o errexit
 | 
					  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::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::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.*"
 | 
					  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
 | 
					  # 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=client "${kube_flags[@]:?}"
 | 
				
			||||||
  kubectl rollout undo statefulset --dry-run=server "${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[@]:?}"
 | 
					  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::wait_object_assert statefulset "{{range.items}}{{${image_field0:?}}}:{{end}}" "${IMAGE_STATEFULSET_R1}:"
 | 
				
			||||||
  kube::test::get_object_assert statefulset "{{range.items}}{{${container_len:?}}}{{end}}" "1"
 | 
					  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
 | 
					  # Rollback to revision 1000000 - should fail
 | 
				
			||||||
  output_message=$(! kubectl rollout undo statefulset --to-revision=1000000 "${kube_flags[@]:?}" 2>&1)
 | 
					  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"
 | 
					  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_field0:?}}}:{{end}}" "${IMAGE_STATEFULSET_R2}:"
 | 
				
			||||||
  kube::test::wait_object_assert statefulset "{{range.items}}{{${image_field1:?}}}:{{end}}" "${IMAGE_PAUSE_V2}:"
 | 
					  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::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
 | 
					  # Clean up - delete newest configuration
 | 
				
			||||||
  kubectl delete -f hack/testdata/rollingupdate-statefulset-rv2.yaml "${kube_flags[@]:?}"
 | 
					  kubectl delete -f hack/testdata/rollingupdate-statefulset-rv2.yaml "${kube_flags[@]:?}"
 | 
				
			||||||
  # Post-condition: no pods from statefulset controller
 | 
					  # Post-condition: no pods from statefulset controller
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user