Skip to content

Commit de2aa0b

Browse files
committed
Refactor DDNS API to use channels
1 parent ebc80f4 commit de2aa0b

File tree

4 files changed

+260
-69
lines changed

4 files changed

+260
-69
lines changed

cmd/root.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"os"
66
"path/filepath"
77
"strings"
8+
"time"
89

910
"github.com/juju/errors"
1011
"github.com/mattolenik/cloudflare-ddns-client/conf"
1112
"github.com/mattolenik/cloudflare-ddns-client/ddns"
1213
"github.com/mattolenik/cloudflare-ddns-client/errhandler"
1314
"github.com/mattolenik/cloudflare-ddns-client/meta"
1415
"github.com/mattolenik/cloudflare-ddns-client/providers"
16+
"github.com/mattolenik/cloudflare-ddns-client/task"
1517
"github.com/rs/zerolog"
1618
"github.com/rs/zerolog/log"
1719
"github.com/spf13/cobra"
@@ -35,15 +37,38 @@ For example:
3537
if err != nil {
3638
return errors.Annotatef(err, "failed to configure DDNS provider")
3739
}
38-
daemon := ddns.NewDDNSDaemon(provider, ddns.NewDefaultIPProvider(), ddns.NewDefaultConfigProvider())
40+
daemon := ddns.NewDefaultDaemon(provider, ddns.NewDefaultIPProvider(), ddns.NewDefaultConfigProvider())
3941
if conf.Daemon.Get() {
40-
return errors.Trace(daemon.StartWithDefaults())
42+
return errors.Trace(runDaemon(provider, daemon))
4143
}
4244
return errors.Trace(daemon.Update())
4345
},
4446
Version: meta.Version,
4547
}
4648

