...

Source file src/sigs.k8s.io/cli-utils/cmd/status/cmdstatus_test.go

Documentation: sigs.k8s.io/cli-utils/cmd/status

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package status
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/spf13/cobra"
    15  	"github.com/stretchr/testify/assert"
    16  	"k8s.io/apimachinery/pkg/runtime/schema"
    17  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    18  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    19  	"sigs.k8s.io/cli-utils/pkg/apply/poller"
    20  	"sigs.k8s.io/cli-utils/pkg/inventory"
    21  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
    22  	pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
    23  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    24  	"sigs.k8s.io/cli-utils/pkg/manifestreader"
    25  	"sigs.k8s.io/cli-utils/pkg/object"
    26  )
    27  
    28  var (
    29  	inventoryTemplate = `
    30  kind: ConfigMap
    31  apiVersion: v1
    32  metadata:
    33    labels:
    34      cli-utils.sigs.k8s.io/inventory-id: test
    35    name: foo
    36    namespace: default
    37  `
    38  	depObject = object.ObjMetadata{
    39  		Name:      "foo",
    40  		Namespace: "default",
    41  		GroupKind: schema.GroupKind{
    42  			Group: "apps",
    43  			Kind:  "Deployment",
    44  		},
    45  	}
    46  
    47  	stsObject = object.ObjMetadata{
    48  		Name:      "bar",
    49  		Namespace: "default",
    50  		GroupKind: schema.GroupKind{
    51  			Group: "apps",
    52  			Kind:  "StatefulSet",
    53  		},
    54  	}
    55  )
    56  
    57  type fakePoller struct {
    58  	events []pollevent.Event
    59  }
    60  
    61  func (f *fakePoller) Poll(ctx context.Context, _ object.ObjMetadataSet,
    62  	_ polling.PollOptions) <-chan pollevent.Event {
    63  	eventChannel := make(chan pollevent.Event)
    64  	go func() {
    65  		defer close(eventChannel)
    66  		for _, e := range f.events {
    67  			eventChannel <- e
    68  		}
    69  		<-ctx.Done()
    70  	}()
    71  	return eventChannel
    72  }
    73  
    74  func TestCommand(t *testing.T) {
    75  	testCases := map[string]struct {
    76  		pollUntil      string
    77  		printer        string
    78  		timeout        time.Duration
    79  		input          string
    80  		inventory      object.ObjMetadataSet
    81  		events         []pollevent.Event
    82  		expectedErrMsg string
    83  		expectedOutput string
    84  	}{
    85  		"no inventory template": {
    86  			pollUntil:      "known",
    87  			printer:        "events",
    88  			input:          "",
    89  			expectedErrMsg: "Package uninitialized. Please run \"init\" command.",
    90  		},
    91  		"no inventory in live state": {
    92  			pollUntil:      "known",
    93  			printer:        "events",
    94  			input:          inventoryTemplate,
    95  			expectedOutput: "no resources found in the inventory\n",
    96  		},
    97  		"wait for all known": {
    98  			pollUntil: "known",
    99  			printer:   "events",
   100  			input:     inventoryTemplate,
   101  			inventory: object.ObjMetadataSet{
   102  				depObject,
   103  				stsObject,
   104  			},
   105  			events: []pollevent.Event{
   106  				{
   107  					Type: pollevent.ResourceUpdateEvent,
   108  					Resource: &pollevent.ResourceStatus{
   109  						Identifier: depObject,
   110  						Status:     status.InProgressStatus,
   111  						Message:    "inProgress",
   112  					},
   113  				},
   114  				{
   115  					Type: pollevent.ResourceUpdateEvent,
   116  					Resource: &pollevent.ResourceStatus{
   117  						Identifier: stsObject,
   118  						Status:     status.CurrentStatus,
   119  						Message:    "current",
   120  					},
   121  				},
   122  			},
   123  			expectedOutput: `
   124  foo/deployment.apps/default/foo is InProgress: inProgress
   125  foo/statefulset.apps/default/bar is Current: current
   126  `,
   127  		},
   128  		"wait for all current": {
   129  			pollUntil: "current",
   130  			printer:   "events",
   131  			input:     inventoryTemplate,
   132  			inventory: object.ObjMetadataSet{
   133  				depObject,
   134  				stsObject,
   135  			},
   136  			events: []pollevent.Event{
   137  				{
   138  					Type: pollevent.ResourceUpdateEvent,
   139  					Resource: &pollevent.ResourceStatus{
   140  						Identifier: depObject,
   141  						Status:     status.InProgressStatus,
   142  						Message:    "inProgress",
   143  					},
   144  				},
   145  				{
   146  					Type: pollevent.ResourceUpdateEvent,
   147  					Resource: &pollevent.ResourceStatus{
   148  						Identifier: stsObject,
   149  						Status:     status.InProgressStatus,
   150  						Message:    "inProgress",
   151  					},
   152  				},
   153  				{
   154  					Type: pollevent.ResourceUpdateEvent,
   155  					Resource: &pollevent.ResourceStatus{
   156  						Identifier: stsObject,
   157  						Status:     status.CurrentStatus,
   158  						Message:    "current",
   159  					},
   160  				},
   161  				{
   162  					Type: pollevent.ResourceUpdateEvent,
   163  					Resource: &pollevent.ResourceStatus{
   164  						Identifier: depObject,
   165  						Status:     status.CurrentStatus,
   166  						Message:    "current",
   167  					},
   168  				},
   169  			},
   170  			expectedOutput: `
   171  foo/deployment.apps/default/foo is InProgress: inProgress
   172  foo/statefulset.apps/default/bar is InProgress: inProgress
   173  foo/statefulset.apps/default/bar is Current: current
   174  foo/deployment.apps/default/foo is Current: current
   175  `,
   176  		},
   177  		"wait for all deleted": {
   178  			pollUntil: "deleted",
   179  			printer:   "events",
   180  			input:     inventoryTemplate,
   181  			inventory: object.ObjMetadataSet{
   182  				depObject,
   183  				stsObject,
   184  			},
   185  			events: []pollevent.Event{
   186  				{
   187  					Type: pollevent.ResourceUpdateEvent,
   188  					Resource: &pollevent.ResourceStatus{
   189  						Identifier: stsObject,
   190  						Status:     status.NotFoundStatus,
   191  						Message:    "notFound",
   192  					},
   193  				},
   194  				{
   195  					Type: pollevent.ResourceUpdateEvent,
   196  					Resource: &pollevent.ResourceStatus{
   197  						Identifier: depObject,
   198  						Status:     status.NotFoundStatus,
   199  						Message:    "notFound",
   200  					},
   201  				},
   202  			},
   203  			expectedOutput: `
   204  foo/statefulset.apps/default/bar is NotFound: notFound
   205  foo/deployment.apps/default/foo is NotFound: notFound
   206  `,
   207  		},
   208  		"forever with timeout": {
   209  			pollUntil: "forever",
   210  			printer:   "events",
   211  			timeout:   2 * time.Second,
   212  			input:     inventoryTemplate,
   213  			inventory: object.ObjMetadataSet{
   214  				depObject,
   215  				stsObject,
   216  			},
   217  			events: []pollevent.Event{
   218  				{
   219  					Type: pollevent.ResourceUpdateEvent,
   220  					Resource: &pollevent.ResourceStatus{
   221  						Identifier: stsObject,
   222  						Status:     status.InProgressStatus,
   223  						Message:    "inProgress",
   224  					},
   225  				},
   226  				{
   227  					Type: pollevent.ResourceUpdateEvent,
   228  					Resource: &pollevent.ResourceStatus{
   229  						Identifier: depObject,
   230  						Status:     status.InProgressStatus,
   231  						Message:    "inProgress",
   232  					},
   233  				},
   234  			},
   235  			expectedOutput: `
   236  foo/statefulset.apps/default/bar is InProgress: inProgress
   237  foo/deployment.apps/default/foo is InProgress: inProgress
   238  `,
   239  		},
   240  	}
   241  
   242  	jsonTestCases := map[string]struct {
   243  		pollUntil      string
   244  		printer        string
   245  		timeout        time.Duration
   246  		input          string
   247  		inventory      object.ObjMetadataSet
   248  		events         []pollevent.Event
   249  		expectedErrMsg string
   250  		expectedOutput []map[string]interface{}
   251  	}{
   252  		"wait for all known json": {
   253  			pollUntil: "known",
   254  			printer:   "json",
   255  			input:     inventoryTemplate,
   256  			inventory: object.ObjMetadataSet{
   257  				depObject,
   258  				stsObject,
   259  			},
   260  			events: []pollevent.Event{
   261  				{
   262  					Type: pollevent.ResourceUpdateEvent,
   263  					Resource: &pollevent.ResourceStatus{
   264  						Identifier: depObject,
   265  						Status:     status.InProgressStatus,
   266  						Message:    "inProgress",
   267  					},
   268  				},
   269  				{
   270  					Type: pollevent.ResourceUpdateEvent,
   271  					Resource: &pollevent.ResourceStatus{
   272  						Identifier: stsObject,
   273  						Status:     status.CurrentStatus,
   274  						Message:    "current",
   275  					},
   276  				},
   277  			},
   278  			expectedOutput: []map[string]interface{}{
   279  				{
   280  					"group":          "apps",
   281  					"kind":           "Deployment",
   282  					"namespace":      "default",
   283  					"name":           "foo",
   284  					"timestamp":      "",
   285  					"type":           "status",
   286  					"inventory-name": "foo",
   287  					"status":         "InProgress",
   288  					"message":        "inProgress",
   289  				},
   290  				{
   291  					"group":          "apps",
   292  					"kind":           "StatefulSet",
   293  					"namespace":      "default",
   294  					"name":           "bar",
   295  					"timestamp":      "",
   296  					"type":           "status",
   297  					"inventory-name": "foo",
   298  					"status":         "Current",
   299  					"message":        "current",
   300  				},
   301  			},
   302  		},
   303  		"wait for all current json": {
   304  			pollUntil: "current",
   305  			printer:   "json",
   306  			input:     inventoryTemplate,
   307  			inventory: object.ObjMetadataSet{
   308  				depObject,
   309  				stsObject,
   310  			},
   311  			events: []pollevent.Event{
   312  				{
   313  					Type: pollevent.ResourceUpdateEvent,
   314  					Resource: &pollevent.ResourceStatus{
   315  						Identifier: depObject,
   316  						Status:     status.InProgressStatus,
   317  						Message:    "inProgress",
   318  					},
   319  				},
   320  				{
   321  					Type: pollevent.ResourceUpdateEvent,
   322  					Resource: &pollevent.ResourceStatus{
   323  						Identifier: stsObject,
   324  						Status:     status.InProgressStatus,
   325  						Message:    "inProgress",
   326  					},
   327  				},
   328  				{
   329  					Type: pollevent.ResourceUpdateEvent,
   330  					Resource: &pollevent.ResourceStatus{
   331  						Identifier: stsObject,
   332  						Status:     status.CurrentStatus,
   333  						Message:    "current",
   334  					},
   335  				},
   336  				{
   337  					Type: pollevent.ResourceUpdateEvent,
   338  					Resource: &pollevent.ResourceStatus{
   339  						Identifier: depObject,
   340  						Status:     status.CurrentStatus,
   341  						Message:    "current",
   342  					},
   343  				},
   344  			},
   345  			expectedOutput: []map[string]interface{}{
   346  				{
   347  					"group":          "apps",
   348  					"kind":           "Deployment",
   349  					"namespace":      "default",
   350  					"name":           "foo",
   351  					"timestamp":      "",
   352  					"type":           "status",
   353  					"inventory-name": "foo",
   354  					"status":         "InProgress",
   355  					"message":        "inProgress",
   356  				},
   357  				{
   358  					"group":          "apps",
   359  					"kind":           "StatefulSet",
   360  					"namespace":      "default",
   361  					"name":           "bar",
   362  					"timestamp":      "",
   363  					"type":           "status",
   364  					"inventory-name": "foo",
   365  					"status":         "InProgress",
   366  					"message":        "inProgress",
   367  				},
   368  				{
   369  					"group":          "apps",
   370  					"kind":           "StatefulSet",
   371  					"namespace":      "default",
   372  					"name":           "bar",
   373  					"timestamp":      "",
   374  					"type":           "status",
   375  					"inventory-name": "foo",
   376  					"status":         "Current",
   377  					"message":        "current",
   378  				},
   379  				{
   380  					"group":          "apps",
   381  					"kind":           "Deployment",
   382  					"namespace":      "default",
   383  					"name":           "foo",
   384  					"timestamp":      "",
   385  					"type":           "status",
   386  					"inventory-name": "foo",
   387  					"status":         "Current",
   388  					"message":        "current",
   389  				},
   390  			},
   391  		},
   392  		"wait for all deleted json": {
   393  			pollUntil: "deleted",
   394  			printer:   "json",
   395  			input:     inventoryTemplate,
   396  			inventory: object.ObjMetadataSet{
   397  				depObject,
   398  				stsObject,
   399  			},
   400  			events: []pollevent.Event{
   401  				{
   402  					Type: pollevent.ResourceUpdateEvent,
   403  					Resource: &pollevent.ResourceStatus{
   404  						Identifier: stsObject,
   405  						Status:     status.NotFoundStatus,
   406  						Message:    "notFound",
   407  					},
   408  				},
   409  				{
   410  					Type: pollevent.ResourceUpdateEvent,
   411  					Resource: &pollevent.ResourceStatus{
   412  						Identifier: depObject,
   413  						Status:     status.NotFoundStatus,
   414  						Message:    "notFound",
   415  					},
   416  				},
   417  			},
   418  			expectedOutput: []map[string]interface{}{
   419  				{
   420  					"group":          "apps",
   421  					"kind":           "StatefulSet",
   422  					"namespace":      "default",
   423  					"name":           "bar",
   424  					"timestamp":      "",
   425  					"type":           "status",
   426  					"inventory-name": "foo",
   427  					"status":         "NotFound",
   428  					"message":        "notFound",
   429  				},
   430  				{
   431  					"group":          "apps",
   432  					"kind":           "Deployment",
   433  					"namespace":      "default",
   434  					"name":           "foo",
   435  					"timestamp":      "",
   436  					"type":           "status",
   437  					"inventory-name": "foo",
   438  					"status":         "NotFound",
   439  					"message":        "notFound",
   440  				},
   441  			},
   442  		},
   443  		"forever with timeout json": {
   444  			pollUntil: "forever",
   445  			printer:   "json",
   446  			timeout:   2 * time.Second,
   447  			input:     inventoryTemplate,
   448  			inventory: object.ObjMetadataSet{
   449  				depObject,
   450  				stsObject,
   451  			},
   452  			events: []pollevent.Event{
   453  				{
   454  					Type: pollevent.ResourceUpdateEvent,
   455  					Resource: &pollevent.ResourceStatus{
   456  						Identifier: stsObject,
   457  						Status:     status.InProgressStatus,
   458  						Message:    "inProgress",
   459  					},
   460  				},
   461  				{
   462  					Type: pollevent.ResourceUpdateEvent,
   463  					Resource: &pollevent.ResourceStatus{
   464  						Identifier: depObject,
   465  						Status:     status.InProgressStatus,
   466  						Message:    "inProgress",
   467  					},
   468  				},
   469  			},
   470  			expectedOutput: []map[string]interface{}{
   471  				{
   472  					"group":          "apps",
   473  					"kind":           "StatefulSet",
   474  					"namespace":      "default",
   475  					"name":           "bar",
   476  					"timestamp":      "",
   477  					"type":           "status",
   478  					"inventory-name": "foo",
   479  					"status":         "InProgress",
   480  					"message":        "inProgress",
   481  				},
   482  				{
   483  					"group":          "apps",
   484  					"kind":           "Deployment",
   485  					"namespace":      "default",
   486  					"name":           "foo",
   487  					"timestamp":      "",
   488  					"type":           "status",
   489  					"inventory-name": "foo",
   490  					"status":         "InProgress",
   491  					"message":        "inProgress",
   492  				},
   493  			},
   494  		},
   495  	}
   496  
   497  	for tn, tc := range testCases {
   498  		t.Run(tn, func(t *testing.T) {
   499  			tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
   500  			defer tf.Cleanup()
   501  
   502  			loader := manifestreader.NewFakeLoader(tf, tc.inventory)
   503  			runner := &Runner{
   504  				factory:    tf,
   505  				invFactory: inventory.FakeClientFactory(tc.inventory),
   506  				loader:     NewInventoryLoader(loader),
   507  				PollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) {
   508  					return &fakePoller{tc.events}, nil
   509  				},
   510  
   511  				pollUntil: tc.pollUntil,
   512  				output:    tc.printer,
   513  				timeout:   tc.timeout,
   514  				invType:   Local,
   515  			}
   516  
   517  			cmd := &cobra.Command{
   518  				PreRunE: runner.preRunE,
   519  				RunE:    runner.runE,
   520  			}
   521  			cmd.SetIn(strings.NewReader(tc.input))
   522  			var buf bytes.Buffer
   523  			cmd.SetOut(&buf)
   524  			cmd.SetArgs([]string{})
   525  
   526  			err := cmd.Execute()
   527  
   528  			if tc.expectedErrMsg != "" {
   529  				if !assert.Error(t, err) {
   530  					t.FailNow()
   531  				}
   532  				assert.Contains(t, err.Error(), tc.expectedErrMsg)
   533  				return
   534  			}
   535  
   536  			assert.NoError(t, err)
   537  			assert.Equal(t, strings.TrimSpace(buf.String()), strings.TrimSpace(tc.expectedOutput))
   538  		})
   539  	}
   540  
   541  	for tn, tc := range jsonTestCases {
   542  		t.Run(tn, func(t *testing.T) {
   543  			tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
   544  			defer tf.Cleanup()
   545  
   546  			loader := manifestreader.NewFakeLoader(tf, tc.inventory)
   547  			runner := &Runner{
   548  				factory:    tf,
   549  				invFactory: inventory.FakeClientFactory(tc.inventory),
   550  				loader:     NewInventoryLoader(loader),
   551  				PollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) {
   552  					return &fakePoller{tc.events}, nil
   553  				},
   554  
   555  				pollUntil: tc.pollUntil,
   556  				output:    tc.printer,
   557  				timeout:   tc.timeout,
   558  				invType:   Local,
   559  			}
   560  
   561  			cmd := &cobra.Command{
   562  				RunE: runner.runE,
   563  			}
   564  			cmd.SetIn(strings.NewReader(tc.input))
   565  			var buf bytes.Buffer
   566  			cmd.SetOut(&buf)
   567  			cmd.SetArgs([]string{})
   568  
   569  			err := cmd.Execute()
   570  			if tc.expectedErrMsg != "" {
   571  				if !assert.Error(t, err) {
   572  					t.FailNow()
   573  				}
   574  				assert.Contains(t, err.Error(), tc.expectedErrMsg)
   575  				return
   576  			}
   577  
   578  			assert.NoError(t, err)
   579  			actual := strings.Split(buf.String(), "\n")
   580  			assertOutput(t, tc.expectedOutput, actual)
   581  		})
   582  	}
   583  }
   584  
   585  // nolint:unparam
   586  func assertOutput(t *testing.T, expectedOutput []map[string]interface{}, actual []string) bool {
   587  	for i, expectedMap := range expectedOutput {
   588  		if len(expectedMap) == 0 {
   589  			return assert.Empty(t, actual[i])
   590  		}
   591  
   592  		var m map[string]interface{}
   593  		err := json.Unmarshal([]byte(actual[i]), &m)
   594  		if !assert.NoError(t, err) {
   595  			return false
   596  		}
   597  
   598  		if _, found := expectedMap["timestamp"]; found {
   599  			if _, ok := m["timestamp"]; ok {
   600  				delete(expectedMap, "timestamp")
   601  				delete(m, "timestamp")
   602  			} else {
   603  				t.Error("expected to find key 'timestamp', but didn't")
   604  				return false
   605  			}
   606  		}
   607  		if !assert.Equal(t, expectedMap, m) {
   608  			return false
   609  		}
   610  	}
   611  	return true
   612  }
   613  

View as plain text