From 7146139ae856cdc17be1387559670f974464f3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louren=C3=A7o=20Vales?= <133565059+lourencovales@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:58:09 +0200 Subject: [PATCH] added CheckApply function; made some changes to structure --- engine/resources/cloudflare_dns.go | 152 ++++++++++++++++++++++++++--- 1 file changed, 138 insertions(+), 14 deletions(-) diff --git a/engine/resources/cloudflare_dns.go b/engine/resources/cloudflare_dns.go index a738b736..794a3ca6 100644 --- a/engine/resources/cloudflare_dns.go +++ b/engine/resources/cloudflare_dns.go @@ -33,9 +33,12 @@ import ( "context" "fmt" - "github.com/cloudflare/cloudflare-go/v6" "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/traits" + "github.com/purpleidea/mgmt/util/errwrap" + + "github.com/cloudflare/cloudflare-go/v6" + "github.com/cloudflare/cloudflare-go/v6/zones" ) func init() { @@ -49,19 +52,41 @@ type CloudflareDNSRes struct { APIToken string `lang:"apitoken"` - Name string `lang:"name"` + Comment string `lang:"comment"` - TTL int `lang:"ttl"` + Content string `lang:"content"` + + // using a *int64 here to help with disambiguating nil values + Priority *int64 `lang:"priority"` + + // using a *bool here to help with disambiguating nil values + Proxied *bool `lang:"proxied"` + + Purged bool `lang:"purged"` + + RecordName string `lang:"record_name"` + + State string `lang:"state"` + + TTL int64 `lang:"ttl"` Type string `lang:"type"` Zone string `lang:"zone"` - client *cloudflare.API + client *cloudflare.Client + zoneID string +} + +func (obj *CloudflareDNSRes) Default() engine.Res { + return &CloudflareDNSRes{ + State: "exists", + TTL: 1, // this sets TTL to automatic + } } func (obj *CloudflareDNSRes) Validate() error { - if obj.Name == "" { + if obj.RecordName == "" { return fmt.Errorf("record name is required") } @@ -73,12 +98,24 @@ func (obj *CloudflareDNSRes) Validate() error { return fmt.Errorf("record type is required") } - if obj.TTL < 60 || obj.TTL > 86400 { // API requirement - return fmt.Errorf("TTL must be between 60 and 86400 seconds") + if (obj.TTL < 60 || obj.TTL > 86400) && obj.TTL != 1 { // API requirement + return fmt.Errorf("TTL must be between 60s and 86400s, or set to 1") } if obj.Zone == "" { - return fmt.Errof("zone name is required") + return fmt.Errorf("zone name is required") + } + + if obj.State != "exists" && obj.State != "absent" && obj.State != "" { + return fmt.Errorf("state must be either 'exists', 'absent', or empty") + } + + if obj.State == "exists" && obj.Content == "" && !obj.Purge { + return fmt.Errorf("content is required when state is 'exists'") + } + + if obj.MetaParams().Poll == 0 { + return fmt.Errorf("cloudflare:dns requiers polling, set Meta:poll param (e.g., 60 seconds)") } return nil @@ -87,12 +124,25 @@ func (obj *CloudflareDNSRes) Validate() error { func (obj *CloudflareDNSRes) Init(init *engine.Init) error { obj.init = init - api, err := cloudflare.NewWithAPIToken(obj.APIToken) - if err != nil { - return fmt.Errorf("failed to init Cloudflare API client") - } + obj.client = cloudflare.NewClient( + option.WithAPIToken(obj.APIToken), + ) - obj.client = api + //TODO: does it make more sense to check it here or in CheckApply()? + //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") + //} + + //if len(zoneList.Result) == 0 { + // return fmt.Errorf("zone %s not found", obj.Zone) + //} + + obj.zoneID = zoneList.Results[0].ID return nil } @@ -100,6 +150,7 @@ func (obj *CloudflareDNSRes) Init(init *engine.Init) error { func (obj *CloudflareDNSRes) Cleanup() error { obj.APIToken = "" obj.client = nil + obj.zoneID = "" return nil } @@ -111,7 +162,7 @@ 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{ - Name: cloudflare.F(obj.Zone), + RecordName: cloudflare.F(obj.Zone), }) if err != nil { return false, fmt.Errorf(err) @@ -125,4 +176,77 @@ func (obj *CloudflareDNSRes) CheckApply(ctx context.Context, apply bool) (bool, return false, fmt.Errorf("there's more than one zone with name %s", obj.Zone) } + // We start by checking the need for purging + if obj.Purge { + checkOK, err := obj.purgeCheckApply(ctx, apply) + if err != nil { + return false, err + } + if !checkOK { + return false, nil + } + } + + // List existing records + listParams := dns.RecordListParams{ + ZoneID: cloudflare.F(obj.zoneID), + Name: cloudflare.F(obj.RecordName), + Type: cloudflare.F(dns.RecordListParamsType(obj.Type)), + } + + recordList, err := obj.client.DNS.Records.List(ctx, listParams) + if err != nil { + return false, errwrap.Wrapf(err, "failed to list DNS records") + } + + recordExists := len(records.Result) > 0 + var record dns.Record + if recordExists { + record = recordList.Result[0] + } + + switch obj.State { + case "exists", "": + if !recordExists { + if !apply { + return false, nil + } + + if err := obj.createRecord(ctx); err != nil { + return false, err + } + + return true, nil + } + + if obj.needsUpdate(record) { + if !apply { + return false, nil + } + + if err := obj.updateRecord(ctx, record.ID); err != nil { + return false, err + } + return true, nil + } + + case "absent": + if recordExists { + if !apply { + return false, nil + } + + deleteParams := dns.RecordDeleteParams{ + ZoneID: cloudflare.F(obj.zoneID), + } + + _, err := obj.client.DNS.Reords.Delete(ctx, record.ID, deleteParams) + if err != nil { + return false, errwrap.Wrapf(err, "failed to delete DNS record") + } + return true, nil + } + } + + return true, nil }