Skip to content

Commit

Permalink
refactor(updater): smart message generation (part 3 of shoutrrr suppo…
Browse files Browse the repository at this point in the history
…rt) (#762)
  • Loading branch information
favonia committed Jun 25, 2024
1 parent dc18a68 commit c09e2b2
Show file tree
Hide file tree
Showing 13 changed files with 870 additions and 300 deletions.
24 changes: 9 additions & 15 deletions cmd/ddns/ddns.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,8 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, setter.Setter

func stopUpdating(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) {
if c.DeleteOnStop {
if ok, msg := updater.DeleteIPs(ctx, ppfmt, c, s); ok {
monitor.LogAll(ctx, ppfmt, msg, c.Monitors)
} else {
monitor.FailureAll(ctx, ppfmt, msg, c.Monitors)
}
resp := updater.DeleteIPs(ctx, ppfmt, c, s)
monitor.SendAll(ctx, ppfmt, c.Monitors, resp)
}
}

Expand Down Expand Up @@ -94,10 +91,10 @@ func realMain() int { //nolint:funlen
// Read the config and get the handler and the setter
c, s, configOk := initConfig(ctx, ppfmt)
// Ping the monitor regardless of whether initConfig succeeded
monitor.StartAll(ctx, ppfmt, formatName(), c.Monitors)
monitor.StartAll(ctx, ppfmt, c.Monitors, formatName())
// Bail out now if initConfig failed
if !configOk {
monitor.ExitStatusAll(ctx, ppfmt, 1, "Config errors", c.Monitors)
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "Config errors")
ppfmt.Infof(pp.EmojiBye, "Bye!")
return 1
}
Expand All @@ -121,13 +118,10 @@ func realMain() int { //nolint:funlen

// Update the IP addresses
if first && !c.UpdateOnStart {
monitor.SuccessAll(ctx, ppfmt, "Started (no action)", c.Monitors)
monitor.SuccessAll(ctx, ppfmt, c.Monitors, "Started (no action)")
} else {
if ok, msg := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s); ok {
monitor.SuccessAll(ctx, ppfmt, msg, c.Monitors)
} else {
monitor.FailureAll(ctx, ppfmt, msg, c.Monitors)
}
resp := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s)
monitor.SendAll(ctx, ppfmt, c.Monitors, resp)
}

// Check if cron was disabled
Expand All @@ -142,7 +136,7 @@ func realMain() int { //nolint:funlen
if next.IsZero() {
ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future")
stopUpdating(ctx, ppfmt, c, s)
monitor.ExitStatusAll(ctx, ppfmt, 1, "No scheduled updates", c.Monitors)
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "No scheduled updates")
ppfmt.Infof(pp.EmojiBye, "Bye!")
return 1
}
Expand All @@ -153,7 +147,7 @@ func realMain() int { //nolint:funlen
// Wait for the next signal or the alarm, whichever comes first
if !sig.SleepUntil(ppfmt, next) {
stopUpdating(ctx, ppfmt, c, s)
monitor.ExitStatusAll(ctx, ppfmt, 0, "Terminated", c.Monitors)
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Terminated")
ppfmt.Infof(pp.EmojiBye, "Bye!")
return 0
}
Expand Down
11 changes: 11 additions & 0 deletions internal/monitor/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package monitor

import (
"context"
"strings"

"github.com/favonia/cloudflare-ddns/internal/pp"
"github.com/favonia/cloudflare-ddns/internal/response"
)

//go:generate mockgen -typed -destination=../mocks/mock_monitor.go -package=mocks . Monitor
Expand Down Expand Up @@ -33,3 +35,12 @@ type Monitor interface {
// ExitStatus records the exit status (as an integer in the POSIX style).
ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool
}

func Send(ctx context.Context, ppfmt pp.PP, m Monitor, r response.Response) bool {
msg := strings.Join(r.MonitorMessages, "\n")
if r.Ok {
return m.Log(ctx, ppfmt, msg)
} else {
return m.Failure(ctx, ppfmt, msg)
}
}
22 changes: 17 additions & 5 deletions internal/monitor/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/favonia/cloudflare-ddns/internal/pp"
"github.com/favonia/cloudflare-ddns/internal/response"
)

// DescribeAll calls [Monitor.Describe] for each monitor in the group with the callback.
Expand All @@ -14,7 +15,7 @@ func DescribeAll(callback func(service, params string), ms []Monitor) {
}

