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 }