github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/undertaker/undertaker_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package undertaker_test
     5  
     6  import (
     7  	"context"
     8  	"time"
     9  
    10  	"github.com/juju/clock/testclock"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	"github.com/juju/worker/v3"
    16  	"github.com/juju/worker/v3/workertest"
    17  	"go.uber.org/mock/gomock"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/core/life"
    21  	"github.com/juju/juju/core/status"
    22  	watcher "github.com/juju/juju/core/watcher"
    23  	"github.com/juju/juju/core/watcher/watchertest"
    24  	"github.com/juju/juju/environs"
    25  	cloudspec "github.com/juju/juju/environs/cloudspec"
    26  	environscontext "github.com/juju/juju/environs/context"
    27  	"github.com/juju/juju/rpc/params"
    28  	"github.com/juju/juju/worker/undertaker"
    29  )
    30  
    31  // OldUndertakerSuite is *not* complete. But it's a lot more so
    32  // than it was before, and should be much easier to extend.
    33  type OldUndertakerSuite struct {
    34  	testing.IsolationSuite
    35  	fix fixture
    36  }
    37  
    38  var _ = gc.Suite(&OldUndertakerSuite{})
    39  
    40  func (s *OldUndertakerSuite) SetUpTest(c *gc.C) {
    41  	s.IsolationSuite.SetUpTest(c)
    42  	minute := time.Minute
    43  	s.fix = fixture{
    44  		clock: testclock.NewDilatedWallClock(10 * time.Millisecond),
    45  		info: params.UndertakerModelInfoResult{
    46  			Result: params.UndertakerModelInfo{
    47  				Life:           "dying",
    48  				DestroyTimeout: &minute,
    49  			},
    50  		},
    51  	}
    52  }
    53  
    54  func (s *OldUndertakerSuite) TestAliveError(c *gc.C) {
    55  	s.fix.info.Result.Life = "alive"
    56  	s.fix.dirty = true
    57  	stub := s.fix.run(c, func(w worker.Worker) {
    58  		err := workertest.CheckKilled(c, w)
    59  		c.Check(err, gc.ErrorMatches, "model still alive")
    60  	})
    61  	stub.CheckCallNames(c, "WatchModel", "ModelInfo")
    62  }
    63  
    64  func (s *OldUndertakerSuite) TestAlreadyDeadRemoves(c *gc.C) {
    65  	s.fix.info.Result.Life = "dead"
    66  	stub := s.fix.run(c, func(w worker.Worker) {
    67  		workertest.CheckKilled(c, w)
    68  	})
    69  	stub.CheckCallNames(c, "WatchModel", "ModelInfo", "SetStatus", "ModelConfig", "CloudSpec", "Destroy", "RemoveModel")
    70  }
    71  
    72  func (s *OldUndertakerSuite) TestDyingDeadRemoved(c *gc.C) {
    73  	stub := s.fix.run(c, func(w worker.Worker) {
    74  		workertest.CheckKilled(c, w)
    75  	})
    76  	stub.CheckCallNames(c,
    77  		"WatchModel",
    78  		"ModelInfo",
    79  		"SetStatus",
    80  		"WatchModelResources",
    81  		"ProcessDyingModel",
    82  		"SetStatus",
    83  		"ModelConfig",
    84  		"CloudSpec",
    85  		"Destroy",
    86  		"RemoveModel",
    87  	)
    88  }
    89  
    90  func (s *OldUndertakerSuite) TestSetStatusDestroying(c *gc.C) {
    91  	stub := s.fix.run(c, func(w worker.Worker) {
    92  		workertest.CheckKilled(c, w)
    93  	})
    94  	stub.CheckCallNames(c,
    95  		"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel",
    96  		"SetStatus", "ModelConfig", "CloudSpec", "Destroy", "RemoveModel")
    97  	stub.CheckCall(
    98  		c, 2, "SetStatus", status.Destroying,
    99  		"cleaning up cloud resources", map[string]interface{}(nil),
   100  	)
   101  	stub.CheckCall(
   102  		c, 5, "SetStatus", status.Destroying,
   103  		"tearing down cloud environment", map[string]interface{}(nil),
   104  	)
   105  }
   106  
   107  func (s *OldUndertakerSuite) TestControllerStopsWhenModelDead(c *gc.C) {
   108  	s.fix.info.Result.IsSystem = true
   109  	stub := s.fix.run(c, func(w worker.Worker) {
   110  		workertest.CheckKilled(c, w)
   111  	})
   112  	stub.CheckCallNames(c,
   113  		"WatchModel",
   114  		"ModelInfo",
   115  		"SetStatus",
   116  		"WatchModelResources",
   117  		"ProcessDyingModel",
   118  	)
   119  }
   120  
   121  func (s *OldUndertakerSuite) TestModelInfoErrorFatal(c *gc.C) {
   122  	s.fix.errors = []error{nil, errors.New("pow")}
   123  	s.fix.dirty = true
   124  	stub := s.fix.run(c, func(w worker.Worker) {
   125  		err := workertest.CheckKilled(c, w)
   126  		c.Check(err, gc.ErrorMatches, "pow")
   127  	})
   128  	stub.CheckCallNames(c, "WatchModel", "ModelInfo")
   129  }
   130  
   131  func (s *OldUndertakerSuite) TestWatchModelResourcesErrorFatal(c *gc.C) {
   132  	s.fix.errors = []error{nil, nil, nil, errors.New("pow")}
   133  	s.fix.dirty = true
   134  	stub := s.fix.run(c, func(w worker.Worker) {
   135  		err := workertest.CheckKilled(c, w)
   136  		c.Check(err, gc.ErrorMatches, "proccesing model death: pow")
   137  	})
   138  	stub.CheckCallNames(c, "WatchModel", "ModelInfo", "SetStatus", "WatchModelResources")
   139  }
   140  
   141  func (s *OldUndertakerSuite) TestProcessDyingModelErrorRetried(c *gc.C) {
   142  	s.fix.errors = []error{
   143  		nil, // WatchModel
   144  		nil, // ModelInfo
   145  		nil, // SetStatus
   146  		nil, // WatchModelResources,
   147  		&params.Error{Code: params.CodeHasHostedModels},
   148  		nil, // SetStatus
   149  		&params.Error{Code: params.CodeModelNotEmpty},
   150  		nil, // SetStatus
   151  		nil, // ProcessDyingModel,
   152  		nil, // SetStatus
   153  		nil, // ModelConfig
   154  		nil, // CloudSpec
   155  		nil, // Destroy,
   156  		nil, // RemoveModel
   157  	}
   158  	stub := s.fix.run(c, func(w worker.Worker) {
   159  		workertest.CheckKilled(c, w)
   160  	})
   161  	stub.CheckCallNames(c,
   162  		"WatchModel",
   163  		"ModelInfo",
   164  		"SetStatus",
   165  		"WatchModelResources",
   166  		"ProcessDyingModel",
   167  		"SetStatus",
   168  		"ProcessDyingModel",
   169  		"SetStatus",
   170  		"ProcessDyingModel",
   171  		"SetStatus",
   172  		"ModelConfig",
   173  		"CloudSpec",
   174  		"Destroy",
   175  		"RemoveModel",
   176  	)
   177  }
   178  
   179  func (s *OldUndertakerSuite) TestProcessDyingModelErrorFatal(c *gc.C) {
   180  	s.fix.errors = []error{
   181  		nil, // WatchModel
   182  		nil, // ModelInfo
   183  		nil, // SetStatus
   184  		nil, // WatchModelResources,
   185  		errors.New("nope"),
   186  	}
   187  	s.fix.dirty = true
   188  	stub := s.fix.run(c, func(w worker.Worker) {
   189  		err := workertest.CheckKilled(c, w)
   190  		c.Check(err, gc.ErrorMatches, "proccesing model death: nope")
   191  	})
   192  	stub.CheckCallNames(c,
   193  		"WatchModel",
   194  		"ModelInfo",
   195  		"SetStatus",
   196  		"WatchModelResources",
   197  		"ProcessDyingModel",
   198  	)
   199  }
   200  
   201  func (s *OldUndertakerSuite) TestDestroyErrorFatal(c *gc.C) {
   202  	s.fix.errors = []error{nil, nil, nil, nil, nil, errors.New("pow")}
   203  	s.fix.info.Result.Life = "dead"
   204  	s.fix.dirty = true
   205  	stub := s.fix.run(c, func(w worker.Worker) {
   206  		err := workertest.CheckKilled(c, w)
   207  		c.Check(err, gc.ErrorMatches, "cannot destroy cloud resources: process destroy environ: pow")
   208  	})
   209  	stub.CheckCallNames(c, "WatchModel", "ModelInfo", "SetStatus", "ModelConfig", "CloudSpec", "Destroy")
   210  }
   211  
   212  func (s *OldUndertakerSuite) TestDestroyErrorForced(c *gc.C) {
   213  	s.fix.errors = []error{nil, nil, nil, nil, nil, errors.New("pow")}
   214  	s.fix.info.Result.Life = "dead"
   215  	s.fix.info.Result.ForceDestroyed = true
   216  	destroyTimeout := 500 * time.Millisecond
   217  	s.fix.info.Result.DestroyTimeout = &destroyTimeout
   218  	stub := s.fix.run(c, func(w worker.Worker) {
   219  		err := workertest.CheckKilled(c, w)
   220  		c.Assert(err, jc.ErrorIsNil)
   221  	})
   222  	// Removal continues despite the error calling destroy.
   223  	mainCalls, destroyCloudCalls := s.sortCalls(c, stub)
   224  	c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "RemoveModel"})
   225  	c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"})
   226  	// Logged the failed destroy call.
   227  	s.fix.logger.stub.CheckCallNames(c, "Errorf")
   228  }
   229  
   230  func (s *OldUndertakerSuite) TestRemoveModelErrorFatal(c *gc.C) {
   231  	s.fix.errors = []error{nil, nil, nil, nil, nil, nil, errors.New("pow")}
   232  	s.fix.info.Result.Life = "dead"
   233  	s.fix.dirty = true
   234  	stub := s.fix.run(c, func(w worker.Worker) {
   235  		err := workertest.CheckKilled(c, w)
   236  		c.Check(err, gc.ErrorMatches, "cannot remove model: pow")
   237  	})
   238  	mainCalls, destroyCloudCalls := s.sortCalls(c, stub)
   239  	c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "RemoveModel"})
   240  	c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"})
   241  }
   242  
   243  func (s *OldUndertakerSuite) TestDestroyTimeout(c *gc.C) {
   244  	notEmptyErr := &params.Error{Code: params.CodeModelNotEmpty}
   245  	s.fix.errors = []error{nil, nil, nil, nil, notEmptyErr, notEmptyErr, notEmptyErr, notEmptyErr, errors.Timeoutf("error")}
   246  	s.fix.dirty = true
   247  	s.fix.advance = 2 * time.Minute
   248  	stub := s.fix.run(c, func(w worker.Worker) {
   249  		err := workertest.CheckKilled(c, w)
   250  		c.Assert(err, gc.ErrorMatches, ".* timeout")
   251  	})
   252  	// Depending on timing there can be 1 or more ProcessDyingModel calls.
   253  	calls := stub.Calls()
   254  	var callNames []string
   255  	for i, call := range calls {
   256  		if call.FuncName == "ProcessDyingModel" {
   257  			continue
   258  		}
   259  		if i > 4 && call.FuncName == "SetStatus" {
   260  			continue
   261  		}
   262  		callNames = append(callNames, call.FuncName)
   263  	}
   264  	c.Assert(callNames, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources"})
   265  }
   266  
   267  func (s *OldUndertakerSuite) TestDestroyTimeoutForce(c *gc.C) {
   268  	s.fix.info.Result.ForceDestroyed = true
   269  	s.fix.advance = 2 * time.Minute
   270  	stub := s.fix.run(c, func(w worker.Worker) {
   271  		err := workertest.CheckKilled(c, w)
   272  		c.Assert(err, jc.ErrorIsNil)
   273  	})
   274  	mainCalls, destroyCloudCalls := s.sortCalls(c, stub)
   275  	c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", "SetStatus", "RemoveModel"})
   276  	c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"})
   277  	s.fix.logger.stub.CheckNoCalls(c)
   278  }
   279  
   280  func (s *OldUndertakerSuite) TestEnvironDestroyTimeout(c *gc.C) {
   281  	timeout := time.Millisecond
   282  	s.fix.info.Result.DestroyTimeout = &timeout
   283  	s.fix.dirty = true
   284  	stub := s.fix.run(c, func(w worker.Worker) {
   285  		err := workertest.CheckKilled(c, w)
   286  		c.Assert(err, jc.ErrorIsNil)
   287  	})
   288  	mainCalls, destroyCloudCalls := s.sortCalls(c, stub)
   289  	c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", "SetStatus", "RemoveModel"})
   290  	c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"})
   291  	s.fix.logger.stub.CheckCall(c, 0, "Warningf", "timeout ignored for graceful model destroy", []interface{}(nil))
   292  }
   293  
   294  func (s *OldUndertakerSuite) TestEnvironDestroyTimeoutForce(c *gc.C) {
   295  	timeout := time.Second
   296  	s.fix.info.Result.DestroyTimeout = &timeout
   297  	s.fix.info.Result.ForceDestroyed = true
   298  	s.fix.dirty = true
   299  	stub := s.fix.run(c, func(w worker.Worker) {
   300  		err := workertest.CheckKilled(c, w)
   301  		c.Assert(err, jc.ErrorIsNil)
   302  	})
   303  	mainCalls, destroyCloudCalls := s.sortCalls(c, stub)
   304  	c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", "SetStatus", "RemoveModel"})
   305  	c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"})
   306  }
   307  
   308  func (s *OldUndertakerSuite) sortCalls(c *gc.C, stub *testing.Stub) (mainCalls []string, destroyCloudCalls []string) {
   309  	calls := stub.Calls()
   310  	for _, call := range calls {
   311  		switch call.FuncName {
   312  		case "ModelConfig", "CloudSpec", "Destroy":
   313  			destroyCloudCalls = append(destroyCloudCalls, call.FuncName)
   314  		default:
   315  			mainCalls = append(mainCalls, call.FuncName)
   316  		}
   317  	}
   318  	return
   319  }
   320  
   321  func (s *OldUndertakerSuite) TestEnvironDestroyForceTimeoutZero(c *gc.C) {
   322  	zero := time.Second * 0
   323  	s.fix.info.Result.DestroyTimeout = &zero
   324  	s.fix.info.Result.ForceDestroyed = true
   325  	s.fix.dirty = true
   326  	stub := s.fix.run(c, func(w worker.Worker) {
   327  		err := workertest.CheckKilled(c, w)
   328  		c.Assert(err, jc.ErrorIsNil)
   329  	})
   330  	stub.CheckCallNames(c, "WatchModel", "ModelInfo", "RemoveModel")
   331  	s.fix.logger.stub.CheckNoCalls(c)
   332  }
   333  
   334  type UndertakerSuite struct{}
   335  
   336  var _ = gc.Suite(&UndertakerSuite{})
   337  
   338  func (s *UndertakerSuite) TestExitOnModelChanged(c *gc.C) {
   339  	ctrl := gomock.NewController(c)
   340  	defer ctrl.Finish()
   341  
   342  	facade := NewMockFacade(ctrl)
   343  	facade.EXPECT().SetStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
   344  
   345  	modelChanged := make(chan struct{}, 1)
   346  	modelChanged <- struct{}{}
   347  	modelResources := make(chan struct{}, 1)
   348  	modelResources <- struct{}{}
   349  	facade.EXPECT().WatchModel().DoAndReturn(func() (watcher.NotifyWatcher, error) {
   350  		return watchertest.NewMockNotifyWatcher(modelChanged), nil
   351  	})
   352  	facade.EXPECT().WatchModelResources().DoAndReturn(func() (watcher.NotifyWatcher, error) {
   353  		return watchertest.NewMockNotifyWatcher(modelResources), nil
   354  	})
   355  	facade.EXPECT().ModelConfig().Return(nil, nil)
   356  
   357  	gomock.InOrder(
   358  		facade.EXPECT().ModelInfo().Return(params.UndertakerModelInfoResult{
   359  			Result: params.UndertakerModelInfo{
   360  				Life:           life.Dying,
   361  				ForceDestroyed: false,
   362  			},
   363  		}, nil),
   364  		facade.EXPECT().ProcessDyingModel().Return(nil),
   365  		facade.EXPECT().CloudSpec().DoAndReturn(func() (cloudspec.CloudSpec, error) {
   366  			modelChanged <- struct{}{}
   367  			return cloudspec.CloudSpec{}, nil
   368  		}),
   369  		facade.EXPECT().ModelInfo().Return(params.UndertakerModelInfoResult{
   370  			Result: params.UndertakerModelInfo{
   371  				Life:           life.Dying,
   372  				ForceDestroyed: true, // changed from false to true to cause worker to exit.
   373  			},
   374  		}, nil),
   375  	)
   376  
   377  	credentialAPI := NewMockCredentialAPI(ctrl)
   378  
   379  	w, err := undertaker.NewUndertaker(undertaker.Config{
   380  		Facade:        facade,
   381  		CredentialAPI: credentialAPI,
   382  		Logger:        loggo.GetLogger("test"),
   383  		Clock:         testclock.NewDilatedWallClock(testing.ShortWait),
   384  		NewCloudDestroyerFunc: func(ctx context.Context, op environs.OpenParams) (environs.CloudDestroyer, error) {
   385  			return &waitDestroyer{}, nil
   386  		},
   387  	})
   388  	c.Assert(err, jc.ErrorIsNil)
   389  
   390  	workertest.CheckKilled(c, w)
   391  
   392  	err = w.Wait()
   393  	c.Assert(err, gc.ErrorMatches, "model destroy parameters changed")
   394  }
   395  
   396  type waitDestroyer struct {
   397  	environs.Environ
   398  }
   399  
   400  func (w *waitDestroyer) Destroy(ctx environscontext.ProviderCallContext) error {
   401  	<-ctx.Done()
   402  	return ctx.Err()
   403  }