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

     1  // Copyright 2014-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package operation
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/charm/v12/hooks"
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  
    13  	"github.com/juju/juju/core/model"
    14  	"github.com/juju/juju/core/relation"
    15  	"github.com/juju/juju/core/secrets"
    16  	"github.com/juju/juju/core/status"
    17  	"github.com/juju/juju/worker/common/charmrunner"
    18  	"github.com/juju/juju/worker/uniter/hook"
    19  	"github.com/juju/juju/worker/uniter/remotestate"
    20  	"github.com/juju/juju/worker/uniter/runner"
    21  	"github.com/juju/juju/worker/uniter/runner/context"
    22  	"github.com/juju/juju/worker/uniter/runner/jujuc"
    23  )
    24  
    25  type runHook struct {
    26  	info hook.Info
    27  
    28  	callbacks     Callbacks
    29  	runnerFactory runner.Factory
    30  
    31  	name   string
    32  	runner runner.Runner
    33  	logger Logger
    34  
    35  	hookFound bool
    36  
    37  	RequiresMachineLock
    38  }
    39  
    40  // String is part of the Operation interface.
    41  func (rh *runHook) String() string {
    42  	suffix := ""
    43  	switch {
    44  	case rh.info.Kind.IsRelation():
    45  		if rh.info.RemoteUnit == "" {
    46  			suffix = fmt.Sprintf(" (%d; app: %s)", rh.info.RelationId, rh.info.RemoteApplication)
    47  		} else if rh.info.DepartingUnit != "" {
    48  			suffix = fmt.Sprintf(" (%d; unit: %s, departee: %s)", rh.info.RelationId, rh.info.RemoteUnit, rh.info.DepartingUnit)
    49  		} else {
    50  			suffix = fmt.Sprintf(" (%d; unit: %s)", rh.info.RelationId, rh.info.RemoteUnit)
    51  		}
    52  	case rh.info.Kind.IsStorage():
    53  		suffix = fmt.Sprintf(" (%s)", rh.info.StorageId)
    54  	case rh.info.Kind.IsSecret():
    55  		if rh.info.SecretRevision == 0 || !hook.SecretHookRequiresRevision(rh.info.Kind) {
    56  			suffix = fmt.Sprintf(" (%s)", rh.info.SecretURI)
    57  		} else {
    58  			suffix = fmt.Sprintf(" (%s/%d)", rh.info.SecretURI, rh.info.SecretRevision)
    59  		}
    60  	}
    61  	return fmt.Sprintf("run %s%s hook", rh.info.Kind, suffix)
    62  }
    63  
    64  // Prepare ensures the hook can be executed.
    65  // Prepare is part of the Operation interface.
    66  func (rh *runHook) Prepare(state State) (*State, error) {
    67  	name, err := rh.callbacks.PrepareHook(rh.info)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	rnr, err := rh.runnerFactory.NewHookRunner(rh.info)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	kind := hooks.Kind(name)
    77  	leaderNeeded := kind == hooks.LeaderElected
    78  	if kind == hooks.SecretRotate || kind == hooks.SecretExpired || kind == hooks.SecretRemove {
    79  		secretMetadata, err := rnr.Context().SecretMetadata()
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  		if uri, err := secrets.ParseURI(rh.info.SecretURI); err == nil {
    84  			md, ok := secretMetadata[uri.ID]
    85  			leaderNeeded = ok && md.Owner.Kind() != names.UnitTagKind
    86  		}
    87  	}
    88  
    89  	if leaderNeeded {
    90  		// Check if leadership has changed between queueing of the hook and
    91  		// Actual execution. Skip execution if we are no longer the leader.
    92  		var isLeader bool
    93  		isLeader, err = rnr.Context().IsLeader()
    94  		if err == nil && !isLeader {
    95  			rh.logger.Infof("unit is no longer the leader; skipping %q execution", name)
    96  			return nil, ErrSkipExecute
    97  		}
    98  		if err != nil {
    99  			return nil, err
   100  		}
   101  	}
   102  
   103  	err = rnr.Context().Prepare()
   104  	if err != nil {
   105  		return nil, errors.Trace(err)
   106  	}
   107  	rh.name = name
   108  	rh.runner = rnr
   109  
   110  	return stateChange{
   111  		Kind: RunHook,
   112  		Step: Pending,
   113  		Hook: &rh.info,
   114  	}.apply(state), nil
   115  }
   116  
   117  // RunningHookMessage returns the info message to print when running a hook.
   118  func RunningHookMessage(hookName string, info hook.Info) string {
   119  	if info.Kind.IsRelation() && info.RemoteUnit != "" {
   120  		return fmt.Sprintf("running %s hook for %s", hookName, info.RemoteUnit)
   121  	}
   122  	if info.Kind.IsSecret() {
   123  		revMsg := ""
   124  		if info.SecretRevision > 0 && hook.SecretHookRequiresRevision(info.Kind) {
   125  			revMsg = fmt.Sprintf("/%d", info.SecretRevision)
   126  		}
   127  		return fmt.Sprintf("running %s hook for %s%s", hookName, info.SecretURI, revMsg)
   128  	}
   129  	return fmt.Sprintf("running %s hook", hookName)
   130  }
   131  
   132  // Execute runs the hook.
   133  // Execute is part of the Operation interface.
   134  func (rh *runHook) Execute(state State) (*State, error) {
   135  	message := RunningHookMessage(rh.name, rh.info)
   136  	if err := rh.beforeHook(state); err != nil {
   137  		return nil, err
   138  	}
   139  	// In order to reduce controller load, the uniter no longer
   140  	// records when it is running the update-status hook. If the
   141  	// hook fails, that is recorded.
   142  	if hooks.Kind(rh.name) != hooks.UpdateStatus {
   143  		if err := rh.callbacks.SetExecutingStatus(message); err != nil {
   144  			return nil, err
   145  		}
   146  	}
   147  	// The before hook may have updated unit status and we don't want that
   148  	// to count so reset it here before running the hook.
   149  	rh.runner.Context().ResetExecutionSetUnitStatus()
   150  
   151  	rh.hookFound = true
   152  	step := Done
   153  
   154  	handlerType, err := rh.runner.RunHook(rh.name)
   155  	cause := errors.Cause(err)
   156  	switch {
   157  	case charmrunner.IsMissingHookError(cause):
   158  		rh.hookFound = false
   159  		err = nil
   160  	case cause == context.ErrRequeueAndReboot:
   161  		step = Queued
   162  		fallthrough
   163  	case cause == context.ErrReboot:
   164  		err = ErrNeedsReboot
   165  	case cause == runner.ErrTerminated:
   166  		// Queue the hook again so it is re-run.
   167  		// It is likely the whole process group was terminated as
   168  		// part of shutdown, but in case not, the unit agent will
   169  		// treat the hook as pending (not queued) and record a hook error.
   170  		rh.logger.Warningf("hook %q was terminated", rh.name)
   171  		step = Queued
   172  		return stateChange{
   173  			Kind:     RunHook,
   174  			Step:     step,
   175  			Hook:     &rh.info,
   176  			HookStep: &step,
   177  		}.apply(state), runner.ErrTerminated
   178  	case err == nil:
   179  	default:
   180  		rh.logger.Errorf("hook %q (via %s) failed: %v", rh.name, handlerType, err)
   181  		rh.callbacks.NotifyHookFailed(rh.name, rh.runner.Context())
   182  		return nil, ErrHookFailed
   183  	}
   184  
   185  	if rh.hookFound {
   186  		rh.logger.Infof("ran %q hook (via %s)", rh.name, handlerType)
   187  		rh.callbacks.NotifyHookCompleted(rh.name, rh.runner.Context())
   188  	} else {
   189  		rh.logger.Infof("skipped %q hook (missing)", rh.name)
   190  	}
   191  
   192  	var hasRunStatusSet bool
   193  	var afterHookErr error
   194  	if hasRunStatusSet, afterHookErr = rh.afterHook(state); afterHookErr != nil {
   195  		return nil, afterHookErr
   196  	}
   197  	return stateChange{
   198  		Kind:            RunHook,
   199  		Step:            step,
   200  		Hook:            &rh.info,
   201  		HookStep:        &step,
   202  		HasRunStatusSet: hasRunStatusSet,
   203  	}.apply(state), err
   204  }
   205  
   206  func (rh *runHook) beforeHook(state State) error {
   207  	var err error
   208  	switch rh.info.Kind {
   209  	case hooks.Install:
   210  		// If the charm has already updated the unit status in a previous hook,
   211  		// then don't overwrite that here.
   212  		if !state.StatusSet {
   213  			err = rh.runner.Context().SetUnitStatus(jujuc.StatusInfo{
   214  				Status: string(status.Maintenance),
   215  				Info:   status.MessageInstallingCharm,
   216  			})
   217  		}
   218  	case hooks.Stop:
   219  		err = rh.runner.Context().SetUnitStatus(jujuc.StatusInfo{
   220  			Status: string(status.Maintenance),
   221  			Info:   "stopping charm software",
   222  		})
   223  	case hooks.Remove:
   224  		err = rh.runner.Context().SetUnitStatus(jujuc.StatusInfo{
   225  			Status: string(status.Maintenance),
   226  			Info:   "cleaning up prior to charm deletion",
   227  		})
   228  	case hooks.PreSeriesUpgrade:
   229  		err = rh.callbacks.SetUpgradeSeriesStatus(model.UpgradeSeriesPrepareRunning, "pre-series-upgrade hook running")
   230  	case hooks.PostSeriesUpgrade:
   231  		err = rh.callbacks.SetUpgradeSeriesStatus(model.UpgradeSeriesCompleteRunning, "post-series-upgrade hook running")
   232  	}
   233  
   234  	if err != nil {
   235  		rh.logger.Errorf("error updating workload status before %v hook: %v", rh.info.Kind, err)
   236  		return err
   237  	}
   238  	return nil
   239  }
   240  
   241  // afterHook runs after a hook completes, or after a hook that is
   242  // not implemented by the charm is expected to have run if it were
   243  // implemented.
   244  func (rh *runHook) afterHook(state State) (_ bool, err error) {
   245  	defer func() {
   246  		if err != nil {
   247  			rh.logger.Errorf("error updating workload status after %v hook: %v", rh.info.Kind, err)
   248  		}
   249  	}()
   250  
   251  	ctx := rh.runner.Context()
   252  	hasRunStatusSet := ctx.HasExecutionSetUnitStatus() || state.StatusSet
   253  	switch rh.info.Kind {
   254  	case hooks.Stop:
   255  		err = ctx.SetUnitStatus(jujuc.StatusInfo{
   256  			Status: string(status.Maintenance),
   257  		})
   258  	case hooks.Remove:
   259  		// Charm is no longer of this world.
   260  		err = ctx.SetUnitStatus(jujuc.StatusInfo{
   261  			Status: string(status.Terminated),
   262  		})
   263  	case hooks.Start:
   264  		if hasRunStatusSet {
   265  			break
   266  		}
   267  		rh.logger.Debugf("unit %v has started but has not yet set status", ctx.UnitName())
   268  		// We've finished the start hook and the charm has not updated its
   269  		// own status so we'll set it to unknown.
   270  		err = ctx.SetUnitStatus(jujuc.StatusInfo{
   271  			Status: string(status.Unknown),
   272  		})
   273  	case hooks.RelationBroken:
   274  		var isLeader bool
   275  		isLeader, err = ctx.IsLeader()
   276  		if !isLeader || err != nil {
   277  			return hasRunStatusSet && err == nil, err
   278  		}
   279  		rel, rErr := ctx.Relation(rh.info.RelationId)
   280  		if rErr == nil && rel.Suspended() {
   281  			err = rel.SetStatus(relation.Suspended)
   282  		}
   283  	}
   284  	return hasRunStatusSet && err == nil, err
   285  }
   286  
   287  func createUpgradeSeriesStatusMessage(name string, hookFound bool) string {
   288  	if !hookFound {
   289  		return fmt.Sprintf("%s hook not found, skipping", name)
   290  	}
   291  	return fmt.Sprintf("%s completed", name)
   292  }
   293  
   294  // Commit updates relation state to include the fact of the hook's execution,
   295  // records the impact of start and collect-metrics hooks, and queues follow-up
   296  // config-changed hooks to directly follow install and upgrade-charm hooks.
   297  // Commit is part of the Operation interface.
   298  func (rh *runHook) Commit(state State) (*State, error) {
   299  	var err error
   300  	err = rh.callbacks.CommitHook(rh.info)
   301  	if err != nil {
   302  		return nil, errors.Annotatef(err, "committing hook %q", rh.name)
   303  	}
   304  
   305  	change := stateChange{
   306  		Kind: Continue,
   307  		Step: Pending,
   308  	}
   309  
   310  	switch rh.info.Kind {
   311  	case hooks.ConfigChanged:
   312  		if !state.Started {
   313  			change = stateChange{
   314  				Kind: RunHook,
   315  				Step: Queued,
   316  				Hook: &hook.Info{Kind: hooks.Start},
   317  			}
   318  		}
   319  	case hooks.UpgradeCharm:
   320  		change = stateChange{
   321  			Kind: RunHook,
   322  			Step: Queued,
   323  			Hook: &hook.Info{Kind: hooks.ConfigChanged},
   324  		}
   325  	case hooks.PreSeriesUpgrade:
   326  		message := createUpgradeSeriesStatusMessage(rh.name, rh.hookFound)
   327  		err = rh.callbacks.SetUpgradeSeriesStatus(model.UpgradeSeriesPrepareCompleted, message)
   328  	case hooks.PostSeriesUpgrade:
   329  		message := createUpgradeSeriesStatusMessage(rh.name, rh.hookFound)
   330  		err = rh.callbacks.SetUpgradeSeriesStatus(model.UpgradeSeriesCompleted, message)
   331  	case hooks.SecretRotate:
   332  		var info map[string]jujuc.SecretMetadata
   333  		info, err = rh.runner.Context().SecretMetadata()
   334  		if err != nil {
   335  			break
   336  		}
   337  		originalRevision := 0
   338  		uri, _ := secrets.ParseURI(rh.info.SecretURI)
   339  		if m, ok := info[uri.ID]; ok {
   340  			originalRevision = m.LatestRevision
   341  		}
   342  		rh.logger.Debugf("set secret rotated for %q, original rev %v", rh.info.SecretURI, originalRevision)
   343  		err = rh.callbacks.SetSecretRotated(rh.info.SecretURI, originalRevision)
   344  	}
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	newState := change.apply(state)
   350  
   351  	switch rh.info.Kind {
   352  	case hooks.Install:
   353  		newState.Installed = true
   354  		newState.Removed = false
   355  	case hooks.Start:
   356  		newState.Started = true
   357  		newState.Stopped = false
   358  	case hooks.Stop:
   359  		newState.Stopped = true
   360  	case hooks.Remove:
   361  		newState.Removed = true
   362  	}
   363  
   364  	return newState, nil
   365  }
   366  
   367  // RemoteStateChanged is called when the remote state changed during execution
   368  // of the operation.
   369  func (rh *runHook) RemoteStateChanged(snapshot remotestate.Snapshot) {
   370  }