cfdns

package module
v0.0.1-beta Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 29, 2023 License: GPL-2.0 Imports: 19 Imported by: 0

README

CFDNS

Go Report Card

About

Non-Official GO CloudFlare DNS API client for go. It was created because the official API is not stable and breaks its consumers multiple times a year. Some of the breaks are immediately apparent because the compiler itself can find the problem, sometimes the expectation can't be detected automatically, while when the returned error is changed, leading to unexpected behavior in code that might be mission-critical.

This library was designed to support only the DNS service.

Project Status

This project is in pre-release stage and backwards compatibility is not guaranteed.

How to Get

go get github.com/simplesurance/cfdns@latest

How to Use

Listing Records

Listing records uses the Iterator pattern to completely abstract the complexity of pagination, while keeping constant memory usage, even when the resulting list is arbitrarily large.

ctx := context.Background()
apitoken := os.Getenv("CFTOKEN")

creds, err := cfdns.APIToken(apitoken)
if err != nil {
	panic(err)
}

client := cfdns.NewClient(creds)

iter := client.ListZones(&cfdns.ListZonesRequest{})
for {
	zone, err := iter.Next(ctx)
	if err != nil {
		if errors.Is(err, io.EOF) {
			break
		}

		panic(err)
	}

	fmt.Printf("Found zone %s\n", zone.Name)
}
Create and Delete a DNS Record

All methods that do not return a list receive a context and a request struct and return a struct and an error.

ctx := context.Background()
apitoken := os.Getenv("TEST_CF_APITOKEN")
testZoneID := os.Getenv("TEST_CF_ZONE_ID")

creds, err := cfdns.APIToken(apitoken)
if err != nil {
	panic(err)
}

client := cfdns.NewClient(creds)

resp, err := client.CreateRecord(ctx, &cfdns.CreateRecordRequest{
	ZoneID:  testZoneID,
	Name:    "example-record",
	Type:    "CNAME",
	Content: "github.com",
	Proxied: false,
	Comment: "Created by cfdns example",
	TTL:     time.Duration(30 * time.Minute),
})
if err != nil {
	panic(err)
}

fmt.Printf("Created DNS record %s", resp.Name)

// cleanup
_, _ = client.DeleteRecord(ctx, &cfdns.DeleteRecordRequest{
	ZoneID:   testZoneID,
	RecordID: resp.ID,
})

// Output: Created DNS record example-record.simplesurance.top

Error Handling

Rules for errors returned are as follows:

  1. If the error response came from sending the HTTP response to the CloudFlare, even if it is an invalid response, the error is HTTPError;
  2. If the response is a valid CloudFlare API response, then the error is also a CloudFlareError (so it is both HTTPError and CloudFlareError);
  3. If not valid HTTP response could be obtained from the server, then some other error is returned.

In all cases, the caller MUST use errors.As() to get either the HTTPError or CloudFlareError object.

HTTPError

All errors that result from calling the CloudFlare REST API allow reading the HTTP response that caused it.

ctx := context.Background()
apitoken := os.Getenv("TEST_CF_APITOKEN")
testZoneID := os.Getenv("TEST_CF_ZONE_ID")

creds, err := cfdns.APIToken(apitoken)
if err != nil {
	panic(err)
}

client := cfdns.NewClient(creds)

_, err = client.CreateRecord(ctx, &cfdns.CreateRecordRequest{
	ZoneID:  testZoneID,
	Name:    "invalid name",
	Type:    "A",
	Content: "github.com",
	Comment: "Created by cfdns example",
	TTL:     30 * time.Minute,
})

httpErr := cfdns.HTTPError{}
if !errors.As(err, &httpErr) {
	panic("not an HTTP error")
}

fmt.Printf("Got HTTP error %v", httpErr.Code) // can also access response headers and raw response body

// Output: Got HTTP error 400
CloudFlareError
ctx := context.Background()
apitoken := os.Getenv("TEST_CF_APITOKEN")
testZoneID := os.Getenv("TEST_CF_ZONE_ID")

creds, err := cfdns.APIToken(apitoken)
if err != nil {
	panic(err)
}

client := cfdns.NewClient(creds)

_, err = client.CreateRecord(ctx, &cfdns.CreateRecordRequest{
	ZoneID:  testZoneID,
	Name:    "invalid name",
	Type:    "A",
	Content: "github.com",
	Comment: "Created by cfdns example",
	TTL:     30 * time.Minute,
})

cfErr := cfdns.CloudFlareError{}
if !errors.As(err, &cfErr) {
	panic("not a CloudFlareError")
}

fmt.Printf("Got HTTP error %v\n", cfErr.HTTPError.Code) // can also access response headers and raw response body

for _, cfe := range cfErr.Errors {
	fmt.Printf("- CF error %d: %s\n", cfe.Code, cfe.Message) // can also access response headers and raw response body
}

// Output:
// Got HTTP error 400
// - CF error 1004: DNS Validation Error

