...

Source file src/k8s.io/kubectl/pkg/cmd/rollout/rollout_history_test.go

Documentation: k8s.io/kubectl/pkg/cmd/rollout

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package rollout
    18  
    19  import (
    20  	"bytes"
    21  	"io"
    22  	"net/http"
    23  	"testing"
    24  
    25  	appsv1 "k8s.io/api/apps/v1"
    26  	"k8s.io/apimachinery/pkg/api/meta"
    27  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/cli-runtime/pkg/genericclioptions"
    30  	"k8s.io/cli-runtime/pkg/genericiooptions"
    31  	"k8s.io/client-go/rest/fake"
    32  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    33  	"k8s.io/kubectl/pkg/polymorphichelpers"
    34  	"k8s.io/kubectl/pkg/scheme"
    35  )
    36  
    37  type fakeHistoryViewer struct {
    38  	viewHistoryFn func(namespace, name string, revision int64) (string, error)
    39  	getHistoryFn  func(namespace, name string) (map[int64]runtime.Object, error)
    40  }
    41  
    42  func (h *fakeHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
    43  	return h.viewHistoryFn(namespace, name, revision)
    44  }
    45  
    46  func (h *fakeHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
    47  	return h.getHistoryFn(namespace, name)
    48  }
    49  
    50  func setupFakeHistoryViewer(t *testing.T) *fakeHistoryViewer {
    51  	fhv := &fakeHistoryViewer{
    52  		viewHistoryFn: func(namespace, name string, revision int64) (string, error) {
    53  			t.Fatalf("ViewHistory mock not implemented")
    54  			return "", nil
    55  		},
    56  		getHistoryFn: func(namespace, name string) (map[int64]runtime.Object, error) {
    57  			t.Fatalf("GetHistory mock not implemented")
    58  			return nil, nil
    59  		},
    60  	}
    61  	polymorphichelpers.HistoryViewerFn = func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (polymorphichelpers.HistoryViewer, error) {
    62  		return fhv, nil
    63  	}
    64  	return fhv
    65  }
    66  
    67  func TestRolloutHistory(t *testing.T) {
    68  	ns := scheme.Codecs.WithoutConversion()
    69  	tf := cmdtesting.NewTestFactory().WithNamespace("test")
    70  	defer tf.Cleanup()
    71  
    72  	info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
    73  	encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
    74  
    75  	tf.Client = &RolloutPauseRESTClient{
    76  		RESTClient: &fake.RESTClient{
    77  			GroupVersion:         rolloutPauseGroupVersionEncoder,
    78  			NegotiatedSerializer: ns,
    79  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
    80  				switch p, m := req.URL.Path, req.Method; {
    81  				case p == "/namespaces/test/deployments/foo" && m == "GET":
    82  					responseDeployment := &appsv1.Deployment{}
    83  					responseDeployment.Name = "foo"
    84  					body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
    85  					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
    86  				default:
    87  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
    88  					return nil, nil
    89  				}
    90  			}),
    91  		},
    92  	}
    93  
    94  	testCases := map[string]struct {
    95  		flags            map[string]string
    96  		expectedOutput   string
    97  		expectedRevision int64
    98  	}{
    99  		"should display ViewHistory output for all revisions": {
   100  			expectedOutput: `deployment.apps/foo 
   101  Fake ViewHistory Output
   102  
   103  `,
   104  			expectedRevision: int64(0),
   105  		},
   106  		"should display ViewHistory output for a single revision": {
   107  			flags: map[string]string{"revision": "2"},
   108  			expectedOutput: `deployment.apps/foo with revision #2
   109  Fake ViewHistory Output
   110  
   111  `,
   112  			expectedRevision: int64(2),
   113  		},
   114  	}
   115  
   116  	for name, tc := range testCases {
   117  		t.Run(name, func(tt *testing.T) {
   118  			fhv := setupFakeHistoryViewer(tt)
   119  			var actualNamespace, actualName *string
   120  			var actualRevision *int64
   121  			fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) {
   122  				actualNamespace = &namespace
   123  				actualName = &name
   124  				actualRevision = &revision
   125  				return "Fake ViewHistory Output\n", nil
   126  			}
   127  
   128  			streams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
   129  			cmd := NewCmdRolloutHistory(tf, streams)
   130  			for k, v := range tc.flags {
   131  				cmd.Flags().Set(k, v)
   132  			}
   133  			cmd.Run(cmd, []string{"deployment/foo"})
   134  
   135  			expectedErrorOutput := ""
   136  			if errBuf.String() != expectedErrorOutput {
   137  				tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
   138  			}
   139  
   140  			if buf.String() != tc.expectedOutput {
   141  				tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
   142  			}
   143  
   144  			expectedNamespace := "test"
   145  			if actualNamespace == nil || *actualNamespace != expectedNamespace {
   146  				tt.Fatalf("expected ViewHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace)
   147  			}
   148  
   149  			expectedName := "foo"
   150  			if actualName == nil || *actualName != expectedName {
   151  				tt.Fatalf("expected ViewHistory to have been called with name %s, but it was %v", expectedName, *actualName)
   152  			}
   153  
   154  			if actualRevision == nil {
   155  				tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was ", tc.expectedRevision)
   156  			} else if *actualRevision != tc.expectedRevision {
   157  				tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was %v", tc.expectedRevision, *actualRevision)
   158  			}
   159  		})
   160  	}
   161  }
   162  
   163  func TestMultipleResourceRolloutHistory(t *testing.T) {
   164  	ns := scheme.Codecs.WithoutConversion()
   165  	tf := cmdtesting.NewTestFactory().WithNamespace("test")
   166  	defer tf.Cleanup()
   167  
   168  	info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
   169  	encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
   170  
   171  	tf.Client = &RolloutPauseRESTClient{
   172  		RESTClient: &fake.RESTClient{
   173  			GroupVersion:         rolloutPauseGroupVersionEncoder,
   174  			NegotiatedSerializer: ns,
   175  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   176  				switch p, m := req.URL.Path, req.Method; {
   177  				case p == "/namespaces/test/deployments/foo" && m == "GET":
   178  					responseDeployment := &appsv1.Deployment{}
   179  					responseDeployment.Name = "foo"
   180  					body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
   181  					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   182  				case p == "/namespaces/test/deployments/bar" && m == "GET":
   183  					responseDeployment := &appsv1.Deployment{}
   184  					responseDeployment.Name = "bar"
   185  					body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
   186  					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   187  				default:
   188  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
   189  					return nil, nil
   190  				}
   191  			}),
   192  		},
   193  	}
   194  
   195  	testCases := map[string]struct {
   196  		flags          map[string]string
   197  		expectedOutput string
   198  	}{
   199  		"should display ViewHistory output for all revisions": {
   200  			expectedOutput: `deployment.apps/foo 
   201  Fake ViewHistory Output
   202  
   203  deployment.apps/bar 
   204  Fake ViewHistory Output
   205  
   206  `,
   207  		},
   208  		"should display ViewHistory output for a single revision": {
   209  			flags: map[string]string{"revision": "2"},
   210  			expectedOutput: `deployment.apps/foo with revision #2
   211  Fake ViewHistory Output
   212  
   213  deployment.apps/bar with revision #2
   214  Fake ViewHistory Output
   215  
   216  `,
   217  		},
   218  	}
   219  
   220  	for name, tc := range testCases {
   221  		t.Run(name, func(tt *testing.T) {
   222  			fhv := setupFakeHistoryViewer(tt)
   223  			fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) {
   224  				return "Fake ViewHistory Output\n", nil
   225  			}
   226  
   227  			streams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
   228  			cmd := NewCmdRolloutHistory(tf, streams)
   229  			for k, v := range tc.flags {
   230  				cmd.Flags().Set(k, v)
   231  			}
   232  			cmd.Run(cmd, []string{"deployment/foo", "deployment/bar"})
   233  
   234  			expectedErrorOutput := ""
   235  			if errBuf.String() != expectedErrorOutput {
   236  				tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
   237  			}
   238  
   239  			if buf.String() != tc.expectedOutput {
   240  				tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
   241  			}
   242  		})
   243  	}
   244  }
   245  
   246  func TestRolloutHistoryWithOutput(t *testing.T) {
   247  	ns := scheme.Codecs.WithoutConversion()
   248  	tf := cmdtesting.NewTestFactory().WithNamespace("test")
   249  	defer tf.Cleanup()
   250  
   251  	info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON)
   252  	encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder)
   253  
   254  	tf.Client = &RolloutPauseRESTClient{
   255  		RESTClient: &fake.RESTClient{
   256  			GroupVersion:         rolloutPauseGroupVersionEncoder,
   257  			NegotiatedSerializer: ns,
   258  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   259  				switch p, m := req.URL.Path, req.Method; {
   260  				case p == "/namespaces/test/deployments/foo" && m == "GET":
   261  					responseDeployment := &appsv1.Deployment{}
   262  					responseDeployment.Name = "foo"
   263  					body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment))))
   264  					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
   265  				default:
   266  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
   267  					return nil, nil
   268  				}
   269  			}),
   270  		},
   271  	}
   272  
   273  	testCases := map[string]struct {
   274  		flags          map[string]string
   275  		expectedOutput string
   276  	}{
   277  		"json": {
   278  			flags: map[string]string{"revision": "2", "output": "json"},
   279  			expectedOutput: `{
   280      "kind": "ReplicaSet",
   281      "apiVersion": "apps/v1",
   282      "metadata": {
   283          "name": "rev2",
   284          "creationTimestamp": null
   285      },
   286      "spec": {
   287          "selector": null,
   288          "template": {
   289              "metadata": {
   290                  "creationTimestamp": null
   291              },
   292              "spec": {
   293                  "containers": null
   294              }
   295          }
   296      },
   297      "status": {
   298          "replicas": 0
   299      }
   300  }
   301  `,
   302  		},
   303  		"yaml": {
   304  			flags: map[string]string{"revision": "2", "output": "yaml"},
   305  			expectedOutput: `apiVersion: apps/v1
   306  kind: ReplicaSet
   307  metadata:
   308    creationTimestamp: null
   309    name: rev2
   310  spec:
   311    selector: null
   312    template:
   313      metadata:
   314        creationTimestamp: null
   315      spec:
   316        containers: null
   317  status:
   318    replicas: 0
   319  `,
   320  		},
   321  		"yaml all revisions": {
   322  			flags: map[string]string{"output": "yaml"},
   323  			expectedOutput: `apiVersion: apps/v1
   324  kind: ReplicaSet
   325  metadata:
   326    creationTimestamp: null
   327    name: rev1
   328  spec:
   329    selector: null
   330    template:
   331      metadata:
   332        creationTimestamp: null
   333      spec:
   334        containers: null
   335  status:
   336    replicas: 0
   337  ---
   338  apiVersion: apps/v1
   339  kind: ReplicaSet
   340  metadata:
   341    creationTimestamp: null
   342    name: rev2
   343  spec:
   344    selector: null
   345    template:
   346      metadata:
   347        creationTimestamp: null
   348      spec:
   349        containers: null
   350  status:
   351    replicas: 0
   352  `,
   353  		},
   354  		"name": {
   355  			flags: map[string]string{"output": "name"},
   356  			expectedOutput: `replicaset.apps/rev1
   357  replicaset.apps/rev2
   358  `,
   359  		},
   360  	}
   361  
   362  	for name, tc := range testCases {
   363  		t.Run(name, func(t *testing.T) {
   364  			fhv := setupFakeHistoryViewer(t)
   365  			var actualNamespace, actualName *string
   366  			fhv.getHistoryFn = func(namespace, name string) (map[int64]runtime.Object, error) {
   367  				actualNamespace = &namespace
   368  				actualName = &name
   369  				return map[int64]runtime.Object{
   370  					1: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev1"}},
   371  					2: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev2"}},
   372  				}, nil
   373  			}
   374  
   375  			streams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
   376  			cmd := NewCmdRolloutHistory(tf, streams)
   377  			for k, v := range tc.flags {
   378  				cmd.Flags().Set(k, v)
   379  			}
   380  			cmd.Run(cmd, []string{"deployment/foo"})
   381  
   382  			expectedErrorOutput := ""
   383  			if errBuf.String() != expectedErrorOutput {
   384  				t.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String())
   385  			}
   386  
   387  			if buf.String() != tc.expectedOutput {
   388  				t.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String())
   389  			}
   390  
   391  			expectedNamespace := "test"
   392  			if actualNamespace == nil || *actualNamespace != expectedNamespace {
   393  				t.Fatalf("expected GetHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace)
   394  			}
   395  
   396  			expectedName := "foo"
   397  			if actualName == nil || *actualName != expectedName {
   398  				t.Fatalf("expected GetHistory to have been called with name %s, but it was %v", expectedName, *actualName)
   399  			}
   400  		})
   401  	}
   402  }
   403  
   404  func TestValidate(t *testing.T) {
   405  	opts := RolloutHistoryOptions{
   406  		Revision:  0,
   407  		Resources: []string{"deployment/foo"},
   408  	}
   409  	if err := opts.Validate(); err != nil {
   410  		t.Fatalf("unexpected error: %s", err)
   411  	}
   412  
   413  	opts.Revision = -1
   414  	expectedError := "revision must be a positive integer: -1"
   415  	if err := opts.Validate(); err == nil {
   416  		t.Fatalf("unexpected non error")
   417  	} else if err.Error() != expectedError {
   418  		t.Fatalf("expected error %s, but got %s", expectedError, err.Error())
   419  	}
   420  
   421  	opts.Revision = 0
   422  	opts.Resources = []string{}
   423  	expectedError = "required resource not specified"
   424  	if err := opts.Validate(); err == nil {
   425  		t.Fatalf("unexpected non error")
   426  	} else if err.Error() != expectedError {
   427  		t.Fatalf("expected error %s, but got %s", expectedError, err.Error())
   428  	}
   429  }
   430  

View as plain text