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  }