// SuccessAll calls [Monitor.Success] for each monitor in the group.
func SuccessAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool {
func SuccessAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
ok := true
for _, m := range ms {
if !m.Success(ctx, ppfmt, message) {
Expand All @@ -25,7 +26,7 @@ func SuccessAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor)
}

// StartAll calls [Monitor.Start] for each monitor in ms.
func StartAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool {
func StartAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
ok := true
for _, m := range ms {
if !m.Start(ctx, ppfmt, message) {
Expand All @@ -36,7 +37,7 @@ func StartAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bo
}

// FailureAll calls [Monitor.Failure] for each monitor in ms.
func FailureAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool {
func FailureAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
ok := true
for _, m := range ms {
if !m.Failure(ctx, ppfmt, message) {
Expand All @@ -47,7 +48,7 @@ func FailureAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor)
}

// LogAll calls [Monitor.Log] for each monitor in ms.
func LogAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool {
func LogAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool {
ok := true
for _, m := range ms {
if !m.Log(ctx, ppfmt, message) {
Expand All @@ -58,7 +59,7 @@ func LogAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool
}

// ExitStatusAll calls [Monitor.ExitStatus] for each monitor in ms.
func ExitStatusAll(ctx context.Context, ppfmt pp.PP, code int, message string, ms []Monitor) bool {
func ExitStatusAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, code int, message string) bool {
ok := true
for _, m := range ms {
if !m.ExitStatus(ctx, ppfmt, code, message) {
Expand All @@ -67,3 +68,14 @@ func ExitStatusAll(ctx context.Context, ppfmt pp.PP, code int, message string, m
}
return ok
}

// SendAll calls [Send] for each monitor in ms.
func SendAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, resp response.Response) bool {
ok := true
for _, m := range ms {
if !Send(ctx, ppfmt, m, resp) {
ok = false
}
}
return ok
}
54 changes: 48 additions & 6 deletions internal/monitor/composite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package monitor_test

import (
"context"
"strings"
"testing"

"go.uber.org/mock/gomock"

"github.com/favonia/cloudflare-ddns/internal/mocks"
"github.com/favonia/cloudflare-ddns/internal/monitor"
"github.com/favonia/cloudflare-ddns/internal/response"
)

func TestDescribeAll(t *testing.T) {
Expand Down Expand Up @@ -43,7 +45,7 @@ func TestSuccessAll(t *testing.T) {
ms = append(ms, m)
}

monitor.SuccessAll(context.Background(), mockPP, message, ms)
monitor.SuccessAll(context.Background(), mockPP, ms, message)
}

func TestStartAll(t *testing.T) {
Expand All @@ -62,7 +64,7 @@ func TestStartAll(t *testing.T) {
ms = append(ms, m)
}

monitor.StartAll(context.Background(), mockPP, message, ms)
monitor.StartAll(context.Background(), mockPP, ms, message)
}

func TestFailureAll(t *testing.T) {
Expand All @@ -81,7 +83,7 @@ func TestFailureAll(t *testing.T) {
ms = append(ms, m)
}

monitor.FailureAll(context.Background(), mockPP, message, ms)
monitor.FailureAll(context.Background(), mockPP, ms, message)
}

func TestLogAll(t *testing.T) {
Expand All @@ -100,10 +102,10 @@ func TestLogAll(t *testing.T) {
ms = append(ms, m)
}

monitor.LogAll(context.Background(), mockPP, message, ms)
monitor.LogAll(context.Background(), mockPP, ms, message)
}

func TestMonitorsExitStatus(t *testing.T) {
func TestExitStatusAll(t *testing.T) {
t.Parallel()

ms := make([]monitor.Monitor, 0, 5)
Expand All @@ -119,5 +121,45 @@ func TestMonitorsExitStatus(t *testing.T) {
ms = append(ms, m)
}

monitor.ExitStatusAll(context.Background(), mockPP, 42, message, ms)
monitor.ExitStatusAll(context.Background(), mockPP, ms, 42, message)
}

func TestSendAll(t *testing.T) {
t.Parallel()

monitorMessages := []string{"forest", "grass"}
monitorMessage := strings.Join(monitorMessages, "\n")
notifierMessages := []string{"ocean"}

for name, tc := range map[string]struct {
ok bool
}{
"ok": {true},
"notok": {false},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()

ms := make([]monitor.Monitor, 0, 5)
mockCtrl := gomock.NewController(t)
mockPP := mocks.NewMockPP(mockCtrl)

for range 5 {
m := mocks.NewMockMonitor(mockCtrl)
if tc.ok {
m.EXPECT().Log(context.Background(), mockPP, monitorMessage)
} else {
m.EXPECT().Failure(context.Background(), mockPP, monitorMessage)
}
ms = append(ms, m)
}

resp := response.Response{
Ok: tc.ok,
MonitorMessages: monitorMessages,
NotifierMessages: notifierMessages,
}
monitor.SendAll(context.Background(), mockPP, ms, resp)
})
}
}
35 changes: 35 additions & 0 deletions internal/response/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package response

type Response struct {
Ok bool
MonitorMessages []string
NotifierMessages []string
}

func NewEmpty() Response {
return Response{
Ok: true,
MonitorMessages: nil,
NotifierMessages: nil,
}
}

func Merge(rs ...Response) Response {
var (
allOk = true
allMonitorMessages = map[bool][]string{true: {}, false: {}}
allNotifierMessages = []string{}
)

for _, r := range rs {
allOk = allOk && r.Ok
allMonitorMessages[r.Ok] = append(allMonitorMessages[r.Ok], r.MonitorMessages...)
allNotifierMessages = append(allNotifierMessages, r.NotifierMessages...)
}

return Response{
Ok: allOk,
MonitorMessages: allMonitorMessages[allOk],
NotifierMessages: allNotifierMessages,
}
}
12 changes: 0 additions & 12 deletions internal/setter/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,6 @@ import (

//go:generate mockgen -typed -destination=../mocks/mock_setter.go -package=mocks . Setter

// ResponseCode encodes the minimum information to generate messages for monitors and notifiers.
type ResponseCode int

const (
// No updates were needed. The records were already okay.
ResponseNoUpdatesNeeded = iota
// Updates were needed and they are done.
ResponseUpdatesApplied
// Updates were needed and they did not fully complete. The records may be inconsistent.
ResponseUpdatesFailed
)

// Setter uses [api.Handle] to update DNS records.
type Setter interface {
// Set sets a particular domain to the given IP address.
Expand Down
20 changes: 20 additions & 0 deletions internal/setter/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package setter

// ResponseCode encodes the minimum information to generate messages for monitors and notifiers.
type ResponseCode int

const (
// ResponseNoop means no changes were needed.
// The records were already updated or already deleted.
ResponseNoop ResponseCode = iota

// ResponseUpdated means records should be updated
// and we updated them, or that they should be deleted
// and we deleted them.
ResponseUpdated

// ResponseFailed means records should be updated
// but we failed to finish the updating, or that they
// should be deleted and we failed to finish the deletion.
ResponseFailed
)
20 changes: 10 additions & 10 deletions internal/setter/setter.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP,
rs, ok := s.Handle.ListRecords(ctx, ppfmt, domain, ipnet)
if !ok {
ppfmt.Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", recordType, domainDescription)
return ResponseUpdatesFailed
return ResponseFailed
}

// The intention of these two lists is to find or create a good record and then delete everything else.
Expand All @@ -77,7 +77,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP,
// If it's up to date and there are no other records, we are done!
if foundMatched && len(unprocessedMatched) == 0 && len(unprocessedUnmatched) == 0 {
ppfmt.Infof(pp.EmojiAlreadyDone, "The %s records of %q are already up to date", recordType, domainDescription)
return ResponseNoUpdatesNeeded
return ResponseNoop
}

// This counts the stale records that have not being deleted yet.
Expand Down Expand Up @@ -164,12 +164,12 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP,
// Check whether we are done. It is okay to have duplicates, but it is not okay to have remaining stale records.
if !foundMatched || numUndeletedUnmatched > 0 {
ppfmt.Errorf(pp.EmojiError,
"Failed to complete updating of %s records of %q; records might be inconsistent",
"Failed to finish updating %s records of %q; records might be inconsistent",
recordType, domainDescription)
return ResponseUpdatesFailed
return ResponseFailed
}

return ResponseUpdatesApplied
return ResponseUpdated
}

// Delete deletes all managed DNS records.
Expand All @@ -180,7 +180,7 @@ func (s *setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain,
rmap, ok := s.Handle.ListRecords(ctx, ppfmt, domain, ipnet)
if !ok {
ppfmt.Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", recordType, domainDescription)
return ResponseUpdatesFailed
return ResponseFailed
}

// Sorting is not needed for correctness, but it will make the function deterministic.
Expand All @@ -192,7 +192,7 @@ func (s *setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain,

if len(unmatchedIDs) == 0 {
ppfmt.Infof(pp.EmojiAlreadyDone, "The %s records of %q were already deleted", recordType, domainDescription)
return ResponseNoUpdatesNeeded
return ResponseNoop
}

allOk := true
Expand All @@ -206,10 +206,10 @@ func (s *setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain,
}
if !allOk {
ppfmt.Errorf(pp.EmojiError,
"Failed to complete deleting of %s records of %q; records might be inconsistent",
"Failed to finish deleting %s records of %q; records might be inconsistent",
recordType, domainDescription)
return ResponseUpdatesFailed
return ResponseFailed
}

return ResponseUpdatesApplied
return ResponseUpdated
}
Loading

0 comments on commit c09e2b2

Please sign in to comment.