Skip to content

Commit d1f3fc3

Browse files
committed
retry on CNAME conflicts
1 parent 65e431c commit d1f3fc3

File tree

2 files changed

+93
-12
lines changed

2 files changed

+93
-12
lines changed

main.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"os"
1010
"os/signal"
11+
"strings"
1112
"syscall"
1213
"time"
1314

@@ -29,14 +30,19 @@ var (
2930

3031
register, unRegister bool
3132

32-
r53 *route53.Client
33+
r53 Route53Client
3334
)
3435

36+
type Route53Client interface {
37+
ChangeResourceRecordSets(ctx context.Context, params *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error)
38+
GetChange(ctx context.Context, params *route53.GetChangeInput, optFns ...func(*route53.Options)) (*route53.GetChangeOutput, error)
39+
}
40+
3541
func configureFromFlags(ctx context.Context) {
3642
flag.StringVar(&dns, "dns", "my.example.com", "DNS name to register in Route53")
3743
flag.StringVar(&hostedZone, "hostedzone", "Z2AAAABCDEFGT4", "Hosted zone ID in route53")
3844
flag.IntVar(&dnsTTL, "dnsttl", 10, "Timeout for DNS entry")
39-
flag.StringVar(&ipAddress, "ipaddress", "public-ipv4", "IP Address for A Record")
45+
flag.StringVar(&ipAddress, "ipaddress", "1.2.4.5", "IP Address for A Record")
4046
flag.BoolVar(&register, "register", false, "Register DNS and exit")
4147
flag.BoolVar(&unRegister, "unregister", false, "Unregister DNS and exit")
4248
flag.Parse()
@@ -122,7 +128,7 @@ func tearDownDNS(ctx context.Context) {
122128
log.Print("DNS Timeout expiry finished")
123129
}
124130

125-
func setupDNS(ctx context.Context) {
131+
func setupDNS(ctx context.Context) error {
126132
log.Printf("Setting up Route 53 DNS Name A %s => %s", dns, ipAddress)
127133

128134
input := &route53.ChangeResourceRecordSetsInput{
@@ -149,22 +155,34 @@ func setupDNS(ctx context.Context) {
149155
HostedZoneId: aws.String(hostedZone),
150156
}
151157

152-
changeSet, err := r53.ChangeResourceRecordSets(ctx, input)
153-
if err != nil {
154-
log.Printf("Failed to create DNS: %v", err.Error())
155-
return
158+
var changeSet *route53.ChangeResourceRecordSetsOutput
159+
for {
160+
var err error
161+
changeSet, err = r53.ChangeResourceRecordSets(ctx, input)
162+
if err != nil {
163+
if !strings.Contains(err.Error(), "conflicting RRSet of type CNAME") {
164+
log.Printf("Failed to create DNS: %v", err.Error())
165+
return err
166+
}
167+
// If the error is due to a conflicting CNAME, wait and try again
168+
if err := SleepWithContext(ctx, 5*time.Second); err != nil {
169+
return err
170+
}
171+
continue
172+
}
173+
break
156174
}
157175

158176
log.Print("Request sent to Route 53...")
159-
waitForSync(ctx, changeSet)
177+
return waitForSync(ctx, changeSet)
160178
}
161179

162-
func waitForSync(ctx context.Context, changeSet *route53.ChangeResourceRecordSetsOutput) {
180+
func waitForSync(ctx context.Context, changeSet *route53.ChangeResourceRecordSetsOutput) error {
163181
failures := 0
164182
for {
165183
if err := SleepWithContext(ctx, 5*time.Second); err != nil {
166184
log.Print("Context cancelled, stop waiting for Route53 ChangeSet to propogate")
167-
return
185+
return err
168186
}
169187

170188
changeOutput, err := r53.GetChange(ctx, &route53.GetChangeInput{
@@ -181,7 +199,7 @@ func waitForSync(ctx context.Context, changeSet *route53.ChangeResourceRecordSet
181199

182200
if changeOutput.ChangeInfo.Status == "INSYNC" {
183201
log.Print("Route53 Change Completed")
184-
break
202+
return nil
185203
}
186204

187205
log.Printf("Route53 Change not yet propogated (ChangeInfo.Status = %s)...", changeOutput.ChangeInfo.Status)

main_test.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package main
22

33
import (
4+
"context"
45
"net/http"
56
"net/http/httptest"
67
"os"
78
"testing"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/service/route53"
12+
"github.com/aws/aws-sdk-go-v2/service/route53/types"
13+
"github.com/aws/smithy-go"
14+
"github.com/aws/smithy-go/ptr"
815
)
916

1017
func Test_getEcsMetadata(t *testing.T) {
@@ -14,7 +21,7 @@ func Test_getEcsMetadata(t *testing.T) {
1421
w.Write([]byte(`{"Name":"curl","Networks":[{"IPv4Addresses":["` + want + `"]}]}`))
1522
})
1623
server := httptest.NewServer(handler)
17-
defer server.Close()
24+
t.Cleanup(server.Close)
1825

1926
os.Setenv("ECS_CONTAINER_METADATA_URI_V4", server.URL)
2027

@@ -27,3 +34,59 @@ func Test_getEcsMetadata(t *testing.T) {
2734
t.Errorf("getEcsMetadata() = %v, want %v", got, want)
2835
}
2936
}
37+
38+
type mockRoute53 struct {
39+
calls int
40+
wantErrs []error
41+
}
42+
43+
func (m *mockRoute53) ChangeResourceRecordSets(ctx context.Context, input *route53.ChangeResourceRecordSetsInput, opts ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) {
44+
var err error
45+
if m.calls < len(m.wantErrs) {
46+
err = m.wantErrs[m.calls]
47+
}
48+
m.calls++
49+
return &route53.ChangeResourceRecordSetsOutput{
50+
ChangeInfo: &types.ChangeInfo{
51+
Id: aws.String("mockChangeId"),
52+
},
53+
}, err
54+
}
55+
56+
func (m *mockRoute53) GetChange(ctx context.Context, input *route53.GetChangeInput, opts ...func(*route53.Options)) (*route53.GetChangeOutput, error) {
57+
return &route53.GetChangeOutput{
58+
ChangeInfo: &types.ChangeInfo{
59+
Status: types.ChangeStatusInsync,
60+
},
61+
}, nil
62+
}
63+
64+
func Test_setupDNS(t *testing.T) {
65+
ctx := context.Background()
66+
67+
dns = "cname.nextjs.internal."
68+
ipAddress = "1.2.3.4"
69+
70+
t.Run("success", func(t *testing.T) {
71+
r53 = &mockRoute53{}
72+
73+
if err := setupDNS(ctx); err != nil {
74+
t.Errorf("setupDNS() error = %v", err)
75+
}
76+
})
77+
78+
t.Run("retries", func(t *testing.T) {
79+
r53 = &mockRoute53{
80+
wantErrs: []error{&smithy.OperationError{
81+
ServiceID: "Route 53",
82+
OperationName: "ChangeResourceRecordSets",
83+
Err: &types.InvalidChangeBatch{
84+
Message: ptr.String("[RRSet of type A with DNS name cname.nextjs.internal. is not permitted because a conflicting RRSet of type CNAME with the same DNS name already exists in zone nextjs.internal.]"),
85+
},
86+
}},
87+
}
88+
if err := setupDNS(ctx); err != nil {
89+
t.Errorf("setupDNS() error = %v", err)
90+
}
91+
})
92+
}

0 commit comments

Comments
 (0)