diff --git a/engine/resources/cloudflare_dns.go b/engine/resources/cloudflare_dns.go index 06637188..f6adf302 100644 --- a/engine/resources/cloudflare_dns.go +++ b/engine/resources/cloudflare_dns.go @@ -47,40 +47,74 @@ func init() { engine.RegisterResource("cloudflare:dns", func() engine.Res { return &CloudflareDNSRes{} }) } -// TODO: description of cloudflare_dns resource +// CloudflareDNSRes is a resource for managing DNS records in Cloudflare zones. +// This resource uses the Cloudflare API to create, update, and delete DNS +// records in a specified zone. It supports various record types including A, +// AAAA, CNAME, MX, TXT, NS, SRV, and PTR records. The resource requires polling +// to detect changes, as the Cloudflare API does not provide an event stream. +// The Purge functionality allows enforcing that only managed DNS records exist +// in the zone, removing any unmanaged records. type CloudflareDNSRes struct { traits.Base traits.GraphQueryable init *engine.Init + // APIToken is the Cloudflare API token used for authentication. This is + // required and must have the necessary permissions to manage DNS records + // in the specified zone. APIToken string `lang:"apitoken"` + // Comment is an optional comment to attach to the DNS record. This is + // stored in Cloudflare and can be used for documentation purposes. Comment string `lang:"comment"` + // Content is the value for the DNS record. This is required when State + // is "exists" unless Purge is true. The format depends on the record + // Type (e.g., IP address for A records, hostname for CNAME records). Content string `lang:"content"` - // using a *int64 here to help with disambiguating nil values + // Priority is the priority value for records that support it (e.g., MX + // records). This is a pointer to distinguish between an unset value and + // a zero value. Priority *int64 `lang:"priority"` - // using a *bool here to help with disambiguating nil values + // Proxied specifies whether the record should be proxied through + // Cloudflare's CDN. This is a pointer to distinguish between an unset + // value and false. Only applicable to certain record types. Proxied *bool `lang:"proxied"` + // Purge specifies whether to delete all DNS records in the zone that are + // not defined in the mgmt graph. When true, this resource will query the + // graph for other cloudflare:dns resources in the same zone and delete + // any records not managed by those resources. Purge bool `lang:"purge"` + // RecordName is the name of the DNS record (e.g., "www.example.com" or + // "@" for the zone apex). This is required. RecordName string `lang:"record_name"` + // State determines whether the DNS record should exist or be absent. + // Valid values are "exists" (default) or "absent". When set to "absent", + // the record will be deleted if it exists. State string `lang:"state"` + // TTL is the time-to-live value for the DNS record in seconds. Must be + // between 60 and 86400, or set to 1 for automatic TTL. Default is 1. TTL int64 `lang:"ttl"` + // Type is the DNS record type (e.g., "A", "AAAA", "CNAME", "MX", "TXT", + // "NS", "SRV", "PTR"). This is required. Type string `lang:"type"` + // Zone is the name of the Cloudflare zone (domain) where the DNS record + // should be managed (e.g., "example.com"). This is required. Zone string `lang:"zone"` client *cloudflare.Client zoneID string } +// Default returns some sensible defaults for this resource. func (obj *CloudflareDNSRes) Default() engine.Res { return &CloudflareDNSRes{ State: "exists", @@ -88,6 +122,7 @@ func (obj *CloudflareDNSRes) Default() engine.Res { } } +// Validate checks if the resource data structure was populated correctly. func (obj *CloudflareDNSRes) Validate() error { if obj.RecordName == "" { return fmt.Errorf("record name is required") @@ -117,13 +152,15 @@ func (obj *CloudflareDNSRes) Validate() error { return fmt.Errorf("content is required when state is 'exists'") } - if obj.MetaParams().Poll == 0 || obj.MetaParams().Poll < 1 { + if obj.MetaParams().Poll == 0 || obj.MetaParams().Poll < 1 { // CF accepts ~4req/s so this is good enough return fmt.Errorf("cloudflare:dns requires polling, set Meta:poll param (e.g., 60 seconds), min. 1s") } return nil } +// Init runs some startup code for this resource. It initializes the Cloudflare +// API client and validates that the specified zone exists. func (obj *CloudflareDNSRes) Init(init *engine.Init) error { obj.init = init @@ -150,6 +187,8 @@ func (obj *CloudflareDNSRes) Init(init *engine.Init) error { return nil } +// Cleanup is run by the engine to clean up after the resource is done. It +// clears sensitive data and releases the API client connection. func (obj *CloudflareDNSRes) Cleanup() error { obj.APIToken = "" obj.client = nil @@ -163,6 +202,10 @@ func (obj *CloudflareDNSRes) Watch(context.Context) error { return fmt.Errorf("invalid Watch call: requires poll metaparam") } +// CheckApply is the main convergence function for this resource. It checks the +// current state of the DNS record against the desired state and applies changes +// if necessary. If apply is false, it only checks if changes are needed. If +// Purge is enabled, it will first delete any unmanaged records in the zone. 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), @@ -256,6 +299,8 @@ func (obj *CloudflareDNSRes) CheckApply(ctx context.Context, apply bool) (bool, return true, nil } +// Cmp compares two resources and returns an error if they differ. This is used +// to determine if two resources are equivalent for graph operations. func (obj *CloudflareDNSRes) Cmp(r engine.Res) error { if obj == nil && r == nil { return nil @@ -274,7 +319,6 @@ func (obj *CloudflareDNSRes) Cmp(r engine.Res) error { return fmt.Errorf("apitoken differs") } - // check how this being a pointer influences this check if obj.Proxied != res.Proxied { return fmt.Errorf("proxied values differ") } @@ -311,7 +355,6 @@ func (obj *CloudflareDNSRes) Cmp(r engine.Res) error { return fmt.Errorf("content param differs") } - // check how this being a pointer influences this check if obj.Priority != res.Priority { return fmt.Errorf("the priority param differs") } @@ -319,6 +362,9 @@ func (obj *CloudflareDNSRes) Cmp(r engine.Res) error { return nil } +// buildRecordParam creates the appropriate record parameter structure based on +// the record type. This is a helper function used by buildNewRecordParam and +// buildEditRecordParam. // TODO: double check the fields for each record, might have missed some func (obj *CloudflareDNSRes) buildRecordParam() (any, error) { ttl := dns.TTL(obj.TTL) @@ -452,7 +498,7 @@ func (obj *CloudflareDNSRes) buildRecordParam() (any, error) { } } -// buildNewRecordParam creates the appropriate record parameter for creating +// buildNewRecordParam creates the appropriate record parameter for creating new // records. func (obj *CloudflareDNSRes) buildNewRecordParam() (dns.RecordNewParamsBodyUnion, error) { result, err := obj.buildRecordParam() @@ -472,6 +518,8 @@ func (obj *CloudflareDNSRes) buildEditRecordParam() (dns.RecordEditParamsBodyUni return result.(dns.RecordEditParamsBodyUnion), nil } +// createRecord creates a new DNS record in Cloudflare using the resource's +// parameters. func (obj *CloudflareDNSRes) createRecord(ctx context.Context) error { recordParams, err := obj.buildNewRecordParam() if err != nil { @@ -491,6 +539,8 @@ func (obj *CloudflareDNSRes) createRecord(ctx context.Context) error { return nil } +// updateRecord updates an existing DNS record in Cloudflare with the resource's +// parameters. func (obj *CloudflareDNSRes) updateRecord(ctx context.Context, recordID string) error { recordParams, err := obj.buildEditRecordParam() if err != nil { @@ -510,6 +560,8 @@ func (obj *CloudflareDNSRes) updateRecord(ctx context.Context, recordID string) return nil } +// needsUpdate compares the current DNS record with the desired state and +// returns true if an update is needed. func (obj *CloudflareDNSRes) needsUpdate(record dns.RecordResponse) bool { if obj.Content != record.Content { return true @@ -541,6 +593,10 @@ func (obj *CloudflareDNSRes) needsUpdate(record dns.RecordResponse) bool { } +// purgeCheckApply deletes all DNS records in the zone that are not defined in +// the mgmt graph. It queries the graph for other cloudflare:dns resources in +// the same zone and builds an exclusion list. If apply is false, it only checks +// if purge is needed. func (obj *CloudflareDNSRes) purgeCheckApply(ctx context.Context, apply bool) (bool, error) { listParams := dns.RecordListParams{ ZoneID: cloudflare.F(obj.zoneID),