Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notifier): send more messages (part 5 of shoutrrr support) #772

Merged
merged 5 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions cmd/ddns/ddns.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
func stopUpdating(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) {
if c.DeleteOnStop {
resp := updater.DeleteIPs(ctx, ppfmt, c, s)
monitor.SendResponseAll(ctx, ppfmt, c.Monitors, resp, false)
notifier.SendResponseAll(ctx, ppfmt, c.Notifiers, resp)
monitor.LogMessageAll(ctx, ppfmt, c.Monitors, resp)
notifier.SendMessageAll(ctx, ppfmt, c.Notifiers, resp)

Check warning on line 61 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L60-L61

Added lines #L60 - L61 were not covered by tests
}
}

Expand Down Expand Up @@ -92,23 +92,29 @@

// Read the config and get the handler and the setter
c, s, configOk := initConfig(ctx, ppfmt)
// Ping the monitor regardless of whether initConfig succeeded
// Ping monitors regardless of whether initConfig succeeded
monitor.StartAll(ctx, ppfmt, c.Monitors, formatName())
// Bail out now if initConfig failed
if !configOk {
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "Config errors")
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "Configuration errors")
notifier.SendAll(ctx, ppfmt, c.Notifiers,
"Cloudflare DDNS was misconfigured. Please check the logging for more details.")

Check warning on line 101 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L99-L101

Added lines #L99 - L101 were not covered by tests
ppfmt.Infof(pp.EmojiBye, "Bye!")
return 1
}
// If UPDATE_CRON is not `@once` (not single-run mode), then send a notification to signal the start.
if c.UpdateCron != nil {
notifier.SendAll(ctx, ppfmt, c.Notifiers, "Started running Cloudflare DDNS.")

Check warning on line 107 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L106-L107

Added lines #L106 - L107 were not covered by tests
}

// Without the following line, the quiet mode can be too quiet, and some system (Portainer)
// is not happy with completely empty log. As a workaround, we will print a Notice here.
// See GitHub issue #426.
//
// We still want to keep the quiet mode extremely quiet for the single-run mode (UPDATE_CRON=@once),
// hence we are checking whether cron is enabled or not. (The single-run mode is defined as
// having the internal cron disabled.)
if c.UpdateCron != nil && !ppfmt.IsEnabledFor(pp.Verbose) {
// Without the following line, the quiet mode can be too quiet, and some system (Portainer)
// is not happy with completely empty log. As a workaround, we will print a Notice here.
// See GitHub issue #426.
//
// We still want to keep the quiet mode extremely quiet for the single-run mode (UPDATE_CRON=@once),
// hence we are checking whether cron is enabled or not. (The single-run mode is defined as
// having the internal cron disabled.)
ppfmt.Noticef(pp.EmojiMute, "Quiet mode enabled")
}

Expand All @@ -123,8 +129,8 @@
monitor.SuccessAll(ctx, ppfmt, c.Monitors, "Started (no action)")
} else {
resp := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s)
monitor.SendResponseAll(ctx, ppfmt, c.Monitors, resp, true)
notifier.SendResponseAll(ctx, ppfmt, c.Notifiers, resp)
monitor.PingMessageAll(ctx, ppfmt, c.Monitors, resp)
notifier.SendMessageAll(ctx, ppfmt, c.Notifiers, resp)

Check warning on line 133 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L132-L133

Added lines #L132 - L133 were not covered by tests
}

// Check if cron was disabled
Expand All @@ -137,9 +143,19 @@

// If there's nothing scheduled in near future
if next.IsZero() {
ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future")
ppfmt.Errorf(pp.EmojiUserError,
"No scheduled updates in near future; consider changing UPDATE_CRON=%s",
cron.DescribeSchedule(c.UpdateCron),
)

Check warning on line 149 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L146-L149

Added lines #L146 - L149 were not covered by tests
stopUpdating(ctx, ppfmt, c, s)
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "No scheduled updates")
notifier.SendAll(ctx, ppfmt, c.Notifiers,
fmt.Sprintf(
"Cloudflare DDNS stopped because there are no scheduled updates in near future. "+
"Consider changing the value of UPDATE_CRON (%s).",
cron.DescribeSchedule(c.UpdateCron),
),
)

