From bbf8bbe10d2a6d098757ba31e80eb409f76e5a23 Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Mon, 2 Dec 2024 22:28:58 -0700 Subject: [PATCH 1/9] adds initial cloudflare command to delete dns records --- cmd/cloudflare.go | 81 +++++++++++++++++++++++++++++ cmd/root.go | 1 + go.mod | 10 ++-- go.sum | 18 ++++--- internal/cloudflare/client.go | 95 +++++++++++++++++++++++++++++++++++ internal/cloudflare/dns.go | 61 ++++++++++++++++++++++ 6 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 cmd/cloudflare.go create mode 100644 internal/cloudflare/client.go create mode 100644 internal/cloudflare/dns.go diff --git a/cmd/cloudflare.go b/cmd/cloudflare.go new file mode 100644 index 0000000..22a8a7f --- /dev/null +++ b/cmd/cloudflare.go @@ -0,0 +1,81 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + cloudflare "github.com/konstructio/dropkick/internal/cloudflare" + "github.com/konstructio/dropkick/internal/logger" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +type cloudflareOptions struct { + nuke bool + domain string + subdomain string + quiet bool +} + +func getCloudflareCommand() *cobra.Command { + var opts cloudflareOptions + + cloudflareCmd := &cobra.Command{ + Use: "cloudflare", + Short: "clean cloudflare dns resources", + Long: `clean cloudflare dns resources`, + RunE: func(cmd *cobra.Command, _ []string) error { + opts.quiet = cmd.Flags().Lookup("quiet").Value.String() == "true" + return runCloudflare(cmd.Context(), cmd.OutOrStderr(), opts, os.Getenv("CLOUDFLARE_API_TOKEN")) + }, + } + + cloudflareCmd.Flags().BoolVar(&opts.nuke, "nuke", false, "required to confirm deletion of resources") + cloudflareCmd.Flags().StringVar(&opts.domain, "domain", "", "the cloudflare apex domain to clean") + cloudflareCmd.Flags().StringVar(&opts.subdomain, "subdomain", "", "the subdomain to clean") + + if err := cloudflareCmd.MarkFlagRequired("domain"); err != nil { + log.Fatal(err) + } + + return cloudflareCmd +} + +func runCloudflare(ctx context.Context, output io.Writer, opts cloudflareOptions, token string) error { + if token == "" { + return errors.New("CLOUDFLARE_API_TOKEN environment variable not found: get an api key here https://dash.cloudflare.com/profile/api-tokens") + } + + // Create a logger and make it quiet + var log *logger.Logger + if opts.quiet { + log = logger.New(io.Discard) + } else { + log = logger.New(output) + } + + client, err := cloudflare.New( + ctx, + cloudflare.WithToken(token), + cloudflare.WithZoneName(opts.domain), + cloudflare.WithSubdomain(opts.subdomain), + cloudflare.WithNuke(opts.nuke), + cloudflare.WithLogger(log), + ) + if err != nil { + return fmt.Errorf("unable to create new cloudflare client: %w", err) + } + + if err := client.NukeDNSRecords(ctx); err != nil { + if opts.nuke { + return fmt.Errorf("unable to nuke resources: %w", err) + } + + return fmt.Errorf("unable to process resources: %w", err) + } + + return nil +} diff --git a/cmd/root.go b/cmd/root.go index d6d9688..6b208ed 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ func Execute() { // Add subcommands rootCmd.AddCommand(getCivoCommand()) rootCmd.AddCommand(getDigitalOceanCommand()) + rootCmd.AddCommand(getCloudflareCommand()) // Configure a global flag for "--quiet" rootCmd.PersistentFlags().BoolP("quiet", "q", false, "suppress output from processing while keeping deletion messages to stdout") diff --git a/go.mod b/go.mod index d9b3717..ac850c9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23 require ( github.com/aws/aws-sdk-go v1.55.5 + github.com/cloudflare/cloudflare-go v0.110.0 github.com/digitalocean/godo v1.119.0 github.com/fatih/color v1.17.0 github.com/sirupsen/logrus v1.9.3 @@ -11,6 +12,7 @@ require ( ) require ( + github.com/goccy/go-json v0.10.3 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -21,11 +23,11 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.9.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.31.0 // indirect golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.8.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 41b7bc6..3de0bbb 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.110.0 h1:aBKKUXwRWqErd4rITsnCLESOacxxset/BcpdXn23900= +github.com/cloudflare/cloudflare-go v0.110.0/go.mod h1:2ZZ+EkmThmd6pkZ56UKGXWpz2wsjeqoTg93P4+VSmMg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,6 +58,8 @@ github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLg github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -222,8 +226,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -269,8 +273,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -278,11 +282,13 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/cloudflare/client.go b/internal/cloudflare/client.go new file mode 100644 index 0000000..6efb237 --- /dev/null +++ b/internal/cloudflare/client.go @@ -0,0 +1,95 @@ +package cloudflare + +import ( + "context" + "errors" + "fmt" + "os" + + cloudflarego "github.com/cloudflare/cloudflare-go" + "github.com/konstructio/dropkick/internal/logger" +) + +// Cloudflare is a client for the Cloudflare API. +type Cloudflare struct { + client *cloudflarego.API // The underlying Cloudflare API client. + nuke bool // Whether to nuke resources. + token string // The API token. + subdomain string // The subdomain to delete records from. + zoneID string // The zone ID for the domain. + zoneName string // The domain to clean in cloudflare. + logger *logger.Logger // The logger instance. +} + +// Option is a function that configures a Cloudflare. +type Option func(*Cloudflare) error + +// WithLogger sets the logger for a Cloudflare. +func WithLogger(logger *logger.Logger) Option { + return func(c *Cloudflare) error { + c.logger = logger + return nil + } +} + +// WithZoneName sets the API token for a Cloudflare. +func WithZoneName(zoneName string) Option { + return func(c *Cloudflare) error { + c.zoneName = zoneName + return nil + } +} + +// WithSubdomain sets the subdomain to filter on for aCloudflare. +func WithSubdomain(subdomain string) Option { + return func(c *Cloudflare) error { + c.subdomain = subdomain + return nil + } +} + +// WithToken sets the API token for a Cloudflare. +func WithToken(token string) Option { + return func(c *Cloudflare) error { + c.token = token + return nil + } +} + +// WithNuke sets whether to nuke resources for a Cloudflare. +func WithNuke(nuke bool) Option { + return func(c *Cloudflare) error { + c.nuke = nuke + return nil + } +} + +// New creates a new Cloudflare with the given options. +// It returns an error if the token or region is not set, or if it fails to +// create the underlying Cloudflare API client. +func New(ctx context.Context, opts ...Option) (*Cloudflare, error) { + c := &Cloudflare{} + + for _, opt := range opts { + if err := opt(c); err != nil { + return nil, fmt.Errorf("unable to apply option: %w", err) + } + } + + if c.token == "" { + return nil, errors.New("required token not found for cloudflare client") + } + + cloudflareAPI, err := cloudflarego.NewWithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")) + if err != nil { + return nil, fmt.Errorf("unable to authenticate Cloudflare client: %w", err) + } + + c.client = cloudflareAPI + + if c.logger == nil { + c.logger = logger.None + } + + return c, nil +} diff --git a/internal/cloudflare/dns.go b/internal/cloudflare/dns.go new file mode 100644 index 0000000..d050008 --- /dev/null +++ b/internal/cloudflare/dns.go @@ -0,0 +1,61 @@ +package cloudflare + +import ( + "context" + "fmt" + "strings" + + cloudflarego "github.com/cloudflare/cloudflare-go" +) + +func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { + c.logger.Infof("removing dns records for domain %q", c.zoneName) + + zones, err := c.client.ListZones(ctx) + if err != nil { + return fmt.Errorf("unable to list zones: %w", err) + } + + for _, zone := range zones { + if zone.Name == c.zoneName { + c.zoneID = zone.ID + } + } + if c.zoneID == "" { + return fmt.Errorf("unable to find zone ID for domain: %q", c.zoneName) + } + + records, _, err := c.client.ListDNSRecords(ctx, &cloudflarego.ResourceContainer{ + Identifier: c.zoneID, + }, cloudflarego.ListDNSRecordsParams{}) + if err != nil { + return fmt.Errorf("unable to list records for domain %q: %w", c.zoneName, err) + } + + // if subdomain is set filter the records + var subdomainRecords []cloudflarego.DNSRecord + if c.subdomain != "" { + for _, r := range records { + if strings.Contains(r.Name, c.subdomain) { + subdomainRecords = append(subdomainRecords, r) + } + } + records = subdomainRecords + c.logger.Infof("found %d records for %q", len(records), fmt.Sprintf("%s.%s", c.subdomain, c.zoneName)) + } else { + c.logger.Infof("found %d records for %q", len(records), c.zoneName) + } + + for _, r := range records { + if c.nuke { + c.logger.Infof("nuke enabled, deleting record %q", r.Name) + if err := c.client.DeleteDNSRecord(ctx, &cloudflarego.ResourceContainer{Identifier: c.zoneID}, r.ID); err != nil { + return fmt.Errorf("unable to delete record %q: %w", r.Name, err) + } + } else { + c.logger.Warnf("nuke disabled, found record %q", r.Name) + } + } + + return nil +} From c126d4af60993d0bc2fcdd47464d9a0eeddd2620 Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Mon, 2 Dec 2024 22:42:59 -0700 Subject: [PATCH 2/9] fix context and get zone by id --- cmd/cloudflare.go | 1 - internal/cloudflare/client.go | 9 +++++++-- internal/cloudflare/dns.go | 14 -------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/cmd/cloudflare.go b/cmd/cloudflare.go index 22a8a7f..393ea21 100644 --- a/cmd/cloudflare.go +++ b/cmd/cloudflare.go @@ -58,7 +58,6 @@ func runCloudflare(ctx context.Context, output io.Writer, opts cloudflareOptions } client, err := cloudflare.New( - ctx, cloudflare.WithToken(token), cloudflare.WithZoneName(opts.domain), cloudflare.WithSubdomain(opts.subdomain), diff --git a/internal/cloudflare/client.go b/internal/cloudflare/client.go index 6efb237..e860402 100644 --- a/internal/cloudflare/client.go +++ b/internal/cloudflare/client.go @@ -1,7 +1,6 @@ package cloudflare import ( - "context" "errors" "fmt" "os" @@ -67,7 +66,7 @@ func WithNuke(nuke bool) Option { // New creates a new Cloudflare with the given options. // It returns an error if the token or region is not set, or if it fails to // create the underlying Cloudflare API client. -func New(ctx context.Context, opts ...Option) (*Cloudflare, error) { +func New(opts ...Option) (*Cloudflare, error) { c := &Cloudflare{} for _, opt := range opts { @@ -85,6 +84,12 @@ func New(ctx context.Context, opts ...Option) (*Cloudflare, error) { return nil, fmt.Errorf("unable to authenticate Cloudflare client: %w", err) } + zoneID, err := cloudflareAPI.ZoneIDByName(c.zoneName) + if err != nil { + return nil, fmt.Errorf("unable to get zone ID by name %q: %w", c.zoneName, err) + } + c.zoneID = zoneID + c.client = cloudflareAPI if c.logger == nil { diff --git a/internal/cloudflare/dns.go b/internal/cloudflare/dns.go index d050008..2e2c44a 100644 --- a/internal/cloudflare/dns.go +++ b/internal/cloudflare/dns.go @@ -11,20 +11,6 @@ import ( func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { c.logger.Infof("removing dns records for domain %q", c.zoneName) - zones, err := c.client.ListZones(ctx) - if err != nil { - return fmt.Errorf("unable to list zones: %w", err) - } - - for _, zone := range zones { - if zone.Name == c.zoneName { - c.zoneID = zone.ID - } - } - if c.zoneID == "" { - return fmt.Errorf("unable to find zone ID for domain: %q", c.zoneName) - } - records, _, err := c.client.ListDNSRecords(ctx, &cloudflarego.ResourceContainer{ Identifier: c.zoneID, }, cloudflarego.ListDNSRecordsParams{}) From aa651db8706e42d7edd7a3cc8016cad0dd73c75e Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Mon, 2 Dec 2024 22:51:20 -0700 Subject: [PATCH 3/9] remove alias for internal package --- cmd/cloudflare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cloudflare.go b/cmd/cloudflare.go index 393ea21..8637853 100644 --- a/cmd/cloudflare.go +++ b/cmd/cloudflare.go @@ -7,7 +7,7 @@ import ( "io" "os" - cloudflare "github.com/konstructio/dropkick/internal/cloudflare" + "github.com/konstructio/dropkick/internal/cloudflare" "github.com/konstructio/dropkick/internal/logger" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" From e3c47f9376ba446ccee21b15ad1bfb12b0acb117 Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Mon, 2 Dec 2024 22:53:19 -0700 Subject: [PATCH 4/9] fix comment --- internal/cloudflare/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cloudflare/client.go b/internal/cloudflare/client.go index e860402..7782315 100644 --- a/internal/cloudflare/client.go +++ b/internal/cloudflare/client.go @@ -64,7 +64,7 @@ func WithNuke(nuke bool) Option { } // New creates a new Cloudflare with the given options. -// It returns an error if the token or region is not set, or if it fails to +// It returns an error if the token is not set, or if it fails to // create the underlying Cloudflare API client. func New(opts ...Option) (*Cloudflare, error) { c := &Cloudflare{} From 0e34d90bc61ebb7725b4f850bf7952c50a913146 Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Tue, 3 Dec 2024 12:06:10 -0700 Subject: [PATCH 5/9] fix marked required flag --- cmd/cloudflare.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/cloudflare.go b/cmd/cloudflare.go index 8637853..47cc054 100644 --- a/cmd/cloudflare.go +++ b/cmd/cloudflare.go @@ -9,7 +9,6 @@ import ( "github.com/konstructio/dropkick/internal/cloudflare" "github.com/konstructio/dropkick/internal/logger" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -37,9 +36,7 @@ func getCloudflareCommand() *cobra.Command { cloudflareCmd.Flags().StringVar(&opts.domain, "domain", "", "the cloudflare apex domain to clean") cloudflareCmd.Flags().StringVar(&opts.subdomain, "subdomain", "", "the subdomain to clean") - if err := cloudflareCmd.MarkFlagRequired("domain"); err != nil { - log.Fatal(err) - } + cloudflareCmd.MarkFlagRequired("domain") return cloudflareCmd } From 181bb259dd0b8157ed45917ccf2a2358b4d10dc1 Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Tue, 3 Dec 2024 12:07:39 -0700 Subject: [PATCH 6/9] debug, not filtering --- internal/cloudflare/dns.go | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/internal/cloudflare/dns.go b/internal/cloudflare/dns.go index 2e2c44a..a1aa8e8 100644 --- a/internal/cloudflare/dns.go +++ b/internal/cloudflare/dns.go @@ -18,15 +18,9 @@ func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { return fmt.Errorf("unable to list records for domain %q: %w", c.zoneName, err) } - // if subdomain is set filter the records - var subdomainRecords []cloudflarego.DNSRecord + records = filterRecords(records, c.subdomain) + if c.subdomain != "" { - for _, r := range records { - if strings.Contains(r.Name, c.subdomain) { - subdomainRecords = append(subdomainRecords, r) - } - } - records = subdomainRecords c.logger.Infof("found %d records for %q", len(records), fmt.Sprintf("%s.%s", c.subdomain, c.zoneName)) } else { c.logger.Infof("found %d records for %q", len(records), c.zoneName) @@ -34,14 +28,34 @@ func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { for _, r := range records { if c.nuke { - c.logger.Infof("nuke enabled, deleting record %q", r.Name) + c.logger.Infof("nuke enabled, deleting record %q - %q", r.Type, r.Name) if err := c.client.DeleteDNSRecord(ctx, &cloudflarego.ResourceContainer{Identifier: c.zoneID}, r.ID); err != nil { return fmt.Errorf("unable to delete record %q: %w", r.Name, err) } } else { - c.logger.Warnf("nuke disabled, found record %q", r.Name) + c.logger.Warnf("nuke disabled, found record %q - %q", r.Type, r.Name) } } return nil } + +func filterRecords(records []cloudflarego.DNSRecord, subdomain string) []cloudflarego.DNSRecord { + var filteredRecords []cloudflarego.DNSRecord + aRecord := "A" + txtRecord := "TXT" + fmt.Println("subdomain: ", subdomain) + + for _, r := range records { + if subdomain != "" { + if strings.Contains(r.Name, subdomain) && r.Type == txtRecord || r.Type == aRecord { + filteredRecords = append(filteredRecords, r) + } + } else { + if r.Type == txtRecord || r.Type == aRecord { + filteredRecords = append(filteredRecords, r) + } + } + } + return filteredRecords +} From 07b1e28348b42ce8ae169c21b2d9dd0bb250e2bb Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Tue, 3 Dec 2024 13:37:30 -0700 Subject: [PATCH 7/9] add initial unit test and fix filtering --- internal/cloudflare/dns.go | 16 ++--- internal/cloudflare/dns_test.go | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 internal/cloudflare/dns_test.go diff --git a/internal/cloudflare/dns.go b/internal/cloudflare/dns.go index a1aa8e8..9870efa 100644 --- a/internal/cloudflare/dns.go +++ b/internal/cloudflare/dns.go @@ -18,15 +18,15 @@ func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { return fmt.Errorf("unable to list records for domain %q: %w", c.zoneName, err) } - records = filterRecords(records, c.subdomain) + filteredRecords := filterRecords(records, c.subdomain) if c.subdomain != "" { - c.logger.Infof("found %d records for %q", len(records), fmt.Sprintf("%s.%s", c.subdomain, c.zoneName)) + c.logger.Infof("found %d records for %q", len(filteredRecords), fmt.Sprintf("%s.%s", c.subdomain, c.zoneName)) } else { - c.logger.Infof("found %d records for %q", len(records), c.zoneName) + c.logger.Infof("found %d records for %q", len(filteredRecords), c.zoneName) } - for _, r := range records { + for _, r := range filteredRecords { if c.nuke { c.logger.Infof("nuke enabled, deleting record %q - %q", r.Type, r.Name) if err := c.client.DeleteDNSRecord(ctx, &cloudflarego.ResourceContainer{Identifier: c.zoneID}, r.ID); err != nil { @@ -40,15 +40,17 @@ func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { return nil } +// fetch all txt records with values like "heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/argo/argo-server" +// fetch all txt records with values like "heritage=external-dns,external-dns/owner=default,external-dns/resource" + func filterRecords(records []cloudflarego.DNSRecord, subdomain string) []cloudflarego.DNSRecord { - var filteredRecords []cloudflarego.DNSRecord + filteredRecords := make([]cloudflarego.DNSRecord, 0, len(records)) aRecord := "A" txtRecord := "TXT" - fmt.Println("subdomain: ", subdomain) for _, r := range records { if subdomain != "" { - if strings.Contains(r.Name, subdomain) && r.Type == txtRecord || r.Type == aRecord { + if strings.Contains(r.Name, subdomain) && (r.Type == txtRecord || r.Type == aRecord) { filteredRecords = append(filteredRecords, r) } } else { diff --git a/internal/cloudflare/dns_test.go b/internal/cloudflare/dns_test.go new file mode 100644 index 0000000..a9e2dce --- /dev/null +++ b/internal/cloudflare/dns_test.go @@ -0,0 +1,100 @@ +package cloudflare + +import ( + "reflect" + "testing" + + cloudflarego "github.com/cloudflare/cloudflare-go" +) + +func Test_filterRecords(t *testing.T) { + type args struct { + records []cloudflarego.DNSRecord + subdomain string + } + tests := []struct { + name string + args args + want []cloudflarego.DNSRecord + }{ + { + name: "successful case scenario", + args: args{ + records: []cloudflarego.DNSRecord{ + { + ID: "1", + Type: "CNAME", + }, + { + ID: "2", + Type: "TXT", + }, + }, + }, + want: []cloudflarego.DNSRecord{ + { + ID: "2", + Type: "TXT", + }, + }, + }, + { + name: "successful case scenario with subdomain", + args: args{ + records: []cloudflarego.DNSRecord{ + { + ID: "1", + Type: "CNAME", + Name: "metaphor-development.ci-k1-126b3ab2-civo-gh-cf.kubesecond.com", + }, + { + ID: "2", + Type: "A", + Name: "metaphor-development.ci-k1-2c6ab0c9-civo-gh-cf.kubesecond.com", + }, + }, + subdomain: "ci-k1-2c6ab0c9-civo-gh-cf", + }, + want: []cloudflarego.DNSRecord{ + { + ID: "2", + Type: "A", + Name: "metaphor-development.ci-k1-2c6ab0c9-civo-gh-cf.kubesecond.com", + }, + }, + }, + { + name: "successful case scenario with subdomain", + args: args{ + records: []cloudflarego.DNSRecord{ + { + ID: "1", + Type: "TXT", + Name: "metaphor-development.ci-k1-2c6ab0c9-civo-gh-cf.kubesecond.com", + }, + { + ID: "2", + Type: "A", + Name: "metaphor-development.ci-k1-foobar-civo-gh-cf.kubesecond.com", + }, + }, + subdomain: "ci-k1-2c6ab0c9-civo-gh-cf", + }, + want: []cloudflarego.DNSRecord{ + { + ID: "1", + Type: "TXT", + Name: "metaphor-development.ci-k1-2c6ab0c9-civo-gh-cf.kubesecond.com", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := filterRecords(tt.args.records, tt.args.subdomain); !reflect.DeepEqual(got, tt.want) { + t.Errorf("filterRecords() = %v, want %v", got, tt.want) + } + }) + } +} From f26f849de02f7c5de8eb0c0da9a24ce6d82ba2fd Mon Sep 17 00:00:00 2001 From: Jared Edwards Date: Tue, 3 Dec 2024 16:57:52 -0700 Subject: [PATCH 8/9] remove long Co-authored-by: Patrick D'appollonio <930925+patrickdappollonio@users.noreply.github.com> --- cmd/cloudflare.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/cloudflare.go b/cmd/cloudflare.go index 47cc054..545f6e2 100644 --- a/cmd/cloudflare.go +++ b/cmd/cloudflare.go @@ -25,7 +25,6 @@ func getCloudflareCommand() *cobra.Command { cloudflareCmd := &cobra.Command{ Use: "cloudflare", Short: "clean cloudflare dns resources", - Long: `clean cloudflare dns resources`, RunE: func(cmd *cobra.Command, _ []string) error { opts.quiet = cmd.Flags().Lookup("quiet").Value.String() == "true" return runCloudflare(cmd.Context(), cmd.OutOrStderr(), opts, os.Getenv("CLOUDFLARE_API_TOKEN")) From ab6dbb8c7b76c8df21534f5eddc3f1603dd71d49 Mon Sep 17 00:00:00 2001 From: Patrick D'appollonio <930925+patrickdappollonio@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:42:47 -0500 Subject: [PATCH 9/9] Push updated changes from Thursday. --- internal/cloudflare/client.go | 7 +++++++ internal/cloudflare/dns.go | 33 ++++++++++++++------------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/internal/cloudflare/client.go b/internal/cloudflare/client.go index 7782315..835d668 100644 --- a/internal/cloudflare/client.go +++ b/internal/cloudflare/client.go @@ -20,6 +20,13 @@ type Cloudflare struct { logger *logger.Logger // The logger instance. } +func (c *Cloudflare) getFullName() string { + if c.subdomain != "" { + return fmt.Sprintf("%s.%s", c.subdomain, c.zoneName) + } + return c.zoneName +} + // Option is a function that configures a Cloudflare. type Option func(*Cloudflare) error diff --git a/internal/cloudflare/dns.go b/internal/cloudflare/dns.go index 9870efa..f718fa3 100644 --- a/internal/cloudflare/dns.go +++ b/internal/cloudflare/dns.go @@ -6,10 +6,11 @@ import ( "strings" cloudflarego "github.com/cloudflare/cloudflare-go" + "github.com/konstructio/dropkick/internal/outputwriter" ) func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { - c.logger.Infof("removing dns records for domain %q", c.zoneName) + c.logger.Infof("listing DNS records for %q", c.getFullName()) records, _, err := c.client.ListDNSRecords(ctx, &cloudflarego.ResourceContainer{ Identifier: c.zoneID, @@ -18,22 +19,21 @@ func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { return fmt.Errorf("unable to list records for domain %q: %w", c.zoneName, err) } - filteredRecords := filterRecords(records, c.subdomain) + c.logger.Infof("found %d records for %q", len(records), c.getFullName()) - if c.subdomain != "" { - c.logger.Infof("found %d records for %q", len(filteredRecords), fmt.Sprintf("%s.%s", c.subdomain, c.zoneName)) - } else { - c.logger.Infof("found %d records for %q", len(filteredRecords), c.zoneName) - } + filteredRecords := filterRecords(records, c.getFullName()) + + c.logger.Infof("applied filter %q to %d records, got %d records suitable for deletion", c.getFullName(), len(records), len(filteredRecords)) for _, r := range filteredRecords { if c.nuke { - c.logger.Infof("nuke enabled, deleting record %q - %q", r.Type, r.Name) + c.logger.Infof("deleting %s record %q", r.Type, r.Name) if err := c.client.DeleteDNSRecord(ctx, &cloudflarego.ResourceContainer{Identifier: c.zoneID}, r.ID); err != nil { return fmt.Errorf("unable to delete record %q: %w", r.Name, err) } + outputwriter.WriteStdoutf("deleted %s record %q", r.Type, r.Name) } else { - c.logger.Warnf("nuke disabled, found record %q - %q", r.Type, r.Name) + c.logger.Warnf("refusing to delete %s record %q: nuke is not enabled", r.Type, r.Name) } } @@ -43,21 +43,16 @@ func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error { // fetch all txt records with values like "heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/argo/argo-server" // fetch all txt records with values like "heritage=external-dns,external-dns/owner=default,external-dns/resource" -func filterRecords(records []cloudflarego.DNSRecord, subdomain string) []cloudflarego.DNSRecord { - filteredRecords := make([]cloudflarego.DNSRecord, 0, len(records)) +func filterRecords(records []cloudflarego.DNSRecord, suffix string) []cloudflarego.DNSRecord { aRecord := "A" txtRecord := "TXT" + filteredRecords := make([]cloudflarego.DNSRecord, 0, len(records)) for _, r := range records { - if subdomain != "" { - if strings.Contains(r.Name, subdomain) && (r.Type == txtRecord || r.Type == aRecord) { - filteredRecords = append(filteredRecords, r) - } - } else { - if r.Type == txtRecord || r.Type == aRecord { - filteredRecords = append(filteredRecords, r) - } + if strings.HasSuffix(r.Name, suffix) && (r.Type == txtRecord || r.Type == aRecord) { + filteredRecords = append(filteredRecords, r) } } + return filteredRecords }