From 319f0a64757a2e4ef1d474a6da81ce773945f6ea Mon Sep 17 00:00:00 2001 From: Genevieve L'Esperance Date: Mon, 6 Jan 2020 11:08:26 -0500 Subject: [PATCH] Delete aws route53 record sets that contain filter in zones that do not. WRT https://github.com/genevieve/leftovers/issues/80 --- aws/route53/fakes/record_sets.go | 49 +++++++++++++++++++------ aws/route53/hosted_zone.go | 28 ++++++++++----- aws/route53/hosted_zone_test.go | 62 ++++++++++++++++++++++++++------ aws/route53/hosted_zones.go | 23 ++++++++++-- aws/route53/hosted_zones_test.go | 16 +++++++++ aws/route53/record_sets.go | 26 +++++++++++++- aws/route53/record_sets_test.go | 45 ++++++++++++++++++++--- 7 files changed, 210 insertions(+), 39 deletions(-) diff --git a/aws/route53/fakes/record_sets.go b/aws/route53/fakes/record_sets.go index 36f0be93..22258383 100644 --- a/aws/route53/fakes/record_sets.go +++ b/aws/route53/fakes/record_sets.go @@ -7,7 +7,7 @@ import ( ) type RecordSets struct { - DeleteCall struct { + DeleteAllCall struct { sync.Mutex CallCount int Receives struct { @@ -20,6 +20,20 @@ type RecordSets struct { } Stub func(*string, string, []*awsroute53.ResourceRecordSet) error } + DeleteWithFilterCall struct { + sync.Mutex + CallCount int + Receives struct { + HostedZoneId *string + HostedZoneName string + RecordSets []*awsroute53.ResourceRecordSet + Filter string + } + Returns struct { + Error error + } + Stub func(*string, string, []*awsroute53.ResourceRecordSet, string) error + } GetCall struct { sync.Mutex CallCount int @@ -34,17 +48,30 @@ type RecordSets struct { } } -func (f *RecordSets) Delete(param1 *string, param2 string, param3 []*awsroute53.ResourceRecordSet) error { - f.DeleteCall.Lock() - defer f.DeleteCall.Unlock() - f.DeleteCall.CallCount++ - f.DeleteCall.Receives.HostedZoneId = param1 - f.DeleteCall.Receives.HostedZoneName = param2 - f.DeleteCall.Receives.RecordSets = param3 - if f.DeleteCall.Stub != nil { - return f.DeleteCall.Stub(param1, param2, param3) +func (f *RecordSets) DeleteAll(param1 *string, param2 string, param3 []*awsroute53.ResourceRecordSet) error { + f.DeleteAllCall.Lock() + defer f.DeleteAllCall.Unlock() + f.DeleteAllCall.CallCount++ + f.DeleteAllCall.Receives.HostedZoneId = param1 + f.DeleteAllCall.Receives.HostedZoneName = param2 + f.DeleteAllCall.Receives.RecordSets = param3 + if f.DeleteAllCall.Stub != nil { + return f.DeleteAllCall.Stub(param1, param2, param3) + } + return f.DeleteAllCall.Returns.Error +} +func (f *RecordSets) DeleteWithFilter(param1 *string, param2 string, param3 []*awsroute53.ResourceRecordSet, param4 string) error { + f.DeleteWithFilterCall.Lock() + defer f.DeleteWithFilterCall.Unlock() + f.DeleteWithFilterCall.CallCount++ + f.DeleteWithFilterCall.Receives.HostedZoneId = param1 + f.DeleteWithFilterCall.Receives.HostedZoneName = param2 + f.DeleteWithFilterCall.Receives.RecordSets = param3 + f.DeleteWithFilterCall.Receives.Filter = param4 + if f.DeleteWithFilterCall.Stub != nil { + return f.DeleteWithFilterCall.Stub(param1, param2, param3, param4) } - return f.DeleteCall.Returns.Error + return f.DeleteWithFilterCall.Returns.Error } func (f *RecordSets) Get(param1 *string) ([]*awsroute53.ResourceRecordSet, error) { f.GetCall.Lock() diff --git a/aws/route53/hosted_zone.go b/aws/route53/hosted_zone.go index b8ce7659..c2b13950 100644 --- a/aws/route53/hosted_zone.go +++ b/aws/route53/hosted_zone.go @@ -2,6 +2,7 @@ package route53 import ( "fmt" + "strings" awsroute53 "github.com/aws/aws-sdk-go/service/route53" ) @@ -11,14 +12,16 @@ type HostedZone struct { id *string identifier string recordSets recordSets + filter string } -func NewHostedZone(client hostedZonesClient, id, name *string, recordSets recordSets) HostedZone { +func NewHostedZone(client hostedZonesClient, id, name *string, recordSets recordSets, filter string) HostedZone { return HostedZone{ client: client, id: id, identifier: *name, recordSets: recordSets, + filter: filter, } } @@ -28,14 +31,21 @@ func (h HostedZone) Delete() error { return fmt.Errorf("Get Record Sets: %s", err) } - err = h.recordSets.Delete(h.id, h.identifier, r) - if err != nil { - return fmt.Errorf("Delete Record Sets: %s", err) - } - - _, err = h.client.DeleteHostedZone(&awsroute53.DeleteHostedZoneInput{Id: h.id}) - if err != nil { - return fmt.Errorf("Delete: %s", err) + if strings.Contains(h.Name(), h.filter) { + err = h.recordSets.DeleteAll(h.id, h.identifier, r) + if err != nil { + return fmt.Errorf("Delete All Record Sets: %s", err) + } + + _, err = h.client.DeleteHostedZone(&awsroute53.DeleteHostedZoneInput{Id: h.id}) + if err != nil { + return fmt.Errorf("Delete: %s", err) + } + } else { + err = h.recordSets.DeleteWithFilter(h.id, h.identifier, r, h.filter) + if err != nil { + return fmt.Errorf("Delete Record Sets With Filter: %s", err) + } } return nil diff --git a/aws/route53/hosted_zone_test.go b/aws/route53/hosted_zone_test.go index 0f9b81ee..5cbbbf01 100644 --- a/aws/route53/hosted_zone_test.go +++ b/aws/route53/hosted_zone_test.go @@ -17,6 +17,7 @@ var _ = Describe("HostedZone", func() { recordSets *fakes.RecordSets id *string name *string + filter string hostedZone route53.HostedZone ) @@ -26,55 +27,94 @@ var _ = Describe("HostedZone", func() { recordSets = &fakes.RecordSets{} id = aws.String("the-zone-id") name = aws.String("the-zone-name") + filter = "zone" - hostedZone = route53.NewHostedZone(client, id, name, recordSets) + hostedZone = route53.NewHostedZone(client, id, name, recordSets, filter) }) Describe("Delete", func() { - It("deletes the record sets and deletes the hosted zone", func() { + It("deletes all record sets and deletes the hosted zone", func() { err := hostedZone.Delete() Expect(err).NotTo(HaveOccurred()) Expect(recordSets.GetCall.CallCount).To(Equal(1)) Expect(recordSets.GetCall.Receives.HostedZoneId).To(Equal(id)) - Expect(recordSets.DeleteCall.CallCount).To(Equal(1)) - Expect(recordSets.DeleteCall.Receives.HostedZoneId).To(Equal(id)) + Expect(recordSets.DeleteAllCall.CallCount).To(Equal(1)) + Expect(recordSets.DeleteAllCall.Receives.HostedZoneId).To(Equal(id)) + + Expect(recordSets.DeleteWithFilterCall.CallCount).To(Equal(0)) Expect(client.DeleteHostedZoneCall.CallCount).To(Equal(1)) Expect(client.DeleteHostedZoneCall.Receives.DeleteHostedZoneInput.Id).To(Equal(id)) }) + Context("when the zone does not contain the filter", func() { + BeforeEach(func() { + filter = "banana" + hostedZone = route53.NewHostedZone(client, id, name, recordSets, filter) + }) + + It("deletes only record sets in the zone that contain the filter", func() { + err := hostedZone.Delete() + Expect(err).NotTo(HaveOccurred()) + + Expect(recordSets.GetCall.CallCount).To(Equal(1)) + Expect(recordSets.GetCall.Receives.HostedZoneId).To(Equal(id)) + + Expect(recordSets.DeleteAllCall.CallCount).To(Equal(0)) + + Expect(recordSets.DeleteWithFilterCall.CallCount).To(Equal(1)) + Expect(recordSets.DeleteWithFilterCall.Receives.HostedZoneId).To(Equal(id)) + Expect(recordSets.DeleteWithFilterCall.Receives.Filter).To(Equal("banana")) + + Expect(client.DeleteHostedZoneCall.CallCount).To(Equal(0)) + }) + }) + Context("when record sets fails to get", func() { BeforeEach(func() { - recordSets.GetCall.Returns.Error = errors.New("banana") + recordSets.GetCall.Returns.Error = errors.New("ruhroh") + }) + + It("returns the error", func() { + err := hostedZone.Delete() + Expect(err).To(MatchError("Get Record Sets: ruhroh")) + }) + }) + + Context("when deleting all record sets fails", func() { + BeforeEach(func() { + recordSets.DeleteAllCall.Returns.Error = errors.New("ruhroh") }) It("returns the error", func() { err := hostedZone.Delete() - Expect(err).To(MatchError("Get Record Sets: banana")) + Expect(err).To(MatchError("Delete All Record Sets: ruhroh")) }) }) - Context("when record sets fails to delete", func() { + Context("when deleting record sets with filter fails", func() { BeforeEach(func() { - recordSets.DeleteCall.Returns.Error = errors.New("banana") + filter = "banana" + hostedZone = route53.NewHostedZone(client, id, name, recordSets, filter) + recordSets.DeleteWithFilterCall.Returns.Error = errors.New("ruhroh") }) It("returns the error", func() { err := hostedZone.Delete() - Expect(err).To(MatchError("Delete Record Sets: banana")) + Expect(err).To(MatchError("Delete Record Sets With Filter: ruhroh")) }) }) Context("when the client fails to delete the zone", func() { BeforeEach(func() { - client.DeleteHostedZoneCall.Returns.Error = errors.New("banana") + client.DeleteHostedZoneCall.Returns.Error = errors.New("ruhroh") }) It("returns the error", func() { err := hostedZone.Delete() - Expect(err).To(MatchError("Delete: banana")) + Expect(err).To(MatchError("Delete: ruhroh")) }) }) }) diff --git a/aws/route53/hosted_zones.go b/aws/route53/hosted_zones.go index ec860d63..70417946 100644 --- a/aws/route53/hosted_zones.go +++ b/aws/route53/hosted_zones.go @@ -23,7 +23,8 @@ type HostedZones struct { //go:generate faux --interface recordSets --output fakes/record_sets.go type recordSets interface { Get(hostedZoneId *string) ([]*awsroute53.ResourceRecordSet, error) - Delete(hostedZoneId *string, hostedZoneName string, recordSets []*awsroute53.ResourceRecordSet) error + DeleteAll(hostedZoneId *string, hostedZoneName string, recordSets []*awsroute53.ResourceRecordSet) error + DeleteWithFilter(hostedZoneId *string, hostedZoneName string, recordSets []*awsroute53.ResourceRecordSet, filter string) error } func NewHostedZones(client hostedZonesClient, logger logger, recordSets recordSets) HostedZones { @@ -42,9 +43,9 @@ func (z HostedZones) List(filter string) ([]common.Deletable, error) { var resources []common.Deletable for _, zone := range zones.HostedZones { - r := NewHostedZone(z.client, zone.Id, zone.Name, z.recordSets) + r := NewHostedZone(z.client, zone.Id, zone.Name, z.recordSets, filter) - if !strings.Contains(r.Name(), filter) { + if !strings.Contains(r.Name(), filter) && !z.recordSetsContainFilter(zone.Id, filter) { continue } @@ -62,3 +63,19 @@ func (z HostedZones) List(filter string) ([]common.Deletable, error) { func (z HostedZones) Type() string { return "route53-hosted-zone" } + +// Check if any record sets in the hosted zone reference the filter. +func (z HostedZones) recordSetsContainFilter(hostedZoneId *string, filter string) bool { + records, err := z.recordSets.Get(hostedZoneId) + if err != nil { + return false + } + + for _, record := range records { + if strings.Contains(*record.Name, filter) { + return true + } + } + + return false +} diff --git a/aws/route53/hosted_zones_test.go b/aws/route53/hosted_zones_test.go index 92457897..e1e55317 100644 --- a/aws/route53/hosted_zones_test.go +++ b/aws/route53/hosted_zones_test.go @@ -23,6 +23,7 @@ var _ = Describe("HostedZones", func() { BeforeEach(func() { client = &fakes.HostedZonesClient{} logger = &fakes.Logger{} + recordSets = &fakes.RecordSets{} hostedZones = route53.NewHostedZones(client, logger, recordSets) }) @@ -75,6 +76,21 @@ var _ = Describe("HostedZones", func() { }) }) + Context("when the record sets contain the filter", func() { + BeforeEach(func() { + recordSets.GetCall.Returns.ResourceRecordSetSlice = []*awsroute53.ResourceRecordSet{{ + Name: aws.String("kiwi"), + }} + }) + It("does not return it in the list", func() { + items, err := hostedZones.List("kiwi") + Expect(err).NotTo(HaveOccurred()) + + Expect(logger.PromptWithDetailsCall.CallCount).To(Equal(1)) + Expect(items).To(HaveLen(1)) + }) + }) + Context("when the user responds no to the prompt", func() { BeforeEach(func() { logger.PromptWithDetailsCall.Returns.Proceed = false diff --git a/aws/route53/record_sets.go b/aws/route53/record_sets.go index eabe826e..b009a9d2 100644 --- a/aws/route53/record_sets.go +++ b/aws/route53/record_sets.go @@ -48,7 +48,7 @@ func (r RecordSets) Get(hostedZoneId *string) ([]*awsroute53.ResourceRecordSet, return records, nil } -func (r RecordSets) Delete(hostedZoneId *string, hostedZoneName string, records []*awsroute53.ResourceRecordSet) error { +func (r RecordSets) DeleteAll(hostedZoneId *string, hostedZoneName string, records []*awsroute53.ResourceRecordSet) error { var changes []*awsroute53.Change for _, record := range records { if strings.TrimSuffix(*record.Name, ".") == strings.TrimSuffix(hostedZoneName, ".") && (*record.Type == "NS" || *record.Type == "SOA") { @@ -72,3 +72,27 @@ func (r RecordSets) Delete(hostedZoneId *string, hostedZoneName string, records return nil } + +func (r RecordSets) DeleteWithFilter(hostedZoneId *string, hostedZoneName string, records []*awsroute53.ResourceRecordSet, filter string) error { + var changes []*awsroute53.Change + for _, record := range records { + if strings.Contains(*record.Name, filter) && *record.Type == "A" { + changes = append(changes, &awsroute53.Change{ + Action: aws.String("DELETE"), + ResourceRecordSet: record, + }) + } + } + + if len(changes) > 0 { + _, err := r.client.ChangeResourceRecordSets(&awsroute53.ChangeResourceRecordSetsInput{ + HostedZoneId: hostedZoneId, + ChangeBatch: &awsroute53.ChangeBatch{Changes: changes}, + }) + if err != nil { + return fmt.Errorf("Delete Resource Record Sets in Hosted Zone %s: %s", hostedZoneName, err) + } + } + + return nil +} diff --git a/aws/route53/record_sets_test.go b/aws/route53/record_sets_test.go index 4f8f4f09..8dd29265 100644 --- a/aws/route53/record_sets_test.go +++ b/aws/route53/record_sets_test.go @@ -2,6 +2,7 @@ package route53_test import ( "errors" + "fmt" "github.com/aws/aws-sdk-go/aws" awsroute53 "github.com/aws/aws-sdk-go/service/route53" @@ -116,7 +117,7 @@ var _ = Describe("RecordSets", func() { }) It("deletes the record sets", func() { - err := recordSets.Delete(hostedZoneId, hostedZoneName, records) + err := recordSets.DeleteAll(hostedZoneId, hostedZoneName, records) Expect(err).NotTo(HaveOccurred()) Expect(client.ChangeResourceRecordSetsCall.CallCount).To(Equal(1)) @@ -134,7 +135,7 @@ var _ = Describe("RecordSets", func() { }) It("does not try to delete it", func() { - err := recordSets.Delete(hostedZoneId, hostedZoneName, records) + err := recordSets.DeleteAll(hostedZoneId, hostedZoneName, records) Expect(err).NotTo(HaveOccurred()) Expect(client.ChangeResourceRecordSetsCall.CallCount).To(Equal(0)) @@ -150,7 +151,7 @@ var _ = Describe("RecordSets", func() { }) It("does not try to delete it", func() { - err := recordSets.Delete(hostedZoneId, hostedZoneName, records) + err := recordSets.DeleteAll(hostedZoneId, hostedZoneName, records) Expect(err).NotTo(HaveOccurred()) Expect(client.ChangeResourceRecordSetsCall.CallCount).To(Equal(0)) @@ -163,9 +164,45 @@ var _ = Describe("RecordSets", func() { }) It("returns the error", func() { - err := recordSets.Delete(hostedZoneId, hostedZoneName, records) + err := recordSets.DeleteAll(hostedZoneId, hostedZoneName, records) Expect(err).To(MatchError("Delete Resource Record Sets: banana")) }) }) }) + + Describe("DeleteWithFilter", func() { + var ( + records []*awsroute53.ResourceRecordSet + filter string + ) + + BeforeEach(func() { + records = []*awsroute53.ResourceRecordSet{ + {Name: aws.String(fmt.Sprintf("kiwi-%s", hostedZoneName)), Type: aws.String("A")}, + {Name: aws.String(fmt.Sprintf("banana-%s", hostedZoneName)), Type: aws.String("A")}, + } + filter = "banana" + }) + + It("deletes the record sets that contain the filter", func() { + err := recordSets.DeleteWithFilter(hostedZoneId, hostedZoneName, records, filter) + Expect(err).NotTo(HaveOccurred()) + + Expect(client.ChangeResourceRecordSetsCall.CallCount).To(Equal(1)) + Expect(client.ChangeResourceRecordSetsCall.Receives.ChangeResourceRecordSetsInput.HostedZoneId).To(Equal(hostedZoneId)) + Expect(client.ChangeResourceRecordSetsCall.Receives.ChangeResourceRecordSetsInput.ChangeBatch.Changes[0].Action).To(Equal(aws.String("DELETE"))) + Expect(client.ChangeResourceRecordSetsCall.Receives.ChangeResourceRecordSetsInput.ChangeBatch.Changes[0].ResourceRecordSet.Type).To(Equal(aws.String("A"))) + }) + + Context("when the client fails to delete resource record sets", func() { + BeforeEach(func() { + client.ChangeResourceRecordSetsCall.Returns.Error = errors.New("ruhroh") + }) + + It("returns the error", func() { + err := recordSets.DeleteWithFilter(hostedZoneId, hostedZoneName, records, filter) + Expect(err).To(MatchError(fmt.Sprintf("Delete Resource Record Sets in Hosted Zone %s: ruhroh", hostedZoneName))) + }) + }) + }) })