Documentation

Overview

Package cfdns is a non-official GO CloudFlare DNS API client for go.

All requests sent by this library use configurable exponential back-off for retrying failed requests and implements a soft-limiter to avoid exhausting CloudFlare's client request quota.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrEmptyToken = errors.New("Provided token is empty")

ErrEmptyToken is returned when the credentials generator produces an empty authentication token.

Functions

func ReadAll

func ReadAll[T any](ctx context.Context, it *Iterator[T]) ([]*T, error)

ReadAll is an utility function that reads all elements from an iterator and return them as an array.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(creds Credentials, options ...Option) *Client

func (*Client) CreateRecord

func (c *Client) CreateRecord(
	ctx context.Context,
	req *CreateRecordRequest,
) (*CreateRecordResponse, error)

CreateRecord creates a DNS record on CloudFlare. A TTL of 1 second or less will use CloudFlare's "automatic" TTL.

API Reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-create-dns-record

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/simplesurance/cfdns"
)

func main() {
	ctx := context.Background()
	apitoken := os.Getenv("TEST_CF_APITOKEN")
	testZoneID := os.Getenv("TEST_CF_ZONE_ID")

	creds, err := cfdns.APIToken(apitoken)
	if err != nil {
		panic(err)
	}

	client := cfdns.NewClient(creds)

	resp, err := client.CreateRecord(ctx, &cfdns.CreateRecordRequest{
		ZoneID:  testZoneID,
		Name:    "example-record",
		Type:    "CNAME",
		Content: "github.com",
		Proxied: false,
		Comment: "Created by cfdns example",
		TTL:     30 * time.Minute,
	})
	if err != nil {
		panic(err)
	}

	fmt.Printf("Created DNS record %s", resp.Name)

	// cleanup
	_, _ = client.DeleteRecord(ctx, &cfdns.DeleteRecordRequest{
		ZoneID:   testZoneID,
		RecordID: resp.ID,
	})

}
Output:

Created DNS record example-record.simplesurance.top

func (*Client) DeleteRecord

func (c *Client) DeleteRecord(
	ctx context.Context,
	req *DeleteRecordRequest,
) (*DeleteRecordResponse, error)

DeleteRecord deletes a DNS record on CloudFlare.

API Reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-delete-dns-record

func (*Client) ListRecords

func (c *Client) ListRecords(
	req *ListRecordsRequest,
) *Iterator[ListRecordsResponseItem]

ListRecords lists DNS records on a zone.

API Reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-list-dns-records

func (*Client) ListZones

func (c *Client) ListZones(
	_ *ListZonesRequest,
) *Iterator[ListZonesResponseItem]

Listzones lists zones on CloudFlare.

API Reference: https://developers.cloudflare.com/api/operations/zones-get

Example
package main

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

	"github.com/simplesurance/cfdns"
)

func main() {
	ctx := context.Background()
	apitoken := os.Getenv("TEST_CF_APITOKEN")

	creds, err := cfdns.APIToken(apitoken)
	if err != nil {
		panic(err)
	}

	client := cfdns.NewClient(creds)

	iter := client.ListZones(&cfdns.ListZonesRequest{})
	for {
		zone, err := iter.Next(ctx)
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}

			panic(err)
		}

		fmt.Printf("Found zone %s\n", zone.Name)
	}

}
Output:

Found zone simplesurance.top

func (*Client) UpdateRecord

func (c *Client) UpdateRecord(
	ctx context.Context,
	req *UpdateRecordRequest,
) (*UpdateRecordResponse, error)

UpdateRecord updates a DNS record on CloudFlare. A TTL of 1 second or less will use the "automatic" TTL from CloudFlare.

API Reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-update-dns-record

type CloudFlareError

type CloudFlareError struct {
	HTTPError HTTPError
	// contains filtered or unexported fields
}
Example
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/simplesurance/cfdns"
)

func main() {
	ctx := context.Background()
	apitoken := os.Getenv("TEST_CF_APITOKEN")
	testZoneID := os.Getenv("TEST_CF_ZONE_ID")

	creds, err := cfdns.APIToken(apitoken)
	if err != nil {
		panic(err)
	}

	client := cfdns.NewClient(creds)

	_, err = client.CreateRecord(ctx, &cfdns.CreateRecordRequest{
		ZoneID:  testZoneID,
		Name:    "invalid name",
		Type:    "A",
		Content: "github.com",
		Comment: "Created by cfdns example",
		TTL:     30 * time.Minute,
	})

	cfErr := cfdns.CloudFlareError{}
	if !errors.As(err, &cfErr) {
		panic("not a CloudFlareError")
	}

	fmt.Printf("Got HTTP error %v\n", cfErr.HTTPError.Code) // can also access response headers and raw response body

	for _, cfe := range cfErr.Errors {
		fmt.Printf("- CF error %d: %s\n", cfe.Code, cfe.Message) // can also access response headers and raw response body
	}

}
Output:

