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

adds initial cloudflare command to delete dns records #25

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
76 changes: 76 additions & 0 deletions cmd/cloudflare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
"os"

"github.com/konstructio/dropkick/internal/cloudflare"
"github.com/konstructio/dropkick/internal/logger"
"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",
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")

cloudflareCmd.MarkFlagRequired("domain")

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(
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
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ 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
github.com/spf13/cobra v1.8.0
)

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
Expand All @@ -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
Expand Down
18 changes: 12 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -269,20 +273,22 @@ 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=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
Expand Down
107 changes: 107 additions & 0 deletions internal/cloudflare/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package cloudflare

import (
"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.
}

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

// 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 is not set, or if it fails to
// create the underlying Cloudflare API client.
func New(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)
}

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 {
c.logger = logger.None
}

return c, nil
}
58 changes: 58 additions & 0 deletions internal/cloudflare/dns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cloudflare

import (
"context"
"fmt"
"strings"

cloudflarego "github.com/cloudflare/cloudflare-go"
"github.com/konstructio/dropkick/internal/outputwriter"
)

func (c *Cloudflare) NukeDNSRecords(ctx context.Context) error {
c.logger.Infof("listing DNS records for %q", c.getFullName())

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)
}

c.logger.Infof("found %d records for %q", len(records), c.getFullName())

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("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("refusing to delete %s record %q: nuke is not enabled", r.Type, r.Name)
}
}

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, suffix string) []cloudflarego.DNSRecord {
aRecord := "A"
txtRecord := "TXT"

filteredRecords := make([]cloudflarego.DNSRecord, 0, len(records))
for _, r := range records {
if strings.HasSuffix(r.Name, suffix) && (r.Type == txtRecord || r.Type == aRecord) {
filteredRecords = append(filteredRecords, r)
}
}

return filteredRecords
}
Loading
Loading