github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/status/resource/deployment_test.go (about)

     1  /*
     2  Copyright 2019 The Skaffold 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 resource
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"testing"
    26  	"time"
    27  
    28  	"google.golang.org/protobuf/testing/protocmp"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/diag/validator"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    34  	"github.com/GoogleContainerTools/skaffold/proto/v1"
    35  	"github.com/GoogleContainerTools/skaffold/testutil"
    36  	testEvent "github.com/GoogleContainerTools/skaffold/testutil/event"
    37  )
    38  
    39  func TestDeploymentCheckStatus(t *testing.T) {
    40  	rolloutCmd := "kubectl --context kubecontext rollout status deployment graph --namespace test --watch=false"
    41  	tests := []struct {
    42  		description     string
    43  		commands        util.Command
    44  		expectedErr     string
    45  		expectedDetails string
    46  		cancelled       bool
    47  		complete        bool
    48  	}{
    49  		{
    50  			description: "rollout status success",
    51  			commands: testutil.CmdRunOut(
    52  				rolloutCmd,
    53  				"deployment \"graph\" successfully rolled out",
    54  			),
    55  			expectedDetails: "successfully rolled out",
    56  			complete:        true,
    57  		},
    58  		{
    59  			description: "resource not complete",
    60  			commands: testutil.CmdRunOut(
    61  				rolloutCmd,
    62  				"Waiting for replicas to be available",
    63  			),
    64  			expectedDetails: "waiting for replicas to be available",
    65  		},
    66  		{
    67  			description: "no output",
    68  			commands: testutil.CmdRunOut(
    69  				rolloutCmd,
    70  				"",
    71  			),
    72  		},
    73  		{
    74  			description: "rollout status error",
    75  			commands: testutil.CmdRunOutErr(
    76  				rolloutCmd,
    77  				"",
    78  				errors.New("error"),
    79  			),
    80  			expectedErr: "error",
    81  			complete:    true,
    82  		},
    83  		{
    84  			description: "rollout kubectl client connection error",
    85  			commands: testutil.CmdRunOutErr(
    86  				rolloutCmd,
    87  				"",
    88  				errors.New("Unable to connect to the server"),
    89  			),
    90  			expectedErr: MsgKubectlConnection,
    91  		},
    92  		{
    93  			description: "set status to cancel",
    94  			commands: testutil.CmdRunOutErr(
    95  				rolloutCmd,
    96  				"",
    97  				errors.New("waiting for replicas to be available"),
    98  			),
    99  			cancelled:   true,
   100  			complete:    true,
   101  			expectedErr: "context cancelled",
   102  		},
   103  	}
   104  
   105  	for _, test := range tests {
   106  		testutil.Run(t, test.description, func(t *testutil.T) {
   107  			t.Override(&util.DefaultExecCommand, test.commands)
   108  			testEvent.InitializeState([]latest.Pipeline{{}})
   109  
   110  			r := NewResource("graph", ResourceTypes.Deployment, "test", 0)
   111  			r.CheckStatus(context.Background(), &statusConfig{})
   112  
   113  			if test.cancelled {
   114  				r.UpdateStatus(&proto.ActionableErr{
   115  					ErrCode: proto.StatusCode_STATUSCHECK_USER_CANCELLED,
   116  					Message: "context cancelled",
   117  				})
   118  			}
   119  			t.CheckDeepEqual(test.complete, r.IsStatusCheckCompleteOrCancelled())
   120  			if test.expectedErr != "" {
   121  				t.CheckErrorContains(test.expectedErr, r.Status().Error())
   122  			} else {
   123  				t.CheckDeepEqual(r.status.ae.Message, test.expectedDetails)
   124  			}
   125  		})
   126  	}
   127  }
   128  
   129  func TestParseKubectlError(t *testing.T) {
   130  	tests := []struct {
   131  		description string
   132  		details     string
   133  		err         error
   134  		expectedAe  *proto.ActionableErr
   135  	}{
   136  		{
   137  			description: "rollout status connection error",
   138  			err:         errors.New("Unable to connect to the server"),
   139  			expectedAe: &proto.ActionableErr{
   140  				ErrCode: proto.StatusCode_STATUSCHECK_KUBECTL_CONNECTION_ERR,
   141  				Message: MsgKubectlConnection,
   142  			},
   143  		},
   144  		{
   145  			description: "rollout status kubectl command killed",
   146  			err:         errors.New("signal: killed"),
   147  			expectedAe: &proto.ActionableErr{
   148  				ErrCode: proto.StatusCode_STATUSCHECK_KUBECTL_PID_KILLED,
   149  				Message: "received Ctrl-C or deployments could not stabilize within 10s: kubectl rollout status command interrupted\n",
   150  			},
   151  		},
   152  		{
   153  			description: "rollout status random error",
   154  			err:         errors.New("deployment test not found"),
   155  			expectedAe: &proto.ActionableErr{
   156  				ErrCode: proto.StatusCode_STATUSCHECK_UNKNOWN,
   157  				Message: "deployment test not found",
   158  			},
   159  		},
   160  		{
   161  			description: "rollout status nil error",
   162  			details:     "successfully rolled out",
   163  			expectedAe: &proto.ActionableErr{
   164  				ErrCode: proto.StatusCode_STATUSCHECK_SUCCESS,
   165  				Message: "successfully rolled out",
   166  			},
   167  		},
   168  	}
   169  	for _, test := range tests {
   170  		testutil.Run(t, test.description, func(t *testutil.T) {
   171  			ae := parseKubectlRolloutError(test.details, 10*time.Second, test.err)
   172  			t.CheckDeepEqual(test.expectedAe, ae, protocmp.Transform())
   173  		})
   174  	}
   175  }
   176  
   177  func TestIsErrAndNotRetriable(t *testing.T) {
   178  	tests := []struct {
   179  		description string
   180  		statusCode  proto.StatusCode
   181  		expected    bool
   182  	}{
   183  		{
   184  			description: "rollout status connection error",
   185  			statusCode:  proto.StatusCode_STATUSCHECK_KUBECTL_CONNECTION_ERR,
   186  		},
   187  		{
   188  			description: "rollout status kubectl command killed",
   189  			statusCode:  proto.StatusCode_STATUSCHECK_KUBECTL_PID_KILLED,
   190  			expected:    true,
   191  		},
   192  		{
   193  			description: "rollout status random error",
   194  			statusCode:  proto.StatusCode_STATUSCHECK_UNKNOWN,
   195  			expected:    true,
   196  		},
   197  		{
   198  			description: "rollout status parent context canceled",
   199  			statusCode:  proto.StatusCode_STATUSCHECK_USER_CANCELLED,
   200  			expected:    true,
   201  		},
   202  		{
   203  			description: "rollout status parent context timed out",
   204  			statusCode:  proto.StatusCode_STATUSCHECK_DEADLINE_EXCEEDED,
   205  			expected:    true,
   206  		},
   207  		{
   208  			description: "rollout status nil error",
   209  			statusCode:  proto.StatusCode_STATUSCHECK_SUCCESS,
   210  			expected:    true,
   211  		},
   212  	}
   213  	for _, test := range tests {
   214  		testutil.Run(t, test.description, func(t *testutil.T) {
   215  			actual := isErrAndNotRetryAble(test.statusCode)
   216  			t.CheckDeepEqual(test.expected, actual)
   217  		})
   218  	}
   219  }
   220  
   221  func TestReportSinceLastUpdated(t *testing.T) {
   222  	tmpDir := filepath.Clean(os.TempDir())
   223  	var tests = []struct {
   224  		description  string
   225  		ae           *proto.ActionableErr
   226  		logs         []string
   227  		expected     string
   228  		expectedMute string
   229  	}{
   230  		{
   231  			description: "logs more than 3 lines",
   232  			ae:          &proto.ActionableErr{Message: "waiting for 0/1 deplotment to rollout\n"},
   233  			logs: []string{
   234  				"[pod container] Waiting for mongodb to start...",
   235  				"[pod container] Waiting for connection for 2 sec",
   236  				"[pod container] Retrying 1st attempt ....",
   237  				"[pod container] Waiting for connection for 2 sec",
   238  				"[pod container] Terminating with exit code 11",
   239  			},
   240  			expectedMute: fmt.Sprintf(` - test-ns:deployment/test: container terminated with exit code 11
   241      - test:pod/foo: container terminated with exit code 11
   242        > [pod container] Retrying 1st attempt ....
   243        > [pod container] Waiting for connection for 2 sec
   244        > [pod container] Terminating with exit code 11
   245        Full logs at %s
   246  `, filepath.Join(tmpDir, "skaffold", "statuscheck", "foo.log")),
   247  			expected: ` - test-ns:deployment/test: container terminated with exit code 11
   248      - test:pod/foo: container terminated with exit code 11
   249        > [pod container] Waiting for mongodb to start...
   250        > [pod container] Waiting for connection for 2 sec
   251        > [pod container] Retrying 1st attempt ....
   252        > [pod container] Waiting for connection for 2 sec
   253        > [pod container] Terminating with exit code 11
   254  `,
   255  		},
   256  		{
   257  			description: "logs less than 3 lines",
   258  			ae:          &proto.ActionableErr{Message: "waiting for 0/1 deplotment to rollout\n"},
   259  			logs: []string{
   260  				"[pod container] Waiting for mongodb to start...",
   261  				"[pod container] Waiting for connection for 2 sec",
   262  				"[pod container] Terminating with exit code 11",
   263  			},
   264  			expected: ` - test-ns:deployment/test: container terminated with exit code 11
   265      - test:pod/foo: container terminated with exit code 11
   266        > [pod container] Waiting for mongodb to start...
   267        > [pod container] Waiting for connection for 2 sec
   268        > [pod container] Terminating with exit code 11
   269  `,
   270  			expectedMute: ` - test-ns:deployment/test: container terminated with exit code 11
   271      - test:pod/foo: container terminated with exit code 11
   272        > [pod container] Waiting for mongodb to start...
   273        > [pod container] Waiting for connection for 2 sec
   274        > [pod container] Terminating with exit code 11
   275  `,
   276  		},
   277  		{
   278  			description: "no logs or empty",
   279  			ae:          &proto.ActionableErr{Message: "waiting for 0/1 deplotment to rollout\n"},
   280  			expected: ` - test-ns:deployment/test: container terminated with exit code 11
   281      - test:pod/foo: container terminated with exit code 11
   282  `,
   283  			expectedMute: ` - test-ns:deployment/test: container terminated with exit code 11
   284      - test:pod/foo: container terminated with exit code 11
   285  `,
   286  		},
   287  	}
   288  	for _, test := range tests {
   289  		testutil.Run(t, test.description, func(t *testutil.T) {
   290  			dep := NewResource("test", ResourceTypes.Deployment, "test-ns", 1)
   291  			dep.resources = map[string]validator.Resource{
   292  				"foo": validator.NewResource(
   293  					"test",
   294  					"pod",
   295  					"foo",
   296  					"Pending",
   297  					&proto.ActionableErr{
   298  						ErrCode: proto.StatusCode_STATUSCHECK_RUN_CONTAINER_ERR,
   299  						Message: "container terminated with exit code 11"},
   300  					test.logs,
   301  				),
   302  			}
   303  			dep.UpdateStatus(test.ae)
   304  			t.CheckDeepEqual(test.expectedMute, dep.ReportSinceLastUpdated(true))
   305  			t.CheckTrue(dep.status.changed)
   306  			// force report to false and Report again with mute logs false
   307  			dep.status.reported = false
   308  			t.CheckDeepEqual(test.expected, dep.ReportSinceLastUpdated(false))
   309  		})
   310  	}
   311  }
   312  
   313  func TestReportSinceLastUpdatedMultipleTimes(t *testing.T) {
   314  	var tests = []struct {
   315  		description     string
   316  		podStatuses     []string
   317  		reportStatusSeq []bool
   318  		expected        string
   319  	}{
   320  		{
   321  			description:     "report first time should return status",
   322  			podStatuses:     []string{"cannot pull image"},
   323  			reportStatusSeq: []bool{true},
   324  			expected: ` - test-ns:deployment/test: cannot pull image
   325      - test:pod/foo: cannot pull image
   326  `,
   327  		},
   328  		{
   329  			description:     "report 2nd time should not return when same status",
   330  			podStatuses:     []string{"cannot pull image", "cannot pull image"},
   331  			reportStatusSeq: []bool{true, true},
   332  			expected:        "",
   333  		},
   334  		{
   335  			description:     "report called after multiple changes but last status was not changed.",
   336  			podStatuses:     []string{"cannot pull image", "changed but not reported", "changed but not reported", "changed but not reported"},
   337  			reportStatusSeq: []bool{true, false, false, true},
   338  			expected: ` - test-ns:deployment/test: changed but not reported
   339      - test:pod/foo: changed but not reported
   340  `,
   341  		},
   342  	}
   343  	for _, test := range tests {
   344  		testutil.Run(t, test.description, func(t *testutil.T) {
   345  			dep := NewResource("test", ResourceTypes.Deployment, "test-ns", 1)
   346  			var actual string
   347  			for i, status := range test.podStatuses {
   348  				dep.UpdateStatus(&proto.ActionableErr{
   349  					ErrCode: proto.StatusCode_STATUSCHECK_DEPLOYMENT_ROLLOUT_PENDING,
   350  					Message: status,
   351  				})
   352  				dep.resources = map[string]validator.Resource{
   353  					"foo": validator.NewResource(
   354  						"test",
   355  						"pod",
   356  						"foo",
   357  						"Pending",
   358  						&proto.ActionableErr{
   359  							ErrCode: proto.StatusCode_STATUSCHECK_DEPLOYMENT_ROLLOUT_PENDING,
   360  							Message: status,
   361  						},
   362  						[]string{},
   363  					),
   364  				}
   365  				if test.reportStatusSeq[i] {
   366  					actual = dep.ReportSinceLastUpdated(false)
   367  				}
   368  			}
   369  			t.CheckDeepEqual(test.expected, actual)
   370  		})
   371  	}
   372  }
   373  
   374  func TestStatusCode(t *testing.T) {
   375  	var tests = []struct {
   376  		description      string
   377  		resourceStatuses []proto.StatusCode
   378  		status           proto.StatusCode
   379  		expected         proto.StatusCode
   380  	}{
   381  		{
   382  			description: "user cancelled status returns correctly",
   383  			status:      proto.StatusCode_STATUSCHECK_USER_CANCELLED,
   384  			resourceStatuses: []proto.StatusCode{
   385  				proto.StatusCode_STATUSCHECK_UNHEALTHY,
   386  				proto.StatusCode_STATUSCHECK_SUCCESS,
   387  			},
   388  			expected: proto.StatusCode_STATUSCHECK_USER_CANCELLED,
   389  		},
   390  		{
   391  			description: "successful returns correctly",
   392  			status:      proto.StatusCode_STATUSCHECK_SUCCESS,
   393  			resourceStatuses: []proto.StatusCode{
   394  				proto.StatusCode_STATUSCHECK_CONTAINER_RESTARTING,
   395  				proto.StatusCode_STATUSCHECK_SUCCESS,
   396  			},
   397  			expected: proto.StatusCode_STATUSCHECK_SUCCESS,
   398  		},
   399  		{
   400  			description: "other dep status returns the pod status",
   401  			status:      proto.StatusCode_STATUSCHECK_DEPLOYMENT_ROLLOUT_PENDING,
   402  			resourceStatuses: []proto.StatusCode{
   403  				proto.StatusCode_STATUSCHECK_CONTAINER_RESTARTING,
   404  				proto.StatusCode_STATUSCHECK_SUCCESS,
   405  			},
   406  			expected: proto.StatusCode_STATUSCHECK_CONTAINER_RESTARTING,
   407  		},
   408  	}
   409  	for _, test := range tests {
   410  		testutil.Run(t, test.description, func(t *testutil.T) {
   411  			dep := NewResource("test", ResourceTypes.Deployment, "test-ns", 1)
   412  			dep.UpdateStatus(&proto.ActionableErr{
   413  				ErrCode: test.status,
   414  				Message: "test status code",
   415  			})
   416  			dep.resources = map[string]validator.Resource{}
   417  			for i, sc := range test.resourceStatuses {
   418  				dep.resources[fmt.Sprintf("foo-%d", i)] = validator.NewResource(
   419  					"test",
   420  					"pod",
   421  					"foo",
   422  					"Pending",
   423  					&proto.ActionableErr{
   424  						ErrCode: sc,
   425  						Message: "test status",
   426  					},
   427  					[]string{},
   428  				)
   429  			}
   430  			t.CheckDeepEqual(test.expected, dep.StatusCode())
   431  		})
   432  	}
   433  }
   434  
   435  type statusConfig struct {
   436  	runcontext.RunContext // Embedded to provide the default values.
   437  }
   438  
   439  func (c *statusConfig) GetKubeContext() string { return "kubecontext" }