49+
func runDaemon(provider ddns.DDNSProvider, daemon *ddns.DDNSDaemon) error {
50+
statusChan := daemon.StartWithDefaults()
51+
for {
52+
select {
53+
case status := <-statusChan:
54+
switch status.Type {
55+
case task.Info:
56+
log.Info().Msg(status.Message)
57+
case task.Error:
58+
log.Error().Msg(status.Message)
59+
case task.Fatal:
60+
log.Error().Msg("FATAL: " + status.Message)
61+
return status.Error
62+
}
63+
if status.IsDone {
64+
return nil
65+
}
66+
default:
67+
time.Sleep(1 * time.Second)
68+
}
69+
}
70+
}
71+
4772
func init() {
4873
if path, err := os.Executable(); err == nil {
4974
meta.ProgramDir = filepath.Dir(path)

ddns/ddns.go

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/juju/errors"
99
"github.com/mattolenik/cloudflare-ddns-client/conf"
1010
"github.com/mattolenik/cloudflare-ddns-client/ip"
11-
"github.com/rs/zerolog/log"
11+
"github.com/mattolenik/cloudflare-ddns-client/task"
1212
)
1313

1414
type DDNSProvider interface {
@@ -49,9 +49,9 @@ func NewDefaultConfigProvider() *DefaultConfigProvider {
4949
}
5050

5151
type Daemon interface {
52-
Update(provider DDNSProvider) error
53-
Start(provider DDNSProvider, updatePeriod, failureRetryDelay time.Duration) error
54-
Stop() error
52+
Update() error
53+
Start(updatePeriod, retryDelay time.Duration) chan task.Status
54+
Stop()
5555
}
5656

5757
type DDNSDaemon struct {
@@ -62,8 +62,8 @@ type DDNSDaemon struct {
6262
configProvider ConfigProvider
6363
}
6464

65-
// NewDDNSDaemon creates a new DDNSDaemon
66-
func NewDDNSDaemon(ddnsProvider DDNSProvider, ipProvider IPProvider, configProvider ConfigProvider) *DDNSDaemon {
65+
// NewDefaultDaemon creates a new DDNSDaemon
66+
func NewDefaultDaemon(ddnsProvider DDNSProvider, ipProvider IPProvider, configProvider ConfigProvider) *DDNSDaemon {
6767
if ddnsProvider == nil {
6868
panic("ddnsProvider must not be nil")
6969
}
@@ -87,7 +87,6 @@ func (d *DDNSDaemon) Update() error {
8787
if err != nil {
8888
return errors.Annotate(err, "unable to retrieve public IP")
8989
}
90-
log.Info().Msgf("Found public IP '%s'", ip)
9190
domain, record, err := d.configProvider.Get()
9291
if err != nil {
9392
return errors.Annotate(err, "unable to find domain or record in configuration")
@@ -99,70 +98,82 @@ func (d *DDNSDaemon) Update() error {
9998
// Start continually keeps DDNS up to date.
10099
// updatePeriod - how often to check for updates
101100
// failureRetryDelay - how long to wait until retry after a failure
102-
func (d *DDNSDaemon) Start(updatePeriod, failureRetryDelay time.Duration) error {
101+
func (d *DDNSDaemon) Start(updatePeriod, retryDelay time.Duration) (status chan task.Status) {
103102
var lastIP string
104103
var lastIPUpdate time.Time
105104

106-
log.Info().Msgf("Daemon running, will now monitor for IP updates every %d seconds", int(updatePeriod.Seconds()))
107-
108-
for d.shouldRun {
109-
domain, record, err := d.configProvider.Get()
110-
if err != nil {
111-
return errors.Annotate(err, "unable to find domain or record in configuration")
112-
}
113-
dnsRecordIP, err := d.ddnsProvider.Get(domain, record)
114-
if err != nil {
115-
log.Error().Msgf("Unable to look up current DNS record, will retry in %d seconds. Error was:\n%v", int(updatePeriod.Seconds()), err)
116-
time.Sleep(failureRetryDelay)
117-
continue
118-
}
119-
newIP, err := d.ipProvider.Get()
120-
if err != nil {
121-
log.Error().Msgf("Unable to retrieve public IP, will retry in %d seconds. Error was:\n%v", int(updatePeriod.Seconds()), err)
122-
time.Sleep(failureRetryDelay)
123-
continue
124-
}
125-
if newIP == lastIP && newIP == dnsRecordIP {
126-
log.Info().Msgf(
127-
"No IP change detected since %s (%d seconds ago)",
128-
lastIPUpdate.Format(time.RFC1123Z),
129-
int(time.Since(lastIPUpdate).Seconds()))
105+
status <- task.InfoStatusf("Daemon running, will now monitor for IP updates every %d seconds", int(updatePeriod.Seconds()))
106+
107+
func() {
108+
defer close(status)
109+
for d.shouldRun {
110+
domain, record, err := d.configProvider.Get()
111+
if err != nil {
112+
status <- task.FatalStatusWrap(err, "unable to find domain or record in configuration")
113+
return
114+
}
115+
dnsRecordIP, err := d.ddnsProvider.Get(domain, record)
116+
if err != nil {
117+
status <- task.ErrorStatusf("Unable to look up current DNS record, will retry in %d seconds. Error was:\n%v", int(updatePeriod.Seconds()), err)
118+
time.Sleep(retryDelay)
119+
continue
120+
}
121+
newIP, err := d.ipProvider.Get()
122+
if err != nil {
123+
status <- task.ErrorStatusf("Unable to retrieve public IP, will retry in %d seconds. Error was:\n%v", int(updatePeriod.Seconds()), err)
124+
time.Sleep(retryDelay)
125+
continue
126+
}
127+
128+
// Nothing has changed, log and move on
129+
if newIP == lastIP && newIP == dnsRecordIP {
130+
status <- task.InfoStatusf(
131+
"No IP change detected since %s (%d seconds ago)",
132+
lastIPUpdate.Format(time.RFC1123Z),
133+
int(time.Since(lastIPUpdate).Seconds()))
134+
time.Sleep(updatePeriod)
135+
continue
136+
}
137+
138+
// IP has changed, log depending on how it has changed
139+
if lastIP == "" {
140+
// Log line for first time
141+
status <- task.InfoStatusf("Found public IP '%s'", newIP)
142+
} else if newIP != lastIP {
143+
// Log line for IP change
144+
status <- task.InfoStatusf("Detected new public IP address, it changed from '%s' to '%s'", lastIP, newIP)
145+
} else if dnsRecordIP != newIP {
146+
// Log line for no new IP, but mismatch with DNS record
147+
status <- task.InfoStatusf("Public IP address did not change, but DNS record did match, is '%s' but expected '%s', correcting", dnsRecordIP, newIP)
148+
}
149+
150+
lastIP = newIP
151+
lastIPUpdate = time.Now()
152+
153+
// Reach out to the actual DDNS provider and make the update
154+
err = d.ddnsProvider.Update(domain, record, lastIP)
155+
if err != nil {
156+
status <- task.ErrorStatusf("Unable to update DNS, will retry in %d seconds. Erorr was:\n%v", updatePeriod/time.Second, err)
157+
time.Sleep(retryDelay)
158+
continue
159+
}
160+
// Do another run check before the sleep occurs so as to not draw out the stop operation
161+
if !d.shouldRun {
162+
break
163+
}
130164
time.Sleep(updatePeriod)
131-
continue
132165
}
133-
if lastIP == "" {
134-
// Log line for first time
135-
log.Info().Msgf("Found public IP '%s'", newIP)
136-
} else if newIP != lastIP {
137-
// Log line for IP change
138-
log.Info().Msgf("Detected new public IP address, it changed from '%s' to '%s'", lastIP, newIP)
139-
} else if dnsRecordIP != newIP {
140-
// Log line for no new IP, but mismatch with DNS record
141-
log.Info().Msgf("Public IP address did not change, but DNS record did match, is '%s' but expected '%s', correcting", dnsRecordIP, newIP)
142-
}
143-
lastIP = newIP
144-
lastIPUpdate = time.Now()
145-
146-
err = d.ddnsProvider.Update(domain, record, lastIP)
147-
if err != nil {
148-
log.Error().Msgf("Unable to update DNS, will retry in %d seconds. Erorr was:\n%v", updatePeriod/time.Second, err)
149-
time.Sleep(failureRetryDelay)
150-
continue
151-
}
152-
if !d.shouldRun {
153-
break
154-
}
155-
time.Sleep(updatePeriod)
156-
}
157-
return nil
166+
}()
167+
return status
158168
}
159169

160170
// StartWithDefaults calls Start but with default values
161-
func (d *DDNSDaemon) StartWithDefaults() error {
171+
func (d *DDNSDaemon) StartWithDefaults() chan task.Status {
162172
t := 10 * time.Second
163-
return errors.Trace(d.Start(t, t))
173+
return d.Start(t, t)
164174
}
165175

176+
// Stop instructs the daemon to stop as soon as the current (if any) operation is finished
166177
func (d *DDNSDaemon) Stop() {
167178
d.shouldRun = true
168179
}

ddns/ddns_test.go

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,90 @@
11
package ddns
22

33
import (
4+
"fmt"
45
"testing"
6+
"time"
57

68
"github.com/golang/mock/gomock"
79
"github.com/mattolenik/cloudflare-ddns-client/test"
810
)
911

10-
func TestDDNSDaemon(t *testing.T) {
12+
func TestUpdate(t *testing.T) {
1113
assert, _, ctrl, cleanup := test.NewTools(t)
1214
defer cleanup()
1315

1416
domain := "abc.com"
1517
record := "xyz.abc.com"
16-
expectedIP := "1.1.1.1"
18+
ip := "1.1.1.1"
1719

1820
ddnsProvider := NewMockDDNSProvider(ctrl)
1921
ipProvider := NewMockIPProvider(ctrl)
2022
configProvider := NewMockConfigProvider(ctrl)
21-
ddnsDaemon := NewDDNSDaemon(ddnsProvider, ipProvider, configProvider)
23+
ddnsDaemon := NewDefaultDaemon(ddnsProvider, ipProvider, configProvider)
2224

2325
configProvider.EXPECT().Get().Return(domain, record, nil)
24-
ipProvider.EXPECT().Get().Return(expectedIP, nil)
25-
ddnsProvider.EXPECT().Update(gomock.Eq(domain), gomock.Eq(record), gomock.Eq(expectedIP)).Return(nil).Times(1)
26-
ddnsProvider.EXPECT().Get(domain, record).Return(expectedIP, nil).Times(1)
26+
ipProvider.EXPECT().Get().Return(ip, nil)
27+
ddnsProvider.EXPECT().Update(gomock.Eq(domain), gomock.Eq(record), gomock.Eq(ip)).Return(nil).Times(1)
28+
ddnsProvider.EXPECT().Get(domain, record).Return(ip, nil).Times(1)
2729
assert.NoError(ddnsDaemon.Update())
2830

2931
actualIP, err := ddnsProvider.Get(domain, record)
3032
assert.NoError(err)
31-
assert.Equal(expectedIP, actualIP)
33+
assert.Equal(ip, actualIP)
34+
}
35+
36+
func TestDaemon(t *testing.T) {
37+
assert, _, ctrl, cleanup := test.NewTools(t)
38+
defer cleanup()
39+
40+
domain := "abc.com"
41+
record := "xyz.abc.com"
42+
currentSuffix := 0
43+
currentIP := ""
44+
ipGen := func() (string, error) {
45+
currentSuffix++
46+
currentIP = fmt.Sprintf("1.1.1.%d", currentSuffix)
47+
return currentIP, nil
48+
}
49+
getCurrentIP := func() interface{} { return currentIP }
50+
51+
ddnsProvider := NewMockDDNSProvider(ctrl)
52+
ipProvider := NewMockIPProvider(ctrl)
53+
configProvider := NewMockConfigProvider(ctrl)
54+
ddnsDaemon := NewDefaultDaemon(ddnsProvider, ipProvider, configProvider)
55+
updatePeriod := 50 * time.Millisecond
56+
retryDelay := 50 * time.Millisecond
57+
ddnsDaemon.Start(updatePeriod, retryDelay)
58+
59+
configProvider.EXPECT().Get().Return(domain, record, nil)
60+
ipProvider.EXPECT().Get().DoAndReturn(ipGen)
61+
//ddnsProvider.EXPECT().Update(gomock.Eq(domain), gomock.Eq(record), gomock.Eq(ip)).Return(nil).Times(1)
62+
//ddnsProvider.EXPECT().Get(domain, record).Return(ip, nil).Times(1)
63+
ddnsProvider.EXPECT().
64+
Update(
65+
gomock.Eq(domain),
66+
gomock.Eq(record),
67+
FnMatch(gomock.Eq, getCurrentIP),
68+
).AnyTimes()
69+
}
70+
71+
type funcMatcher struct {
72+
gomock.Matcher
73+
value func() interface{}
74+
matchFn func(interface{}) gomock.Matcher
75+
}
76+
77+
func FnMatch(matchFn func(interface{}) gomock.Matcher, value func() interface{}) gomock.Matcher {
78+
return &funcMatcher{
79+
matchFn: matchFn,
80+
value: value,
81+
}
82+
}
83+
84+
func (f *funcMatcher) Matches(x interface{}) bool {
85+
return f.matchFn(x).Matches(x)
86+
}
87+
88+
func (f *funcMatcher) String() string {
89+
return "runs underlying match against new value each time"
3290
}

0 commit comments

Comments
 (0)