Check warning on line 158 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L152-L158

Added lines #L152 - L158 were not covered by tests
ppfmt.Infof(pp.EmojiBye, "Bye!")
return 1
}
Expand All @@ -150,7 +166,10 @@
// 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, c.Monitors, 0, "Terminated")
monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Stopped")
if c.UpdateCron != nil {
notifier.SendAll(ctx, ppfmt, c.Notifiers, "Cloudflare DDNS stopped.")

Check warning on line 171 in cmd/ddns/ddns.go

View check run for this annotation

Codecov / codecov/patch

cmd/ddns/ddns.go#L169-L171

Added lines #L169 - L171 were not covered by tests
}
ppfmt.Infof(pp.EmojiBye, "Bye!")
return 0
}
Expand Down
20 changes: 10 additions & 10 deletions internal/response/response.go → internal/message/message.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
package response
package message

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

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

func Merge(rs ...Response) Response {
func Merge(msgs ...Message) Message {
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...)
for _, msg := range msgs {
allOk = allOk && msg.Ok
allMonitorMessages[msg.Ok] = append(allMonitorMessages[msg.Ok], msg.MonitorMessages...)
allNotifierMessages = append(allNotifierMessages, msg.NotifierMessages...)
}

return Response{
return Message{
Ok: allOk,
MonitorMessages: allMonitorMessages[allOk],
NotifierMessages: allNotifierMessages,
Expand Down
25 changes: 16 additions & 9 deletions internal/monitor/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"context"
"strings"

"github.com/favonia/cloudflare-ddns/internal/message"
"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 @@ -36,15 +36,22 @@ type Monitor interface {
ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool
}

func SendResponse(ctx context.Context, ppfmt pp.PP, m Monitor, r response.Response, ping bool) bool {
msg := strings.Join(r.MonitorMessages, "\n")
func PingMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.Message) bool {
monitorMsg := strings.Join(msg.MonitorMessages, "\n")
if msg.Ok {
return m.Success(ctx, ppfmt, monitorMsg)
} else {
return m.Failure(ctx, ppfmt, monitorMsg)
}
}

func LogMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.Message) bool {
monitorMsg := strings.Join(msg.MonitorMessages, "\n")
switch {
case !r.Ok:
return m.Failure(ctx, ppfmt, msg)
case ping:
return m.Success(ctx, ppfmt, msg)
case len(r.MonitorMessages) > 0:
return m.Log(ctx, ppfmt, msg)
case !msg.Ok:
return m.Failure(ctx, ppfmt, monitorMsg)
case len(msg.MonitorMessages) > 0:
return m.Log(ctx, ppfmt, monitorMsg)
default:
return true
}
Expand Down
19 changes: 15 additions & 4 deletions internal/monitor/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package monitor
import (
"context"

"github.com/favonia/cloudflare-ddns/internal/message"
"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 Down Expand Up @@ -69,11 +69,22 @@ func ExitStatusAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, code int, mes
return ok
}

// SendResponseAll calls [SendResponse] for each monitor in ms.
func SendResponseAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, resp response.Response, ping bool) bool {
// PingMessageAll calls [SendMessage] for each monitor in ms.
func PingMessageAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, msg message.Message) bool {
ok := true
for _, m := range ms {
if !SendResponse(ctx, ppfmt, m, resp, ping) {
if !PingMessage(ctx, ppfmt, m, msg) {
ok = false
}
}
return ok
}

// LogMessageAll calls [SendMessage] for each monitor in ms.
func LogMessageAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, msg message.Message) bool {
ok := true
for _, m := range ms {
if !LogMessage(ctx, ppfmt, m, msg) {
ok = false
}
}
Expand Down
68 changes: 56 additions & 12 deletions internal/monitor/composite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (

"go.uber.org/mock/gomock"

"github.com/favonia/cloudflare-ddns/internal/message"
"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 @@ -125,7 +125,7 @@ func TestExitStatusAll(t *testing.T) {
monitor.ExitStatusAll(context.Background(), mockPP, ms, 42, message)
}

func TestSendResponseAll(t *testing.T) {
func TestPingMessageAll(t *testing.T) {
t.Parallel()

notifierMessages := []string{"ocean", "moon"}
Expand All @@ -141,13 +141,10 @@ func TestSendResponseAll(t *testing.T) {
monitorMessage := strings.Join(tc1.monitorMessages, "\n")

for name2, tc2 := range map[string]struct {
ok bool
ping bool
ok bool
}{
"success": {true, true},
"log": {true, false},
"fail1": {false, true},
"fail2": {false, false},
"ok": {true},
"notok": {false},
} {
t.Run(fmt.Sprintf("%s/%s", name1, name2), func(t *testing.T) {
t.Parallel()
Expand All @@ -158,9 +155,56 @@ func TestSendResponseAll(t *testing.T) {

for range 5 {
m := mocks.NewMockMonitor(mockCtrl)
switch {
case tc2.ok && tc2.ping:
if tc2.ok {
m.EXPECT().Success(context.Background(), mockPP, monitorMessage)
} else {
m.EXPECT().Failure(context.Background(), mockPP, monitorMessage)
}
ms = append(ms, m)
}

msg := message.Message{
Ok: tc2.ok,
MonitorMessages: tc1.monitorMessages,
NotifierMessages: notifierMessages,
}
monitor.PingMessageAll(context.Background(), mockPP, ms, msg)
})
}
}
}

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

notifierMessages := []string{"ocean", "moon"}

for name1, tc1 := range map[string]struct {
monitorMessages []string
}{
"nil": {nil},
"empty": {[]string{}},
"one": {[]string{"hi"}},
"two": {[]string{"hi", "hey"}},
} {
monitorMessage := strings.Join(tc1.monitorMessages, "\n")

for name2, tc2 := range map[string]struct {
ok bool
}{
"ok": {true},
"notok": {false},
} {
t.Run(fmt.Sprintf("%s/%s", name1, name2), 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)
switch {
case tc2.ok && len(monitorMessage) > 0:
m.EXPECT().Log(context.Background(), mockPP, monitorMessage)
case tc2.ok:
Expand All @@ -170,12 +214,12 @@ func TestSendResponseAll(t *testing.T) {
ms = append(ms, m)
}

resp := response.Response{
msg := message.Message{
Ok: tc2.ok,
MonitorMessages: tc1.monitorMessages,
NotifierMessages: notifierMessages,
}
monitor.SendResponseAll(context.Background(), mockPP, ms, resp, tc2.ping)
monitor.LogMessageAll(context.Background(), mockPP, ms, msg)
})
}
}
Expand Down
8 changes: 4 additions & 4 deletions internal/notifier/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"context"
"strings"

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

//go:generate mockgen -typed -destination=../mocks/mock_notifier.go -package=mocks . Notifier
Expand All @@ -20,9 +20,9 @@ type Notifier interface {
Send(ctx context.Context, ppfmt pp.PP, msg string) bool
}

func SendResponse(ctx context.Context, ppfmt pp.PP, n Notifier, r response.Response) bool {
if len(r.NotifierMessages) == 0 {
func SendMessage(ctx context.Context, ppfmt pp.PP, n Notifier, msg message.Message) bool {
if len(msg.NotifierMessages) == 0 {
return true
}
return n.Send(ctx, ppfmt, strings.Join(r.NotifierMessages, " "))
return n.Send(ctx, ppfmt, strings.Join(msg.NotifierMessages, " "))
}
8 changes: 4 additions & 4 deletions internal/notifier/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package notifier
import (
"context"

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

// DescribeAll calls [Notifier.Describe] for each monitor in the group with the callback.
Expand All @@ -25,11 +25,11 @@ func SendAll(ctx context.Context, ppfmt pp.PP, ns []Notifier, message string) bo
return ok
}

// SendResponseAll calls [SendResponse] for each monitor in the group.
func SendResponseAll(ctx context.Context, ppfmt pp.PP, ns []Notifier, r response.Response) bool {
// SendMessageAll calls [SendMessage] for each monitor in the group.
func SendMessageAll(ctx context.Context, ppfmt pp.PP, ns []Notifier, msg message.Message) bool {
ok := true
for _, n := range ns {
if !SendResponse(ctx, ppfmt, n, r) {
if !SendMessage(ctx, ppfmt, n, msg) {
ok = false
}
}
Expand Down
Loading
Loading