Got HTTP error 400
- CF error 1004: DNS Validation Error

func (CloudFlareError) Error

func (ce CloudFlareError) Error() string

func (*CloudFlareError) IsAnyCFErrorCode

func (rc *CloudFlareError) IsAnyCFErrorCode(code ...int) bool

IsAnyCFErrorCode returns true if the CloudFlare error includes any of the provided codes.

func (CloudFlareError) Unwrap

func (ce CloudFlareError) Unwrap() error

type CreateRecordRequest

type CreateRecordRequest struct {
	ZoneID  string
	Name    string
	Type    string
	Content string
	Proxied bool
	Tags    []string
	Comment string
	TTL     time.Duration
}

type CreateRecordResponse

type CreateRecordResponse struct {
	ID   string
	Name string
}

type Credentials

type Credentials interface {
	// contains filtered or unexported methods
}

func APIToken

func APIToken(token string) (Credentials, error)

type DeleteRecordRequest

type DeleteRecordRequest struct {
	ZoneID   string
	RecordID string
}

type DeleteRecordResponse

type DeleteRecordResponse struct{}

type HTTPError

type HTTPError struct {
	Code    int
	RawBody []byte
	Headers http.Header
}
Example
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/simplesurance/cfdns"
)

func main() {
	ctx := context.Background()
	apitoken := os.Getenv("TEST_CF_APITOKEN")
	testZoneID := os.Getenv("TEST_CF_ZONE_ID")

	creds, err := cfdns.APIToken(apitoken)
	if err != nil {
		panic(err)
	}

	client := cfdns.NewClient(creds)

	_, err = client.CreateRecord(ctx, &cfdns.CreateRecordRequest{
		ZoneID:  testZoneID,
		Name:    "invalid name",
		Type:    "A",
		Content: "github.com",
		Comment: "Created by cfdns example",
		TTL:     30 * time.Minute,
	})

	httpErr := cfdns.HTTPError{}
	if !errors.As(err, &httpErr) {
		panic("not an HTTP error")
	}

	fmt.Printf("Got HTTP error %v", httpErr.Code) // can also access response headers and raw response body

}
Output:

Got HTTP error 400

func (HTTPError) Error

func (e HTTPError) Error() string

func (HTTPError) IsPermanent

func (e HTTPError) IsPermanent() bool

IsPermanent returns true if should not try again the same request.

type Iterator

type Iterator[T any] struct {
	// contains filtered or unexported fields
}

Iterator implements an iterator algorithm from a function that fetches blocks of data. This allows having fixed-memory usage when reading arbitrary-sized structs without leaking implementation details about how to paginate consecutive blocks of data.

func (*Iterator[T]) Next

func (it *Iterator[T]) Next(ctx context.Context) (retElm *T, err error)

Next fetches the next item. If there are no more records io.EOF will be returned.

type ListRecordsRequest

type ListRecordsRequest struct {
	ZoneID string
	Name   string // Name is used to filter by name.
	Type   string // Type is used to filter by type.
}

type ListRecordsResponseItem

type ListRecordsResponseItem struct {
	ID      string
	Content string
	Name    string
	Type    string
	Proxied bool
	Comment string
	TTL     time.Duration
}

type ListZonesRequest

type ListZonesRequest struct{}

type ListZonesResponseItem

type ListZonesResponseItem struct {
	ID   string
	Name string
}

type Option

type Option func(*settings)

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

func WithLogSuccessfulResponses

func WithLogSuccessfulResponses(enable bool) Option

WithLogSuccessfulResponses allows logging full request and response send to CloudFlare in case of successful response. Debug log must also be enabled.

Error responses will always be logged if debug log is enabled.

func WithLogger

func WithLogger(logger *log.Logger) Option

func WithRateLimiter

func WithRateLimiter(ratelim *rate.Limiter) Option

func WithRequestTimeout

func WithRequestTimeout(timeout time.Duration) Option

WithRequestTimeout configures how long to wait for an HTTP request. The default is 1 minute. Setting a value of 0 will make it use the default behavior of the HTTP client being used, that might be waiting forever.

type UpdateRecordRequest

type UpdateRecordRequest struct {
	ZoneID   string
	RecordID string
	Name     string
	Type     string
	Content  string
	Proxied  bool
	Tags     []string
	Comment  string
	TTL      time.Duration
}

type UpdateRecordResponse

type UpdateRecordResponse struct {
	ModifiedOn time.Time
}

Directories

Path Synopsis
log
niltarget
Package niltarget is a log driver that does nothing, dropping all messages.
Package niltarget is a log driver that does nothing, dropping all messages.
testtarget
Package testtarget is a log driver that sends log messages to a go test.
Package testtarget is a log driver that sends log messages to a go test.
texttarget
Package texttarget is a log driver that encodes log messages as text and writes log messages to a provided writer.
Package texttarget is a log driver that encodes log messages as text and writes log messages to a provided writer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL