From bce129c9eb05c8fb25359b457ed666c1a2c45288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louren=C3=A7o=20Vales?= <133565059+lourencovales@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:00:35 +0200 Subject: [PATCH] everything is implemented, now on to testing --- engine/resources/cloudflare_dns.go | 206 ++++++++++++++++++++++------- go.mod | 5 + go.sum | 12 ++ 3 files changed, 176 insertions(+), 47 deletions(-) diff --git a/engine/resources/cloudflare_dns.go b/engine/resources/cloudflare_dns.go index bdaf7d51..ccd06370 100644 --- a/engine/resources/cloudflare_dns.go +++ b/engine/resources/cloudflare_dns.go @@ -38,6 +38,8 @@ import ( "github.com/purpleidea/mgmt/util/errwrap" "github.com/cloudflare/cloudflare-go/v6" + "github.com/cloudflare/cloudflare-go/v6/dns" + "github.com/cloudflare/cloudflare-go/v6/option" "github.com/cloudflare/cloudflare-go/v6/zones" ) @@ -48,6 +50,7 @@ func init() { // TODO: description of cloudflare_dns resource type CloudflareDNSRes struct { traits.Base + traits.GraphQueryable init *engine.Init APIToken string `lang:"apitoken"` @@ -62,7 +65,7 @@ type CloudflareDNSRes struct { // using a *bool here to help with disambiguating nil values Proxied *bool `lang:"proxied"` - Purged bool `lang:"purged"` + Purge bool `lang:"purge"` RecordName string `lang:"record_name"` @@ -129,20 +132,20 @@ func (obj *CloudflareDNSRes) Init(init *engine.Init) error { ) //TODO: does it make more sense to check it here or in CheckApply()? - //zoneListParams := zones.ZoneListParams{ - // name: cloudflare.F(obj.Zone), - //} + zoneListParams := zones.ZoneListParams{ + Name: cloudflare.F(obj.Zone), + } - //zoneList, err := obj.client.Zones.List(context.Background(), zoneListParams) - //if err != nil { - // return errwrap.Wrapf(err, "failed to list zones") - //} + zoneList, err := obj.client.Zones.List(context.Background(), zoneListParams) + if err != nil { + return errwrap.Wrapf(err, "failed to list zones") + } - //if len(zoneList.Result) == 0 { - // return fmt.Errorf("zone %s not found", obj.Zone) - //} + if len(zoneList.Result) == 0 { + return fmt.Errorf("zone %s not found", obj.Zone) + } - obj.zoneID = zoneList.Results[0].ID + obj.zoneID = zoneList.Result[0].ID return nil } @@ -162,10 +165,10 @@ func (obj *CloudflareDNSRes) Watch(context.Context) error { func (obj *CloudflareDNSRes) CheckApply(ctx context.Context, apply bool) (bool, error) { zone, err := obj.client.Zones.List(ctx, zones.ZoneListParams{ - RecordName: cloudflare.F(obj.Zone), + Name: cloudflare.F(obj.Zone), }) if err != nil { - return false, fmt.Errorf(err) + return false, err } if len(zone.Result) == 0 { @@ -190,8 +193,10 @@ func (obj *CloudflareDNSRes) CheckApply(ctx context.Context, apply bool) (bool, // List existing records listParams := dns.RecordListParams{ ZoneID: cloudflare.F(obj.zoneID), - Name: cloudflare.F(obj.RecordName), - Type: cloudflare.F(dns.RecordListParamsType(obj.Type)), + Name: cloudflare.F(dns.RecordListParamsName{ + Exact: cloudflare.F(obj.RecordName), // this matches the exact name + }), + Type: cloudflare.F(dns.RecordListParamsType(obj.Type)), } recordList, err := obj.client.DNS.Records.List(ctx, listParams) @@ -199,8 +204,8 @@ func (obj *CloudflareDNSRes) CheckApply(ctx context.Context, apply bool) (bool, return false, errwrap.Wrapf(err, "failed to list DNS records") } - recordExists := len(records.Result) > 0 - var record dns.Record + recordExists := len(recordList.Result) > 0 + var record dns.RecordResponse if recordExists { record = recordList.Result[0] } @@ -240,7 +245,7 @@ func (obj *CloudflareDNSRes) CheckApply(ctx context.Context, apply bool) (bool, ZoneID: cloudflare.F(obj.zoneID), } - _, err := obj.client.DNS.Reords.Delete(ctx, record.ID, deleteParams) + _, err := obj.client.DNS.Records.Delete(ctx, record.ID, deleteParams) if err != nil { return false, errwrap.Wrapf(err, "failed to delete DNS record") } @@ -278,7 +283,7 @@ func (obj *CloudflareDNSRes) Cmp(r engine.Res) error { return fmt.Errorf("record name differs") } - if obj.Purged != res.Purged { + if obj.Purge != res.Purge { return fmt.Errorf("purge value differs") } @@ -314,7 +319,8 @@ func (obj *CloudflareDNSRes) Cmp(r engine.Res) error { return nil } -func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { +// TODO: double check the fields for each record, might have missed some +func (obj *CloudflareDNSRes) buildRecordParam() (any, error) { ttl := dns.TTL(obj.TTL) switch obj.Type { @@ -331,7 +337,7 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "AAAA": param := dns.AAAARecordParam{ @@ -346,7 +352,7 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "CNAME": param := dns.CNAMERecordParam{ @@ -361,7 +367,7 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "MX": param := dns.MXRecordParam{ @@ -374,12 +380,12 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { param.Proxied = cloudflare.F(*obj.Proxied) } if obj.Priority != nil { // required for MX record - param.Priority = cloudflare.F(*obj.Priority) + param.Priority = cloudflare.F(float64(*obj.Priority)) } if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "TXT": param := dns.TXTRecordParam{ @@ -394,7 +400,7 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "NS": param := dns.NSRecordParam{ @@ -409,25 +415,21 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "SRV": param := dns.SRVRecordParam{ - Name: cloudflare.F(obj.RecordName), - Type: cloudflare.F(dns.SRVRecordTypeSRV), - Content: cloudflare.F(obj.Content), - TTL: cloudflare.F(ttl), + Name: cloudflare.F(obj.RecordName), + Type: cloudflare.F(dns.SRVRecordTypeSRV), + TTL: cloudflare.F(ttl), } if obj.Proxied != nil { param.Proxied = cloudflare.F(*obj.Proxied) } - if obj.Priority != nil { - param.Priority = cloudflare.F(*obj.Priority) - } if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil case "PTR": param := dns.PTRRecordParam{ @@ -442,22 +444,46 @@ func (obj *CloudflareDNSRes) buildRecordParam() dns.RecordNewParamsBodyUnion { if obj.Comment != "" { param.Comment = cloudflare.F(obj.Comment) } - return param + return param, nil - default: // we should return something else here, need to investigate + default: + return nil, fmt.Errorf("record type %s is not supported", obj.Type) } } +// buildNewRecordParam creates the appropriate record parameter for creating +// records. +func (obj *CloudflareDNSRes) buildNewRecordParam() (dns.RecordNewParamsBodyUnion, error) { + result, err := obj.buildRecordParam() + if err != nil { + return nil, err + } + return result.(dns.RecordNewParamsBodyUnion), nil +} + +// buildEditRecordParam creates the appropriate record parameter for editing +// records. +func (obj *CloudflareDNSRes) buildEditRecordParam() (dns.RecordEditParamsBodyUnion, error) { + result, err := obj.buildRecordParam() + if err != nil { + return nil, err + } + return result.(dns.RecordEditParamsBodyUnion), nil +} + func (obj *CloudflareDNSRes) createRecord(ctx context.Context) error { - recordParams := obj.buildRecordParam() + recordParams, err := obj.buildNewRecordParam() + if err != nil { + return err + } createParams := dns.RecordNewParams{ ZoneID: cloudflare.F(obj.zoneID), Body: recordParams, } - _, err := obj.client.DNS.Records.New(ctx, createParams) + _, err = obj.client.DNS.Records.New(ctx, createParams) if err != nil { return errwrap.Wrapf(err, "failed to create dns record") } @@ -466,14 +492,17 @@ func (obj *CloudflareDNSRes) createRecord(ctx context.Context) error { } func (obj *CloudflareDNSRes) updateRecord(ctx context.Context, recordID string) error { - recordParams := obj.buildRecordParam() + recordParams, err := obj.buildEditRecordParam() + if err != nil { + return err + } editParams := dns.RecordEditParams{ ZoneID: cloudflare.F(obj.zoneID), Body: recordParams, } - _, err := obj.client.DNS.Records.Edit(ctx, recordID, editParams) + _, err = obj.client.DNS.Records.Edit(ctx, recordID, editParams) if err != nil { return errwrap.Wrapf(err, "failed to update dns record") } @@ -481,7 +510,7 @@ func (obj *CloudflareDNSRes) updateRecord(ctx context.Context, recordID string) return nil } -func (obj *CloudflareDNSRes) needsUpdate(record dns.Record) bool { +func (obj *CloudflareDNSRes) needsUpdate(record dns.RecordResponse) bool { if obj.Content != record.Content { return true } @@ -490,14 +519,14 @@ func (obj *CloudflareDNSRes) needsUpdate(record dns.Record) bool { return true } - if obj.Proxied != nil && record.Proxied != nil { - if *obj.Proxied != *record.Proxied { + if obj.Proxied != nil { + if *obj.Proxied != record.Proxied { return true } } - if obj.Priority != nil && record.Priority != nil { - if *obj.Priority != *record.Priority { + if obj.Priority != nil { + if float64(*obj.Priority) != record.Priority { return true } } @@ -509,4 +538,87 @@ func (obj *CloudflareDNSRes) needsUpdate(record dns.Record) bool { // TODO add more checks? return false + +} + +func (obj *CloudflareDNSRes) purgeCheckApply(ctx context.Context, apply bool) (bool, error) { + listParams := dns.RecordListParams{ + ZoneID: cloudflare.F(obj.zoneID), + } + + iter := obj.client.DNS.Records.ListAutoPaging(ctx, listParams) + + allRecords := []dns.RecordResponse{} + for iter.Next() { + allRecords = append(allRecords, iter.Current()) + } + if err := iter.Err(); err != nil { + return false, errwrap.Wrapf(err, "failed to list dns records for purge") + } + + excludes := make(map[string]bool) + + graph, err := obj.init.FilteredGraph() + if err != nil { + return false, errwrap.Wrapf(err, "can't read the filtered graph") + } + + for _, vertex := range graph.Vertices() { + res, ok := vertex.(engine.Res) + if !ok { + return false, fmt.Errorf("not a resource") + } + if res.Kind() != "cloudflare:dns" { + continue // we only want cloudflare dns resources + } + if res.Name() == obj.Name() { + continue // skip self + } + + cfRes, ok := res.(*CloudflareDNSRes) + if !ok { + return false, fmt.Errorf("wrong resource type") + } + + if cfRes.Zone == obj.Zone { + recordKey := fmt.Sprintf("%s:%s", cfRes.RecordName, cfRes.Type) + excludes[recordKey] = true + } + } + + checkOK := true + + for _, record := range allRecords { + recordKey := fmt.Sprintf("%s:%s", record.Name, record.Type) + + if excludes[recordKey] { + continue + } + + if apply { + deleteParams := dns.RecordDeleteParams{ + ZoneID: cloudflare.F(obj.zoneID), + } + _, err := obj.client.DNS.Records.Delete(ctx, record.ID, deleteParams) + if err != nil { + return false, errwrap.Wrapf(err, "failed to purge %s", recordKey) + } + } else { + checkOK = false + } + } + + return checkOK, nil +} + +// GraphQueryAllowed returns nil if you're allowed to query the graph. This +// function accepts information about the requesting resource so we can +// determine the access with some form of fine-grained control. +func (obj *CloudflareDNSRes) GraphQueryAllowed(opts ...engine.GraphQueryableOption) error { + options := &engine.GraphQueryableOptions{} // default options + options.Apply(opts...) // apply the options + if options.Kind != "cloudflare:dns" { + return fmt.Errorf("only other cloudflare dns resources can access this info") + } + return nil } diff --git a/go.mod b/go.mod index 2fa99c04..8e1b28f4 100644 --- a/go.mod +++ b/go.mod @@ -70,6 +70,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect github.com/cloudflare/circl v1.5.0 // indirect + github.com/cloudflare/cloudflare-go/v6 v6.1.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect @@ -156,6 +157,10 @@ require ( github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.6-0.20250109003754-5ca813443bd2 // indirect github.com/stmcginnis/gofish v0.20.0 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect diff --git a/go.sum b/go.sum index 36a605f4..16d9f209 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/cloudflare-go/v6 v6.1.0 h1:208leV/QEyIZuxFKNk3ztiOh4PeNW/qvLHvzafcbpjI= +github.com/cloudflare/cloudflare-go/v6 v6.1.0/go.mod h1:Lj3MUqjvKctXRpdRhLQxZYRrNZHuRs0XYuH8JtQGyoI= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -496,6 +498,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tredoe/osutil v1.5.0 h1:UGVxbbHRoZi8xXVmbNZ2vgG6XoJ15ndE4LniiQ3rJKg=