github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/agent/exec/controller_test.go (about) 1 package exec 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "runtime" 8 "testing" 9 10 "github.com/docker/swarmkit/api" 11 "github.com/docker/swarmkit/log" 12 gogotypes "github.com/gogo/protobuf/types" 13 "github.com/stretchr/testify/assert" 14 ) 15 16 func TestResolve(t *testing.T) { 17 var ( 18 ctx = context.Background() 19 executor = &mockExecutor{} 20 task = newTestTask(t, api.TaskStateAssigned, api.TaskStateRunning) 21 ) 22 23 _, status, err := Resolve(ctx, task, executor) 24 assert.NoError(t, err) 25 assert.Equal(t, api.TaskStateAccepted, status.State) 26 assert.Equal(t, "accepted", status.Message) 27 28 task.Status = *status 29 // now, we get no status update. 30 _, status, err = Resolve(ctx, task, executor) 31 assert.NoError(t, err) 32 assert.Equal(t, task.Status, *status) 33 34 // now test an error causing rejection 35 executor.err = errors.New("some error") 36 task = newTestTask(t, api.TaskStateAssigned, api.TaskStateRunning) 37 _, status, err = Resolve(ctx, task, executor) 38 assert.Equal(t, executor.err, err) 39 assert.Equal(t, api.TaskStateRejected, status.State) 40 41 // on Resolve failure, tasks already started should be considered failed 42 task = newTestTask(t, api.TaskStateStarting, api.TaskStateRunning) 43 _, status, err = Resolve(ctx, task, executor) 44 assert.Equal(t, executor.err, err) 45 assert.Equal(t, api.TaskStateFailed, status.State) 46 47 // on Resolve failure, tasks already in terminated state don't need update 48 task = newTestTask(t, api.TaskStateCompleted, api.TaskStateRunning) 49 _, status, err = Resolve(ctx, task, executor) 50 assert.Equal(t, executor.err, err) 51 assert.Equal(t, api.TaskStateCompleted, status.State) 52 53 // task is now foobared, from a reporting perspective but we can now 54 // resolve the controller for some reason. Ensure the task state isn't 55 // touched. 56 task.Status = *status 57 executor.err = nil 58 _, status, err = Resolve(ctx, task, executor) 59 assert.NoError(t, err) 60 assert.Equal(t, task.Status, *status) 61 } 62 63 func TestAcceptPrepare(t *testing.T) { 64 var ( 65 task = newTestTask(t, api.TaskStateAssigned, api.TaskStateRunning) 66 ctx, ctlr, finish = buildTestEnv(t, task) 67 ) 68 defer func() { 69 finish() 70 assert.Equal(t, 1, ctlr.calls["Prepare"]) 71 }() 72 73 ctlr.PrepareFn = func(_ context.Context) error { 74 return nil 75 } 76 77 // Report acceptance. 78 status := checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 79 State: api.TaskStateAccepted, 80 Message: "accepted", 81 }) 82 83 // Actually prepare the task. 84 task.Status = *status 85 86 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 87 State: api.TaskStatePreparing, 88 Message: "preparing", 89 }) 90 91 task.Status = *status 92 93 checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 94 State: api.TaskStateReady, 95 Message: "prepared", 96 }) 97 } 98 99 func TestPrepareAlready(t *testing.T) { 100 var ( 101 task = newTestTask(t, api.TaskStateAssigned, api.TaskStateRunning) 102 ctx, ctlr, finish = buildTestEnv(t, task) 103 ) 104 defer func() { 105 finish() 106 assert.Equal(t, 1, ctlr.calls["Prepare"]) 107 }() 108 ctlr.PrepareFn = func(_ context.Context) error { 109 return ErrTaskPrepared 110 } 111 112 // Report acceptance. 113 status := checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 114 State: api.TaskStateAccepted, 115 Message: "accepted", 116 }) 117 118 // Actually prepare the task. 119 task.Status = *status 120 121 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 122 State: api.TaskStatePreparing, 123 Message: "preparing", 124 }) 125 126 task.Status = *status 127 128 checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 129 State: api.TaskStateReady, 130 Message: "prepared", 131 }) 132 } 133 134 func TestPrepareFailure(t *testing.T) { 135 var ( 136 task = newTestTask(t, api.TaskStateAssigned, api.TaskStateRunning) 137 ctx, ctlr, finish = buildTestEnv(t, task) 138 ) 139 defer func() { 140 finish() 141 assert.Equal(t, ctlr.calls["Prepare"], 1) 142 }() 143 ctlr.PrepareFn = func(_ context.Context) error { 144 return errors.New("test error") 145 } 146 147 // Report acceptance. 148 status := checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 149 State: api.TaskStateAccepted, 150 Message: "accepted", 151 }) 152 153 // Actually prepare the task. 154 task.Status = *status 155 156 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 157 State: api.TaskStatePreparing, 158 Message: "preparing", 159 }) 160 161 task.Status = *status 162 163 checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 164 State: api.TaskStateRejected, 165 Message: "preparing", 166 Err: "test error", 167 }) 168 } 169 170 func TestReadyRunning(t *testing.T) { 171 var ( 172 task = newTestTask(t, api.TaskStateReady, api.TaskStateRunning) 173 ctx, ctlr, finish = buildTestEnv(t, task) 174 ) 175 defer func() { 176 finish() 177 assert.Equal(t, 1, ctlr.calls["Start"]) 178 assert.Equal(t, 2, ctlr.calls["Wait"]) 179 }() 180 181 ctlr.StartFn = func(ctx context.Context) error { 182 return nil 183 } 184 ctlr.WaitFn = func(ctx context.Context) error { 185 if ctlr.calls["Wait"] == 1 { 186 return context.Canceled 187 } else if ctlr.calls["Wait"] == 2 { 188 return nil 189 } else { 190 panic("unexpected call!") 191 } 192 } 193 194 // Report starting 195 status := checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 196 State: api.TaskStateStarting, 197 Message: "starting", 198 }) 199 200 task.Status = *status 201 202 // start the container 203 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 204 State: api.TaskStateRunning, 205 Message: "started", 206 }) 207 208 task.Status = *status 209 210 // resume waiting 211 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 212 State: api.TaskStateRunning, 213 Message: "started", 214 }, ErrTaskRetry) 215 216 task.Status = *status 217 // wait and cancel 218 dctlr := &StatuserController{ 219 StubController: ctlr, 220 cstatus: &api.ContainerStatus{ 221 ExitCode: 0, 222 }, 223 } 224 checkDo(ctx, t, task, dctlr, &api.TaskStatus{ 225 State: api.TaskStateCompleted, 226 Message: "finished", 227 RuntimeStatus: &api.TaskStatus_Container{ 228 Container: &api.ContainerStatus{ 229 ExitCode: 0, 230 }, 231 }, 232 }) 233 } 234 235 func TestReadyRunningExitFailure(t *testing.T) { 236 var ( 237 task = newTestTask(t, api.TaskStateReady, api.TaskStateRunning) 238 ctx, ctlr, finish = buildTestEnv(t, task) 239 ) 240 defer func() { 241 finish() 242 assert.Equal(t, 1, ctlr.calls["Start"]) 243 assert.Equal(t, 1, ctlr.calls["Wait"]) 244 }() 245 246 ctlr.StartFn = func(ctx context.Context) error { 247 return nil 248 } 249 ctlr.WaitFn = func(ctx context.Context) error { 250 return newExitError(1) 251 } 252 253 // Report starting 254 status := checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 255 State: api.TaskStateStarting, 256 Message: "starting", 257 }) 258 259 task.Status = *status 260 261 // start the container 262 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 263 State: api.TaskStateRunning, 264 Message: "started", 265 }) 266 267 task.Status = *status 268 dctlr := &StatuserController{ 269 StubController: ctlr, 270 cstatus: &api.ContainerStatus{ 271 ExitCode: 1, 272 }, 273 } 274 checkDo(ctx, t, task, dctlr, &api.TaskStatus{ 275 State: api.TaskStateFailed, 276 RuntimeStatus: &api.TaskStatus_Container{ 277 Container: &api.ContainerStatus{ 278 ExitCode: 1, 279 }, 280 }, 281 Message: "started", 282 Err: "test error, exit code=1", 283 }) 284 } 285 286 func TestAlreadyStarted(t *testing.T) { 287 var ( 288 task = newTestTask(t, api.TaskStateReady, api.TaskStateRunning) 289 ctx, ctlr, finish = buildTestEnv(t, task) 290 ) 291 defer func() { 292 finish() 293 assert.Equal(t, 1, ctlr.calls["Start"]) 294 assert.Equal(t, 2, ctlr.calls["Wait"]) 295 }() 296 297 ctlr.StartFn = func(ctx context.Context) error { 298 return ErrTaskStarted 299 } 300 ctlr.WaitFn = func(ctx context.Context) error { 301 if ctlr.calls["Wait"] == 1 { 302 return context.Canceled 303 } else if ctlr.calls["Wait"] == 2 { 304 return newExitError(1) 305 } else { 306 panic("unexpected call!") 307 } 308 } 309 310 // Before we can move to running, we have to move to startin. 311 status := checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 312 State: api.TaskStateStarting, 313 Message: "starting", 314 }) 315 316 task.Status = *status 317 318 // start the container 319 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 320 State: api.TaskStateRunning, 321 Message: "started", 322 }) 323 324 task.Status = *status 325 326 status = checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 327 State: api.TaskStateRunning, 328 Message: "started", 329 }, ErrTaskRetry) 330 331 task.Status = *status 332 333 // now take the real exit to test wait cancelling. 334 dctlr := &StatuserController{ 335 StubController: ctlr, 336 cstatus: &api.ContainerStatus{ 337 ExitCode: 1, 338 }, 339 } 340 checkDo(ctx, t, task, dctlr, &api.TaskStatus{ 341 State: api.TaskStateFailed, 342 RuntimeStatus: &api.TaskStatus_Container{ 343 Container: &api.ContainerStatus{ 344 ExitCode: 1, 345 }, 346 }, 347 Message: "started", 348 Err: "test error, exit code=1", 349 }) 350 351 } 352 func TestShutdown(t *testing.T) { 353 var ( 354 task = newTestTask(t, api.TaskStateNew, api.TaskStateShutdown) 355 ctx, ctlr, finish = buildTestEnv(t, task) 356 ) 357 defer func() { 358 finish() 359 assert.Equal(t, 1, ctlr.calls["Shutdown"]) 360 }() 361 ctlr.ShutdownFn = func(_ context.Context) error { 362 return nil 363 } 364 365 checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 366 State: api.TaskStateShutdown, 367 Message: "shutdown", 368 }) 369 } 370 371 // TestDesiredStateRemove checks that the agent maintains SHUTDOWN as the 372 // maximum state in the agent. This is particularly relevant for the case 373 // where a service scale down or deletion sets the desired state of tasks 374 // that are supposed to be removed to REMOVE. 375 func TestDesiredStateRemove(t *testing.T) { 376 var ( 377 task = newTestTask(t, api.TaskStateNew, api.TaskStateRemove) 378 ctx, ctlr, finish = buildTestEnv(t, task) 379 ) 380 defer func() { 381 finish() 382 assert.Equal(t, 1, ctlr.calls["Shutdown"]) 383 }() 384 ctlr.ShutdownFn = func(_ context.Context) error { 385 return nil 386 } 387 388 checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 389 State: api.TaskStateShutdown, 390 Message: "shutdown", 391 }) 392 } 393 394 // TestDesiredStateRemoveOnlyNonterminal checks that the agent will only stop 395 // a container on REMOVE if it's not already in a terminal state. If the 396 // container is already in a terminal state, (like COMPLETE) the agent should 397 // take no action 398 func TestDesiredStateRemoveOnlyNonterminal(t *testing.T) { 399 // go through all terminal states, just for completeness' sake 400 for _, state := range []api.TaskState{ 401 api.TaskStateCompleted, 402 api.TaskStateShutdown, 403 api.TaskStateFailed, 404 api.TaskStateRejected, 405 api.TaskStateRemove, 406 // no TaskStateOrphaned because that's not a state the task can be in 407 // on the agent 408 } { 409 // capture state variable here to run in parallel 410 state := state 411 t.Run(state.String(), func(t *testing.T) { 412 // go parallel to go faster 413 t.Parallel() 414 var ( 415 // create a new task, actual state `state`, desired state 416 // shutdown 417 task = newTestTask(t, state, api.TaskStateShutdown) 418 ctx, ctlr, finish = buildTestEnv(t, task) 419 ) 420 // make the shutdown function a noop 421 ctlr.ShutdownFn = func(_ context.Context) error { 422 return nil 423 } 424 425 // Note we check for error ErrTaskNoop, which will be raised 426 // because nothing happens 427 checkDo(ctx, t, task, ctlr, &api.TaskStatus{ 428 State: state, 429 }, ErrTaskNoop) 430 defer func() { 431 finish() 432 // we should never have called shutdown 433 assert.Equal(t, 0, ctlr.calls["Shutdown"]) 434 }() 435 }) 436 } 437 } 438 439 // StatuserController is used to create a new Controller, which is also a ContainerStatuser. 440 // We cannot add ContainerStatus() to the Controller, due to the check in controller.go:242 441 type StatuserController struct { 442 *StubController 443 cstatus *api.ContainerStatus 444 } 445 446 func (mc *StatuserController) ContainerStatus(ctx context.Context) (*api.ContainerStatus, error) { 447 return mc.cstatus, nil 448 } 449 450 type exitCoder struct { 451 code int 452 } 453 454 func newExitError(code int) error { return &exitCoder{code} } 455 456 func (ec *exitCoder) Error() string { return fmt.Sprintf("test error, exit code=%v", ec.code) } 457 func (ec *exitCoder) ExitCode() int { return ec.code } 458 459 func checkDo(ctx context.Context, t *testing.T, task *api.Task, ctlr Controller, expected *api.TaskStatus, expectedErr ...error) *api.TaskStatus { 460 status, err := Do(ctx, task, ctlr) 461 if len(expectedErr) > 0 { 462 assert.Equal(t, expectedErr[0], err) 463 } else { 464 assert.NoError(t, err) 465 } 466 467 // if the status and task.Status are different, make sure new timestamp is greater 468 if task.Status.Timestamp != nil { 469 // crazy timestamp validation follows 470 previous, err := gogotypes.TimestampFromProto(task.Status.Timestamp) 471 assert.Nil(t, err) 472 473 current, err := gogotypes.TimestampFromProto(status.Timestamp) 474 assert.Nil(t, err) 475 476 if current.Before(previous) { 477 // ensure that the timestamp always proceeds forward 478 t.Fatalf("timestamp must proceed forward: %v < %v", current, previous) 479 } 480 } 481 482 copy := status.Copy() 483 copy.Timestamp = nil // don't check against timestamp 484 assert.Equal(t, expected, copy) 485 486 return status 487 } 488 489 func newTestTask(t *testing.T, state, desired api.TaskState) *api.Task { 490 return &api.Task{ 491 ID: "test-task", 492 Status: api.TaskStatus{ 493 State: state, 494 }, 495 DesiredState: desired, 496 } 497 } 498 499 func buildTestEnv(t *testing.T, task *api.Task) (context.Context, *StubController, func()) { 500 var ( 501 ctx, cancel = context.WithCancel(context.Background()) 502 ctlr = NewStubController() 503 ) 504 505 // Put test name into log messages. Awesome! 506 pc, _, _, ok := runtime.Caller(1) 507 if ok { 508 fn := runtime.FuncForPC(pc) 509 ctx = log.WithLogger(ctx, log.L.WithField("test", fn.Name())) 510 } 511 512 return ctx, ctlr, cancel 513 } 514 515 type mockExecutor struct { 516 Executor 517 518 err error 519 } 520 521 func (m *mockExecutor) Controller(t *api.Task) (Controller, error) { 522 return nil, m.err 523 }