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" }