From 4207146fb6cd5c2f9c431c63949dde6e4a51b7e0 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sat, 18 Dec 2021 09:56:05 +0100 Subject: [PATCH] Refactor calling of hooks on exit --- cmd/backup/main.go | 96 +++++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index ea255fd..77b1a2c 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -17,6 +17,7 @@ import ( "time" "github.com/containrrr/shoutrrr" + "github.com/containrrr/shoutrrr/pkg/router" sTypes "github.com/containrrr/shoutrrr/pkg/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -45,7 +46,7 @@ func main() { defer func() { if pArg := recover(); pArg != nil { if err, ok := pArg.(error); ok { - if hookErr := s.runHooks(err, hookLevelError); hookErr != nil { + if hookErr := s.runHooks(err); hookErr != nil { s.logger.Errorf("An error occurred calling the registered hooks: %s", hookErr) } os.Exit(1) @@ -53,7 +54,7 @@ func main() { panic(pArg) } - if err := s.runHooks(nil, hookLevelInfo); err != nil { + if err := s.runHooks(nil); err != nil { s.logger.Errorf( "Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v", err, @@ -85,10 +86,12 @@ func main() { // script holds all the stateful information required to orchestrate a // single backup run. type script struct { - cli *client.Client - mc *minio.Client - logger *logrus.Logger - hooks []hook + cli *client.Client + mc *minio.Client + logger *logrus.Logger + sender *router.ServiceRouter + hooks []hook + hookLevel hookLevel start time.Time file string @@ -218,11 +221,33 @@ func newScript() (*script, error) { ) } - notificationLevel, ok := notificationLevels[s.c.NotificationLevel] + hookLevel, ok := hookLevels[s.c.NotificationLevel] if !ok { return nil, fmt.Errorf("newScript: unknown NOTIFICATION_LEVEL %s", s.c.NotificationLevel) } - s.registerHook(notificationLevel, s.sendNotification) + s.hookLevel = hookLevel + + if len(s.c.NotificationURLs) > 0 { + sender, senderErr := shoutrrr.CreateSender(s.c.NotificationURLs...) + if senderErr != nil { + return nil, fmt.Errorf("newScript: error creating sender: %w", senderErr) + } + s.sender = sender + // To prevent duplicate notifications, ensure the regsistered callbacks + // run mutually exclusive. + s.registerHook(hookLevelError, func(err error) error { + if err == nil { + return nil + } + return s.notifyFailure(err) + }) + s.registerHook(hookLevelInfo, func(err error) error { + if err != nil { + return nil + } + return s.notifySuccess() + }) + } return s, nil } @@ -234,39 +259,40 @@ func (s *script) registerHook(level hookLevel, action func(err error) error) { s.hooks = append(s.hooks, hook{level, action}) } -// sendNotification sends a notification to third party services, containing -// information about the result of a backup run -func (s *script) sendNotification(err error) error { - if len(s.c.NotificationURLs) == 0 { - return nil +// notifyFailure sends a notification about a failed backup run +func (s *script) notifyFailure(err error) error { + body := fmt.Sprintf( + "Running docker-volume-backup failed with error: %s\n\nLog output of the failed run was:\n\n%s\n", err, s.output.String(), + ) + title := fmt.Sprintf("Failure running docker-volume-backup at %s", s.start.Format(time.RFC3339)) + if err := s.sendNotification(title, body); err != nil { + return fmt.Errorf("notifyFailure: error notifying: %w", err) } + return nil +} - sender, senderErr := shoutrrr.CreateSender(s.c.NotificationURLs...) - if senderErr != nil { - return fmt.Errorf("notifications: error creating sender: %w", senderErr) - } - - var body, title string - if err != nil { - body = fmt.Sprintf( - "Running docker-volume-backup failed with error: %s\n\nLog output of the failed run was:\n\n%s\n", err, s.output.String(), - ) - title = fmt.Sprintf("Failure running docker-volume-backup at %s", s.start.Format(time.RFC3339)) - } else { - body = fmt.Sprintf( - "Running docker-volume-backup succeeded.\n\nLog output was:\n\n%s\n", s.output.String(), - ) - title = fmt.Sprintf("Success running docker-volume-backup at %s", s.start.Format(time.RFC3339)) +// notifyFailure sends a notification about a successful backup run +func (s *script) notifySuccess() error { + title := fmt.Sprintf("Success running docker-volume-backup at %s", s.start.Format(time.RFC3339)) + body := fmt.Sprintf( + "Running docker-volume-backup succeeded.\n\nLog output was:\n\n%s\n", s.output.String(), + ) + if err := s.sendNotification(title, body); err != nil { + return fmt.Errorf("notifySuccess: error notifying: %w", err) } + return nil +} +// sendNotification sends a notification to all configured third party services +func (s *script) sendNotification(title, body string) error { var errs []error - for _, result := range sender.Send(body, &sTypes.Params{"title": title}) { + for _, result := range s.sender.Send(body, &sTypes.Params{"title": title}) { if result != nil { errs = append(errs, result) } } if len(errs) != 0 { - return fmt.Errorf("notifications: error sending message: %w", join(errs...)) + return fmt.Errorf("sendNotification: error sending message: %w", join(errs...)) } return nil } @@ -661,13 +687,13 @@ func (s *script) pruneOldBackups() error { // runHooks runs all hooks that have been registered using the // given levels in the defined ordering. In case executing a hook returns an // error, the following hooks will still be run before the function returns. -func (s *script) runHooks(err error, level hookLevel) error { +func (s *script) runHooks(err error) error { sort.SliceStable(s.hooks, func(i, j int) bool { return s.hooks[i].level < s.hooks[j].level }) var actionErrors []error for _, hook := range s.hooks { - if hook.level > level { + if hook.level > s.hookLevel { continue } if actionErr := hook.action(err); actionErr != nil { @@ -790,11 +816,11 @@ type hookLevel int const ( hookLevelPlumbing hookLevel = iota - hookLevelInfo hookLevelError + hookLevelInfo ) -var notificationLevels = map[string]hookLevel{ +var hookLevels = map[string]hookLevel{ "info": hookLevelInfo, "error": hookLevelError, }