github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/uniter/resolver_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter_test
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/charm/v12/hooks"
    10  	"github.com/juju/errors"
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/names/v5"
    13  	"github.com/juju/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	gc "gopkg.in/check.v1"
    16  
    17  	"github.com/juju/juju/core/model"
    18  	"github.com/juju/juju/rpc/params"
    19  	"github.com/juju/juju/worker/uniter"
    20  	uniteractions "github.com/juju/juju/worker/uniter/actions"
    21  	unitercharm "github.com/juju/juju/worker/uniter/charm"
    22  	"github.com/juju/juju/worker/uniter/container"
    23  	"github.com/juju/juju/worker/uniter/hook"
    24  	"github.com/juju/juju/worker/uniter/leadership"
    25  	"github.com/juju/juju/worker/uniter/operation"
    26  	"github.com/juju/juju/worker/uniter/reboot"
    27  	"github.com/juju/juju/worker/uniter/remotestate"
    28  	"github.com/juju/juju/worker/uniter/resolver"
    29  	"github.com/juju/juju/worker/uniter/secrets"
    30  	"github.com/juju/juju/worker/uniter/storage"
    31  	"github.com/juju/juju/worker/uniter/upgradeseries"
    32  	"github.com/juju/juju/worker/uniter/verifycharmprofile"
    33  )
    34  
    35  type baseResolverSuite struct {
    36  	stub           testing.Stub
    37  	charmURL       string
    38  	remoteState    remotestate.Snapshot
    39  	opFactory      operation.Factory
    40  	resolver       resolver.Resolver
    41  	resolverConfig uniter.ResolverConfig
    42  
    43  	clearResolved   func() error
    44  	reportHookError func(hook.Info) error
    45  
    46  	workloadEvents        container.WorkloadEvents
    47  	firstOptionalResolver *fakeResolver
    48  	lastOptionalResolver  *fakeResolver
    49  }
    50  
    51  type resolverSuite struct {
    52  	baseResolverSuite
    53  }
    54  
    55  type caasResolverSuite struct {
    56  	resolverSuite
    57  }
    58  
    59  type iaasResolverSuite struct {
    60  	resolverSuite
    61  }
    62  
    63  type conflictedResolverSuite struct {
    64  	baseResolverSuite
    65  }
    66  
    67  type rebootResolverSuite struct {
    68  	baseResolverSuite
    69  }
    70  
    71  var _ = gc.Suite(&caasResolverSuite{})
    72  var _ = gc.Suite(&iaasResolverSuite{})
    73  var _ = gc.Suite(&conflictedResolverSuite{})
    74  var _ = gc.Suite(&rebootResolverSuite{})
    75  
    76  const rebootNotDetected = false
    77  const rebootDetected = true
    78  
    79  func (s *caasResolverSuite) SetUpTest(c *gc.C) {
    80  	s.resolverSuite.SetUpTest(c, model.CAAS, rebootNotDetected)
    81  }
    82  
    83  func (s *iaasResolverSuite) SetUpTest(c *gc.C) {
    84  	s.resolverSuite.SetUpTest(c, model.IAAS, rebootNotDetected)
    85  }
    86  
    87  func (s *conflictedResolverSuite) SetUpTest(_ *gc.C) {
    88  	// NoOp, required to not panic.
    89  }
    90  
    91  func (s *rebootResolverSuite) SetUpTest(_ *gc.C) {
    92  	// NoOp, required to not panic.
    93  }
    94  
    95  func (s *baseResolverSuite) SetUpTest(c *gc.C, modelType model.ModelType, rebootDetected bool) {
    96  	attachments, err := storage.NewAttachments(&dummyStorageAccessor{}, names.NewUnitTag("u/0"), &fakeRW{}, nil)
    97  	c.Assert(err, jc.ErrorIsNil)
    98  	secretsTracker, err := secrets.NewSecrets(&dummySecretsAccessor{}, names.NewUnitTag("u/0"), &fakeRW{}, nil)
    99  	c.Assert(err, jc.ErrorIsNil)
   100  	logger := loggo.GetLogger("test")
   101  
   102  	s.workloadEvents = container.NewWorkloadEvents()
   103  	s.firstOptionalResolver = &fakeResolver{}
   104  	s.lastOptionalResolver = &fakeResolver{}
   105  	s.resolverConfig = uniter.ResolverConfig{
   106  		ClearResolved:       func() error { return s.clearResolved() },
   107  		ReportHookError:     func(info hook.Info) error { return s.reportHookError(info) },
   108  		StartRetryHookTimer: func() { s.stub.AddCall("StartRetryHookTimer") },
   109  		StopRetryHookTimer:  func() { s.stub.AddCall("StopRetryHookTimer") },
   110  		ShouldRetryHooks:    true,
   111  		UpgradeSeries:       upgradeseries.NewResolver(logger),
   112  		Secrets:             secrets.NewSecretsResolver(logger, secretsTracker, func(_ string) {}, func(_ string) {}, func(_ []string) {}),
   113  		Reboot:              reboot.NewResolver(logger, rebootDetected),
   114  		Leadership:          leadership.NewResolver(logger),
   115  		Actions:             uniteractions.NewResolver(logger),
   116  		VerifyCharmProfile:  verifycharmprofile.NewResolver(logger, modelType),
   117  		CreatedRelations:    nopResolver{},
   118  		Relations:           nopResolver{},
   119  		Storage:             storage.NewResolver(logger, attachments, modelType),
   120  		Commands:            nopResolver{},
   121  		ModelType:           modelType,
   122  		OptionalResolvers: []resolver.Resolver{
   123  			s.firstOptionalResolver,
   124  			container.NewRemoteContainerInitResolver(),
   125  			container.NewWorkloadHookResolver(logger, s.workloadEvents, nil),
   126  			s.lastOptionalResolver,
   127  		},
   128  		Logger: logger,
   129  	}
   130  
   131  	s.stub = testing.Stub{}
   132  	s.charmURL = "ch:precise/mysql-2"
   133  	s.remoteState = remotestate.Snapshot{
   134  		CharmURL: s.charmURL,
   135  	}
   136  	s.opFactory = operation.NewFactory(operation.FactoryParams{
   137  		Logger: loggo.GetLogger("test"),
   138  	})
   139  
   140  	if s.clearResolved == nil {
   141  		s.clearResolved = func() error {
   142  			return errors.New("unexpected resolved")
   143  		}
   144  	}
   145  
   146  	s.reportHookError = func(hook.Info) error {
   147  		return nil
   148  		//return errors.New("unexpected report hook error")
   149  	}
   150  
   151  	s.resolver = uniter.NewUniterResolver(s.resolverConfig)
   152  }
   153  
   154  // TestStartedNotInstalled tests whether the Started flag overrides the
   155  // Installed flag being unset, in the event of an unexpected inconsistency in
   156  // local state.
   157  func (s *resolverSuite) TestStartedNotInstalled(c *gc.C) {
   158  	localState := resolver.LocalState{
   159  		CharmURL: s.charmURL,
   160  		State: operation.State{
   161  			Kind:      operation.Continue,
   162  			Installed: false,
   163  			Started:   true,
   164  		},
   165  	}
   166  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   167  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   168  }
   169  
   170  // TestNotStartedNotInstalled tests whether the next operation for an
   171  // uninstalled local state is an install hook operation.
   172  func (s *resolverSuite) TestNotStartedNotInstalled(c *gc.C) {
   173  	localState := resolver.LocalState{
   174  		CharmURL: s.charmURL,
   175  		State: operation.State{
   176  			Kind:      operation.Continue,
   177  			Installed: false,
   178  			Started:   false,
   179  		},
   180  	}
   181  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   182  	c.Assert(err, jc.ErrorIsNil)
   183  	c.Assert(op.String(), gc.Equals, "run install hook")
   184  }
   185  
   186  func (s *iaasResolverSuite) TestUpgradeSeriesPrepareStatusChanged(c *gc.C) {
   187  	localState := resolver.LocalState{
   188  		CharmURL:             s.charmURL,
   189  		UpgradeMachineStatus: model.UpgradeSeriesNotStarted,
   190  		State: operation.State{
   191  			Kind:      operation.Continue,
   192  			Installed: true,
   193  			Started:   true,
   194  		},
   195  	}
   196  
   197  	s.remoteState.UpgradeMachineStatus = model.UpgradeSeriesPrepareStarted
   198  	s.remoteState.UpgradeMachineTarget = "ubuntu@20.04"
   199  
   200  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   201  	c.Assert(err, jc.ErrorIsNil)
   202  	c.Assert(op.String(), gc.Equals, "run pre-series-upgrade hook")
   203  }
   204  
   205  func (s *iaasResolverSuite) TestPostSeriesUpgradeHookRunsWhenConditionsAreMet(c *gc.C) {
   206  	localState := resolver.LocalState{
   207  		CharmURL:              s.charmURL,
   208  		UpgradeMachineStatus:  model.UpgradeSeriesNotStarted,
   209  		LeaderSettingsVersion: 1,
   210  		State: operation.State{
   211  			Kind:       operation.Continue,
   212  			Installed:  true,
   213  			Started:    true,
   214  			ConfigHash: "version1",
   215  		},
   216  	}
   217  	s.remoteState.UpgradeMachineStatus = model.UpgradeSeriesCompleteStarted
   218  
   219  	// Bumping the remote state versions verifies that the upgrade-series
   220  	// completion hook takes precedence.
   221  	s.remoteState.ConfigHash = "version2"
   222  	s.remoteState.LeaderSettingsVersion = 2
   223  
   224  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   225  	c.Assert(err, jc.ErrorIsNil)
   226  	c.Assert(op.String(), gc.Equals, "run post-series-upgrade hook")
   227  }
   228  
   229  func (s *iaasResolverSuite) TestRunsOperationToResetLocalUpgradeSeriesStateWhenConditionsAreMet(c *gc.C) {
   230  	localState := resolver.LocalState{
   231  		CharmURL:             s.charmURL,
   232  		UpgradeMachineStatus: model.UpgradeSeriesCompleted,
   233  		State: operation.State{
   234  			Kind:      operation.Continue,
   235  			Installed: true,
   236  			Started:   true,
   237  		},
   238  	}
   239  	s.remoteState.UpgradeMachineStatus = model.UpgradeSeriesNotStarted
   240  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   241  	c.Assert(err, jc.ErrorIsNil)
   242  	c.Assert(op.String(), gc.Equals, "complete upgrade series")
   243  }
   244  
   245  func (s *iaasResolverSuite) TestUniterIdlesWhenRemoteStateIsUpgradeSeriesCompleted(c *gc.C) {
   246  	localState := resolver.LocalState{
   247  		UpgradeMachineStatus: model.UpgradeSeriesNotStarted,
   248  		CharmURL:             s.charmURL,
   249  		State: operation.State{
   250  			Kind:      operation.Continue,
   251  			Installed: true,
   252  		},
   253  	}
   254  	s.remoteState.UpgradeMachineStatus = model.UpgradeSeriesPrepareCompleted
   255  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   256  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   257  }
   258  
   259  func (s *resolverSuite) TestQueuedHookOnAgentRestart(c *gc.C) {
   260  	s.resolver = uniter.NewUniterResolver(s.resolverConfig)
   261  	s.reportHookError = func(hook.Info) error { return errors.New("unexpected") }
   262  	queued := operation.Queued
   263  	localState := resolver.LocalState{
   264  		CharmURL: s.charmURL,
   265  		State: operation.State{
   266  			Kind:      operation.RunHook,
   267  			Step:      operation.Pending,
   268  			Installed: true,
   269  			Started:   true,
   270  			Hook: &hook.Info{
   271  				Kind: hooks.ConfigChanged,
   272  			},
   273  			HookStep: &queued,
   274  		},
   275  	}
   276  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   277  	c.Assert(err, jc.ErrorIsNil)
   278  	c.Assert(op.String(), gc.Equals, "run config-changed hook")
   279  	s.stub.CheckNoCalls(c)
   280  }
   281  
   282  func (s *resolverSuite) TestPendingHookOnAgentRestart(c *gc.C) {
   283  	s.resolverConfig.ShouldRetryHooks = false
   284  	s.resolver = uniter.NewUniterResolver(s.resolverConfig)
   285  	hookError := false
   286  	s.reportHookError = func(hook.Info) error {
   287  		hookError = true
   288  		return nil
   289  	}
   290  	queued := operation.Pending
   291  	localState := resolver.LocalState{
   292  		CharmURL: s.charmURL,
   293  		State: operation.State{
   294  			Kind:      operation.RunHook,
   295  			Step:      operation.Pending,
   296  			Installed: true,
   297  			Started:   true,
   298  			Hook: &hook.Info{
   299  				Kind: hooks.ConfigChanged,
   300  			},
   301  			HookStep: &queued,
   302  		},
   303  	}
   304  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   305  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   306  	c.Assert(hookError, jc.IsTrue)
   307  	s.stub.CheckNoCalls(c)
   308  }
   309  
   310  func (s *resolverSuite) TestHookErrorDoesNotStartRetryTimerIfShouldRetryFalse(c *gc.C) {
   311  	s.resolverConfig.ShouldRetryHooks = false
   312  	s.resolver = uniter.NewUniterResolver(s.resolverConfig)
   313  	s.reportHookError = func(hook.Info) error { return nil }
   314  	localState := resolver.LocalState{
   315  		CharmURL: s.charmURL,
   316  		State: operation.State{
   317  			Kind:      operation.RunHook,
   318  			Step:      operation.Pending,
   319  			Installed: true,
   320  			Started:   true,
   321  			Hook: &hook.Info{
   322  				Kind: hooks.ConfigChanged,
   323  			},
   324  		},
   325  	}
   326  	// Run the resolver; we should not attempt a hook retry
   327  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   328  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   329  	s.stub.CheckNoCalls(c)
   330  }
   331  
   332  func (s *resolverSuite) TestHookErrorStartRetryTimer(c *gc.C) {
   333  	s.reportHookError = func(hook.Info) error { return nil }
   334  	localState := resolver.LocalState{
   335  		CharmURL: s.charmURL,
   336  		State: operation.State{
   337  			Kind:      operation.RunHook,
   338  			Step:      operation.Pending,
   339  			Installed: true,
   340  			Started:   true,
   341  			Hook: &hook.Info{
   342  				Kind: hooks.ConfigChanged,
   343  			},
   344  		},
   345  	}
   346  	// Run the resolver twice; we should start the hook retry
   347  	// timer on the first time through, no change on the second.
   348  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   349  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   350  	s.stub.CheckCallNames(c, "StartRetryHookTimer")
   351  
   352  	_, err = s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   353  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   354  	s.stub.CheckCallNames(c, "StartRetryHookTimer") // no change
   355  }
   356  
   357  func (s *resolverSuite) TestHookErrorStartRetryTimerAgain(c *gc.C) {
   358  	s.reportHookError = func(hook.Info) error { return nil }
   359  	localState := resolver.LocalState{
   360  		CharmURL: s.charmURL,
   361  		State: operation.State{
   362  			Kind:      operation.RunHook,
   363  			Step:      operation.Pending,
   364  			Installed: true,
   365  			Started:   true,
   366  			Hook: &hook.Info{
   367  				Kind: hooks.ConfigChanged,
   368  			},
   369  		},
   370  	}
   371  
   372  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   373  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   374  	s.stub.CheckCallNames(c, "StartRetryHookTimer")
   375  
   376  	s.remoteState.RetryHookVersion = 1
   377  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   378  	c.Assert(err, jc.ErrorIsNil)
   379  	c.Assert(op.String(), gc.Equals, "run config-changed hook")
   380  	s.stub.CheckCallNames(c, "StartRetryHookTimer") // no change
   381  	localState.RetryHookVersion = 1
   382  
   383  	_, err = s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   384  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   385  	s.stub.CheckCallNames(c, "StartRetryHookTimer", "StartRetryHookTimer")
   386  }
   387  
   388  func (s *resolverSuite) TestResolvedRetryHooksStopRetryTimer(c *gc.C) {
   389  	// Resolving a failed hook should stop the retry timer.
   390  	s.testResolveHookErrorStopRetryTimer(c, params.ResolvedRetryHooks)
   391  }
   392  
   393  func (s *resolverSuite) TestResolvedNoHooksStopRetryTimer(c *gc.C) {
   394  	// Resolving a failed hook should stop the retry timer.
   395  	s.testResolveHookErrorStopRetryTimer(c, params.ResolvedNoHooks)
   396  }
   397  
   398  func (s *resolverSuite) testResolveHookErrorStopRetryTimer(c *gc.C, mode params.ResolvedMode) {
   399  	s.stub.ResetCalls()
   400  	s.clearResolved = func() error { return nil }
   401  	s.reportHookError = func(hook.Info) error { return nil }
   402  	localState := resolver.LocalState{
   403  		CharmURL: s.charmURL,
   404  		State: operation.State{
   405  			Kind:      operation.RunHook,
   406  			Step:      operation.Pending,
   407  			Installed: true,
   408  			Started:   true,
   409  			Hook: &hook.Info{
   410  				Kind: hooks.ConfigChanged,
   411  			},
   412  		},
   413  	}
   414  
   415  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   416  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   417  	s.stub.CheckCallNames(c, "StartRetryHookTimer")
   418  
   419  	s.remoteState.ResolvedMode = mode
   420  	_, err = s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   421  	c.Assert(err, jc.ErrorIsNil)
   422  	s.stub.CheckCallNames(c, "StartRetryHookTimer", "StopRetryHookTimer")
   423  }
   424  
   425  func (s *resolverSuite) TestRunHookStopRetryTimer(c *gc.C) {
   426  	s.reportHookError = func(hook.Info) error { return nil }
   427  	localState := resolver.LocalState{
   428  		CharmURL: s.charmURL,
   429  		State: operation.State{
   430  			Kind:      operation.RunHook,
   431  			Step:      operation.Pending,
   432  			Installed: true,
   433  			Started:   true,
   434  			Hook: &hook.Info{
   435  				Kind: hooks.ConfigChanged,
   436  			},
   437  		},
   438  	}
   439  
   440  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   441  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   442  	s.stub.CheckCallNames(c, "StartRetryHookTimer")
   443  
   444  	localState.Kind = operation.Continue
   445  	_, err = s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   446  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   447  	s.stub.CheckCallNames(c, "StartRetryHookTimer", "StopRetryHookTimer")
   448  }
   449  
   450  func (s *resolverSuite) TestRunsConfigChangedIfConfigHashChanges(c *gc.C) {
   451  	localState := resolver.LocalState{
   452  		CharmURL: s.charmURL,
   453  		State: operation.State{
   454  			Kind:       operation.Continue,
   455  			Installed:  true,
   456  			Started:    true,
   457  			ConfigHash: "somehash",
   458  		},
   459  	}
   460  	s.remoteState.ConfigHash = "differenthash"
   461  
   462  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   463  	c.Assert(err, jc.ErrorIsNil)
   464  	c.Assert(op.String(), gc.Equals, "run config-changed hook")
   465  }
   466  
   467  func (s *resolverSuite) TestRunsConfigChangedIfTrustHashChanges(c *gc.C) {
   468  	localState := resolver.LocalState{
   469  		CharmURL: s.charmURL,
   470  		State: operation.State{
   471  			Kind:      operation.Continue,
   472  			Installed: true,
   473  			Started:   true,
   474  			TrustHash: "somehash",
   475  		},
   476  	}
   477  	s.remoteState.TrustHash = "differenthash"
   478  
   479  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   480  	c.Assert(err, jc.ErrorIsNil)
   481  	c.Assert(op.String(), gc.Equals, "run config-changed hook")
   482  }
   483  
   484  func (s *resolverSuite) TestRunsConfigChangedIfAddressesHashChanges(c *gc.C) {
   485  	localState := resolver.LocalState{
   486  		CharmURL: s.charmURL,
   487  		State: operation.State{
   488  			Kind:          operation.Continue,
   489  			Installed:     true,
   490  			Started:       true,
   491  			AddressesHash: "somehash",
   492  		},
   493  	}
   494  	s.remoteState.AddressesHash = "differenthash"
   495  
   496  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   497  	c.Assert(err, jc.ErrorIsNil)
   498  	c.Assert(op.String(), gc.Equals, "run config-changed hook")
   499  }
   500  
   501  func (s *resolverSuite) TestNoOperationIfHashesAllMatch(c *gc.C) {
   502  	localState := resolver.LocalState{
   503  		CharmURL: s.charmURL,
   504  		State: operation.State{
   505  			Kind:          operation.Continue,
   506  			Installed:     true,
   507  			Started:       true,
   508  			ConfigHash:    "config",
   509  			TrustHash:     "trust",
   510  			AddressesHash: "addresses",
   511  		},
   512  	}
   513  	s.remoteState.ConfigHash = "config"
   514  	s.remoteState.TrustHash = "trust"
   515  	s.remoteState.AddressesHash = "addresses"
   516  
   517  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   518  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   519  }
   520  
   521  func (s *resolverSuite) TestUpgradeOperation(c *gc.C) {
   522  	opFactory := setupUpgradeOpFactory()
   523  	localState := resolver.LocalState{
   524  		CharmURL: s.charmURL,
   525  		State: operation.State{
   526  			Kind:      operation.Upgrade,
   527  			Installed: true,
   528  			Started:   true,
   529  		},
   530  	}
   531  	op, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   532  	c.Assert(err, jc.ErrorIsNil)
   533  	c.Assert(op.String(), gc.Equals, fmt.Sprintf("upgrade to %s", s.charmURL))
   534  }
   535  
   536  func (s *iaasResolverSuite) TestUpgradeOperationVerifyCPFail(c *gc.C) {
   537  	opFactory := setupUpgradeOpFactory()
   538  	localState := resolver.LocalState{
   539  		CharmURL: s.charmURL,
   540  		State: operation.State{
   541  			Kind:      operation.Upgrade,
   542  			Installed: true,
   543  			Started:   true,
   544  		},
   545  	}
   546  	s.remoteState.CharmProfileRequired = true
   547  	_, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   548  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   549  }
   550  
   551  func (s *resolverSuite) TestContinueUpgradeOperation(c *gc.C) {
   552  	opFactory := setupUpgradeOpFactory()
   553  	localState := resolver.LocalState{
   554  		CharmURL: s.charmURL,
   555  		State: operation.State{
   556  			Kind:      operation.Continue,
   557  			Installed: true,
   558  			Started:   true,
   559  		},
   560  	}
   561  	s.setupForceCharmModifiedTrue()
   562  	op, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   563  	c.Assert(err, jc.ErrorIsNil)
   564  	c.Assert(op.String(), gc.Equals, fmt.Sprintf("upgrade to %s", s.charmURL))
   565  }
   566  
   567  func (s *resolverSuite) TestNoOperationWithOptionalResolvers(c *gc.C) {
   568  	localState := resolver.LocalState{
   569  		CharmURL: s.charmURL,
   570  		State: operation.State{
   571  			Kind:          operation.Continue,
   572  			Installed:     true,
   573  			Started:       true,
   574  			ConfigHash:    "config",
   575  			TrustHash:     "trust",
   576  			AddressesHash: "addresses",
   577  		},
   578  	}
   579  	s.remoteState.ConfigHash = "config"
   580  	s.remoteState.TrustHash = "trust"
   581  	s.remoteState.AddressesHash = "addresses"
   582  
   583  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   584  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   585  	c.Assert(s.firstOptionalResolver.callCount, gc.Equals, 1)
   586  	c.Assert(s.lastOptionalResolver.callCount, gc.Equals, 1)
   587  }
   588  
   589  func (s *resolverSuite) TestOperationWithOptionalResolvers(c *gc.C) {
   590  	localState := resolver.LocalState{
   591  		CharmURL: s.charmURL,
   592  		State: operation.State{
   593  			Kind:          operation.Continue,
   594  			Installed:     true,
   595  			Started:       true,
   596  			ConfigHash:    "config",
   597  			TrustHash:     "trust",
   598  			AddressesHash: "addresses",
   599  		},
   600  	}
   601  	s.remoteState.ConfigHash = "config"
   602  	s.remoteState.TrustHash = "trust"
   603  	s.remoteState.AddressesHash = "addresses"
   604  
   605  	s.firstOptionalResolver.op = &fakeNoOp{}
   606  
   607  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   608  	c.Assert(err, jc.ErrorIsNil)
   609  	c.Assert(op, gc.Equals, s.firstOptionalResolver.op)
   610  	c.Assert(s.firstOptionalResolver.callCount, gc.Equals, 1)
   611  	c.Assert(s.lastOptionalResolver.callCount, gc.Equals, 0)
   612  }
   613  
   614  func (s *iaasResolverSuite) TestContinueUpgradeOperationVerifyCPFail(c *gc.C) {
   615  	opFactory := setupUpgradeOpFactory()
   616  	localState := resolver.LocalState{
   617  		CharmURL: s.charmURL,
   618  		State: operation.State{
   619  			Kind:      operation.Continue,
   620  			Installed: true,
   621  			Started:   true,
   622  		},
   623  	}
   624  	s.setupForceCharmModifiedTrue()
   625  	s.remoteState.CharmProfileRequired = true
   626  	_, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   627  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   628  }
   629  
   630  func (s *resolverSuite) TestRunHookPendingUpgradeOperation(c *gc.C) {
   631  	opFactory := setupUpgradeOpFactory()
   632  	localState := resolver.LocalState{
   633  		CharmURL: s.charmURL,
   634  		State: operation.State{
   635  			Kind:      operation.RunHook,
   636  			Hook:      &hook.Info{Kind: hooks.ConfigChanged},
   637  			Installed: true,
   638  			Started:   true,
   639  			Step:      operation.Pending,
   640  		},
   641  	}
   642  	s.setupForceCharmModifiedTrue()
   643  	op, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   644  	c.Assert(err, jc.ErrorIsNil)
   645  	c.Assert(op.String(), gc.Equals, fmt.Sprintf("upgrade to %s", s.charmURL))
   646  }
   647  
   648  func (s *resolverSuite) TestRunsSecretRotated(c *gc.C) {
   649  	localState := resolver.LocalState{
   650  		State: operation.State{
   651  			Kind:      operation.Continue,
   652  			Installed: true,
   653  			Started:   true,
   654  			Leader:    true,
   655  		},
   656  	}
   657  	s.remoteState.Leader = true
   658  	s.remoteState.SecretRotations = []string{"secret:9m4e2mr0ui3e8a215n4g"}
   659  
   660  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   661  	c.Assert(err, jc.ErrorIsNil)
   662  	c.Assert(op.String(), gc.Equals, "run secret-rotate (secret:9m4e2mr0ui3e8a215n4g) hook")
   663  }
   664  
   665  func (s *conflictedResolverSuite) TestNextOpConflicted(c *gc.C) {
   666  	s.baseResolverSuite.SetUpTest(c, model.IAAS, rebootNotDetected)
   667  	opFactory := setupUpgradeOpFactory()
   668  	localState := resolver.LocalState{
   669  		CharmURL:   s.charmURL,
   670  		Conflicted: true,
   671  		State: operation.State{
   672  			Kind:      operation.Upgrade,
   673  			Installed: true,
   674  			Started:   true,
   675  		},
   676  	}
   677  	_, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   678  	c.Assert(err, gc.Equals, resolver.ErrWaiting)
   679  }
   680  
   681  func (s *conflictedResolverSuite) TestNextOpConflictedVerifyCPFail(c *gc.C) {
   682  	s.baseResolverSuite.SetUpTest(c, model.IAAS, rebootNotDetected)
   683  	opFactory := setupUpgradeOpFactory()
   684  	localState := resolver.LocalState{
   685  		CharmURL:   s.charmURL,
   686  		Conflicted: true,
   687  		State: operation.State{
   688  			Kind:      operation.Upgrade,
   689  			Installed: true,
   690  			Started:   true,
   691  		},
   692  	}
   693  	s.remoteState.CharmProfileRequired = true
   694  	_, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   695  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   696  }
   697  
   698  func (s *conflictedResolverSuite) TestNextOpConflictedNewResolvedUpgrade(c *gc.C) {
   699  	s.clearResolved = func() error {
   700  		return nil
   701  	}
   702  	s.baseResolverSuite.SetUpTest(c, model.IAAS, rebootNotDetected)
   703  	opFactory := setupUpgradeOpFactory()
   704  	localState := resolver.LocalState{
   705  		CharmURL:   s.charmURL,
   706  		Conflicted: true,
   707  		State: operation.State{
   708  			Kind:      operation.Upgrade,
   709  			Installed: true,
   710  			Started:   true,
   711  		},
   712  	}
   713  	s.remoteState.ResolvedMode = params.ResolvedRetryHooks
   714  	op, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   715  	c.Assert(err, jc.ErrorIsNil)
   716  	c.Assert(op.String(), gc.Equals, fmt.Sprintf("continue upgrade to %s", s.charmURL))
   717  }
   718  
   719  func (s *conflictedResolverSuite) TestNextOpConflictedNewRevertUpgrade(c *gc.C) {
   720  	s.baseResolverSuite.SetUpTest(c, model.IAAS, rebootNotDetected)
   721  	opFactory := setupUpgradeOpFactory()
   722  	localState := resolver.LocalState{
   723  		CharmURL:   s.charmURL,
   724  		Conflicted: true,
   725  		State: operation.State{
   726  			Kind:      operation.Upgrade,
   727  			Installed: true,
   728  			Started:   true,
   729  		},
   730  	}
   731  	s.setupForceCharmModifiedTrue()
   732  	op, err := s.resolver.NextOp(localState, s.remoteState, opFactory)
   733  	c.Assert(err, jc.ErrorIsNil)
   734  	c.Assert(op.String(), gc.Equals, fmt.Sprintf("switch upgrade to %s", s.charmURL))
   735  }
   736  
   737  func (s *rebootResolverSuite) TestNopResolverForNonIAASModels(c *gc.C) {
   738  	s.baseResolverSuite.SetUpTest(c, model.CAAS, rebootDetected)
   739  
   740  	localState := resolver.LocalState{
   741  		CharmURL: s.charmURL,
   742  		State: operation.State{
   743  			Kind:      operation.Continue,
   744  			Installed: true,
   745  			Started:   true,
   746  		},
   747  	}
   748  	_, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   749  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   750  }
   751  
   752  func (s *rebootResolverSuite) TestStartHookTriggerPostReboot(c *gc.C) {
   753  	s.baseResolverSuite.SetUpTest(c, model.IAAS, rebootDetected)
   754  
   755  	localState := resolver.LocalState{
   756  		CharmURL: s.charmURL,
   757  		State: operation.State{
   758  			Kind:      operation.Continue,
   759  			Installed: true,
   760  			Started:   true, // charm must be started
   761  		},
   762  	}
   763  	s.remoteState.UpgradeMachineStatus = model.UpgradeSeriesNotStarted
   764  
   765  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   766  	c.Assert(err, jc.ErrorIsNil)
   767  	c.Assert(op.String(), gc.Equals, "run start hook")
   768  
   769  	// Ensure that start-post-reboot is only triggered once
   770  	_, err = s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   771  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   772  }
   773  
   774  func (s *rebootResolverSuite) TestStartHookDeferredWhenUpgradeIsInProgress(c *gc.C) {
   775  	s.baseResolverSuite.SetUpTest(c, model.IAAS, rebootDetected)
   776  
   777  	localState := resolver.LocalState{
   778  		CharmURL:             s.charmURL,
   779  		UpgradeMachineStatus: model.UpgradeSeriesNotStarted,
   780  		State: operation.State{
   781  			Kind:      operation.Continue,
   782  			Installed: true,
   783  			Started:   true, // charm must be started
   784  		},
   785  	}
   786  
   787  	// Controller indicates a series upgrade is in progress
   788  	statusChecks := []struct {
   789  		status model.UpgradeSeriesStatus
   790  		expOp  string
   791  		expErr error
   792  	}{
   793  		{
   794  			status: model.UpgradeSeriesPrepareStarted,
   795  			expOp:  "run pre-series-upgrade hook",
   796  		},
   797  		{
   798  			status: model.UpgradeSeriesPrepareRunning,
   799  			expErr: resolver.ErrNoOperation,
   800  		},
   801  		{
   802  			status: model.UpgradeSeriesPrepareCompleted,
   803  			expErr: resolver.ErrNoOperation,
   804  		},
   805  		{
   806  			status: model.UpgradeSeriesCompleteStarted,
   807  			expOp:  "run post-series-upgrade hook",
   808  		},
   809  		{
   810  			status: model.UpgradeSeriesCompleteRunning,
   811  			expErr: resolver.ErrNoOperation,
   812  		},
   813  		{
   814  			status: model.UpgradeSeriesCompleted,
   815  			expErr: resolver.ErrNoOperation,
   816  		},
   817  	}
   818  
   819  	for _, statusTest := range statusChecks {
   820  		c.Logf("triggering resolver with upgrade status: %s", statusTest.status)
   821  		s.remoteState.UpgradeMachineStatus = statusTest.status
   822  		s.remoteState.UpgradeMachineTarget = "ubuntu@20.04"
   823  
   824  		op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   825  		if statusTest.expErr != nil {
   826  			c.Assert(err, gc.Equals, statusTest.expErr)
   827  		} else {
   828  			c.Assert(err, jc.ErrorIsNil)
   829  			c.Assert(op.String(), gc.Equals, statusTest.expOp)
   830  		}
   831  	}
   832  
   833  	// Mutate remote state to indicate that upgrade has not been started
   834  	// and run the resolver again. This time, we should get back the
   835  	// deferred start hook.
   836  	s.remoteState.UpgradeMachineStatus = model.UpgradeSeriesNotStarted
   837  
   838  	op, err := s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   839  	c.Assert(err, jc.ErrorIsNil)
   840  	c.Assert(op.String(), gc.Equals, "run start hook")
   841  
   842  	// Ensure that start-post-reboot is only triggered once
   843  	_, err = s.resolver.NextOp(localState, s.remoteState, s.opFactory)
   844  	c.Assert(err, gc.Equals, resolver.ErrNoOperation)
   845  }
   846  
   847  func setupUpgradeOpFactory() operation.Factory {
   848  	return operation.NewFactory(operation.FactoryParams{
   849  		Deployer: &fakeDeployer{},
   850  		Logger:   loggo.GetLogger("test"),
   851  	})
   852  }
   853  
   854  func (s *baseResolverSuite) setupForceCharmModifiedTrue() {
   855  	s.remoteState.ForceCharmUpgrade = true
   856  	s.remoteState.CharmModifiedVersion = 3
   857  }
   858  
   859  // fakeRW implements the storage.UnitStateReadWriter interface
   860  // so SetUpTests can call storage.NewAttachments.  It doesn't
   861  // need to do anything.
   862  type fakeRW struct {
   863  }
   864  
   865  func (m *fakeRW) State() (params.UnitStateResult, error) {
   866  	return params.UnitStateResult{}, nil
   867  }
   868  
   869  func (m *fakeRW) SetState(_ params.SetUnitStateArg) error {
   870  	return nil
   871  }
   872  
   873  // fakeDeployer implements the charm.Deployter interface
   874  // so Upgrade operations can call validate deployer not nil.  It doesn't
   875  // need to do anything.
   876  type fakeDeployer struct {
   877  }
   878  
   879  func (m *fakeDeployer) Stage(_ unitercharm.BundleInfo, _ <-chan struct{}) error {
   880  	return nil
   881  }
   882  
   883  func (m *fakeDeployer) Deploy() error {
   884  	return nil
   885  }
   886  
   887  type fakeResolver struct {
   888  	callCount int
   889  	op        operation.Operation
   890  }
   891  
   892  func (r *fakeResolver) NextOp(
   893  	localState resolver.LocalState,
   894  	remoteState remotestate.Snapshot,
   895  	opFactory operation.Factory,
   896  ) (operation.Operation, error) {
   897  	r.callCount++
   898  	if r.op != nil {
   899  		return r.op, nil
   900  	}
   901  	return nil, resolver.ErrNoOperation
   902  }
   903  
   904  type fakeNoOp struct {
   905  	operation.Operation
   906  }