Repository: cloudflare/cloudflare-go (go)

Issue:
Secondary DNS support
Please add support for creating/managing secondary zones.

## Expected behavior

Support [creating a secondary zone](https://api.cloudflare.com/#secondary-dns-create-secondary-zone-configuration).

## Current behavior

The part of the API is not supported.  I searched the repo for any issues or PRs, but had no results so I'm adding it here.

## Steps to reproduce (for bugs)

n/a (feature request)

## Context

I would like to manage Cloudflare domains via Terraform.  Since the Cloudflare provider uses this library, it cannot support this type of resource until it is implemented here.

## References

- https://github.com/cloudflare/terraform-provider-cloudflare/issues/605



Pull request description:
Secondary zone support
Introduces support for secondary DNS zones.

API documentation: https://api.cloudflare.com/#secondary-dns-properties

Closes #608

Patch:
diff --git a/secondary_dns_zone.go b/secondary_dns_zone.go
new file mode 100644
index 000000000..034defc9f
--- /dev/null
+++ b/secondary_dns_zone.go
@@ -0,0 +1,172 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+const (
+	errSecondaryDNSInvalidAutoRefreshValue = "secondary DNS auto refresh value is invalid"
+	errSecondaryDNSInvalidZoneName         = "secondary DNS zone name is invalid"
+	errSecondaryDNSInvalidPrimaries        = "secondary DNS primaries value is invalid"
+)
+
+// SecondaryDNSZone contains the high level structure of a secondary DNS zone.
+type SecondaryDNSZone struct {
+	ID                 string    `json:"id,omitempty"`
+	Name               string    `json:"name,omitempty"`
+	Primaries          []string  `json:"primaries,omitempty"`
+	AutoRefreshSeconds int       `json:"auto_refresh_seconds,omitempty"`
+	SoaSerial          int       `json:"soa_serial,omitempty"`
+	CreatedTime        time.Time `json:"created_time,omitempty"`
+	CheckedTime        time.Time `json:"checked_time,omitempty"`
+	ModifiedTime       time.Time `json:"modified_time,omitempty"`
+}
+
+// SecondaryDNSZoneDetailResponse is the API response for a single secondary
+// DNS zone.
+type SecondaryDNSZoneDetailResponse struct {
+	Response
+	Result SecondaryDNSZone `json:"result"`
+}
+
+// SecondaryDNSZoneAXFRResponse is the API response for a single secondary
+// DNS AXFR response.
+type SecondaryDNSZoneAXFRResponse struct {
+	Response
+	Result string `json:"result"`
+}
+
+// GetSecondaryDNSZone returns the secondary DNS zone configuration for a
+// single zone.
+//
+// API reference: https://api.cloudflare.com/#secondary-dns-secondary-zone-configuration-details
+func (api *API) GetSecondaryDNSZone(ctx context.Context, zoneID string) (SecondaryDNSZone, error) {
+	uri := fmt.Sprintf("/zones/%s/secondary_dns", zoneID)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return SecondaryDNSZone{}, err
+	}
+
+	var r SecondaryDNSZoneDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return SecondaryDNSZone{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// CreateSecondaryDNSZone creates a secondary DNS zone.
+//
+// API reference: https://api.cloudflare.com/#secondary-dns-create-secondary-zone-configuration
+func (api *API) CreateSecondaryDNSZone(ctx context.Context, zoneID string, zone SecondaryDNSZone) (SecondaryDNSZone, error) {
+	if err := validateRequiredSecondaryDNSZoneValues(zone); err != nil {
+		return SecondaryDNSZone{}, err
+	}
+
+	uri := fmt.Sprintf("/zones/%s/secondary_dns", zoneID)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri,
+		SecondaryDNSZone{
+			Name:               zone.Name,
+			AutoRefreshSeconds: zone.AutoRefreshSeconds,
+			Primaries:          zone.Primaries,
+		},
+	)
+
+	if err != nil {
+		return SecondaryDNSZone{}, err
+	}
+
+	result := SecondaryDNSZoneDetailResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return SecondaryDNSZone{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result.Result, nil
+}
+
+// UpdateSecondaryDNSZone updates an existing secondary DNS zone.
+//
+// API reference: https://api.cloudflare.com/#secondary-dns-update-secondary-zone-configuration
+func (api *API) UpdateSecondaryDNSZone(ctx context.Context, zoneID string, zone SecondaryDNSZone) (SecondaryDNSZone, error) {
+	if err := validateRequiredSecondaryDNSZoneValues(zone); err != nil {
+		return SecondaryDNSZone{}, err
+	}
+
+	uri := fmt.Sprintf("/zones/%s/secondary_dns", zoneID)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri,
+		SecondaryDNSZone{
+			Name:               zone.Name,
+			AutoRefreshSeconds: zone.AutoRefreshSeconds,
+			Primaries:          zone.Primaries,
+		},
+	)
+
+	if err != nil {
+		return SecondaryDNSZone{}, err
+	}
+
+	result := SecondaryDNSZoneDetailResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return SecondaryDNSZone{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result.Result, nil
+}
+
+// DeleteSecondaryDNSZone deletes a secondary DNS zone.
+//
+// API reference: https://api.cloudflare.com/#secondary-dns-delete-secondary-zone-configuration
+func (api *API) DeleteSecondaryDNSZone(ctx context.Context, zoneID string) error {
+	uri := fmt.Sprintf("/zones/%s/secondary_dns", zoneID)
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ForceSecondaryDNSZoneAXFR requests an immediate AXFR request.
+//
+// API reference: https://api.cloudflare.com/#secondary-dns-update-secondary-zone-configuration
+func (api *API) ForceSecondaryDNSZoneAXFR(ctx context.Context, zoneID string) error {
+	uri := fmt.Sprintf("/zones/%s/secondary_dns/force_axfr", zoneID)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil)
+
+	if err != nil {
+		return err
+	}
+
+	result := SecondaryDNSZoneAXFRResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+
+	return nil
+}
+
+// validateRequiredSecondaryDNSZoneValues ensures that the payload matches the
+// API requirements for required fields.
+func validateRequiredSecondaryDNSZoneValues(zone SecondaryDNSZone) error {
+	if zone.Name == "" {
+		return errors.New(errSecondaryDNSInvalidZoneName)
+	}
+
+	if zone.AutoRefreshSeconds == 0 || zone.AutoRefreshSeconds < 0 {
+		return errors.New(errSecondaryDNSInvalidAutoRefreshValue)
+	}
+
+	if len(zone.Primaries) == 0 {
+		return errors.New(errSecondaryDNSInvalidPrimaries)
+	}
+
+	return nil
+}


Test patch:
diff --git a/secondary_zone_dns_test.go b/secondary_zone_dns_test.go
new file mode 100644
index 000000000..b6b473649
--- /dev/null
+++ b/secondary_zone_dns_test.go
@@ -0,0 +1,249 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetSecondaryDNSZone(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodGet, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "269d8f4853475ca241c4e730be286b20",
+				"name": "www.example.com.",
+				"primaries": [
+					"23ff594956f20c2a721606e94745a8aa",
+					"00920f38ce07c2e2f4df50b1f61d4194"
+				],
+				"auto_refresh_seconds": 86400,
+				"soa_serial": 2019102400,
+				"created_time": "2019-10-24T17:09:42.883908+01:00",
+				"checked_time": "2019-10-24T17:09:42.883908+01:00",
+				"modified_time": "2019-10-24T17:09:42.883908+01:00"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/secondary_dns", handler)
+	createdOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	checkedOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	want := SecondaryDNSZone{
+		ID:   "269d8f4853475ca241c4e730be286b20",
+		Name: "www.example.com.",
+		Primaries: []string{
+			"23ff594956f20c2a721606e94745a8aa",
+			"00920f38ce07c2e2f4df50b1f61d4194",
+		},
+		AutoRefreshSeconds: 86400,
+		SoaSerial:          2019102400,
+		CreatedTime:        createdOn,
+		CheckedTime:        checkedOn,
+		ModifiedTime:       modifiedOn,
+	}
+
+	actual, err := client.GetSecondaryDNSZone(context.Background(), "01a7362d577a6c3019a474fd6f485823")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestCreateSecondaryDNSZone(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodPost, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "269d8f4853475ca241c4e730be286b20",
+				"name": "www.example.com.",
+				"primaries": [
+					"23ff594956f20c2a721606e94745a8aa",
+					"00920f38ce07c2e2f4df50b1f61d4194"
+				],
+				"auto_refresh_seconds": 86400,
+				"soa_serial": 2019102400,
+				"created_time": "2019-10-24T17:09:42.883908+01:00",
+				"checked_time": "2019-10-24T17:09:42.883908+01:00",
+				"modified_time": "2019-10-24T17:09:42.883908+01:00"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/secondary_dns", handler)
+	createdOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	checkedOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	want := SecondaryDNSZone{
+		ID:   "269d8f4853475ca241c4e730be286b20",
+		Name: "www.example.com.",
+		Primaries: []string{
+			"23ff594956f20c2a721606e94745a8aa",
+			"00920f38ce07c2e2f4df50b1f61d4194",
+		},
+		AutoRefreshSeconds: 86400,
+		SoaSerial:          2019102400,
+		CreatedTime:        createdOn,
+		CheckedTime:        checkedOn,
+		ModifiedTime:       modifiedOn,
+	}
+
+	newSecondaryZone := SecondaryDNSZone{
+		Name: "www.example.com.",
+		Primaries: []string{
+			"23ff594956f20c2a721606e94745a8aa",
+			"00920f38ce07c2e2f4df50b1f61d4194",
+		},
+		AutoRefreshSeconds: 86400,
+	}
+
+	actual, err := client.CreateSecondaryDNSZone(context.Background(), "01a7362d577a6c3019a474fd6f485823", newSecondaryZone)
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestUpdateSecondaryDNSZone(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodPut, "Expected method 'PUT', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "269d8f4853475ca241c4e730be286b20",
+				"name": "www.example.com.",
+				"primaries": [
+					"23ff594956f20c2a721606e94745a8aa",
+					"00920f38ce07c2e2f4df50b1f61d4194"
+				],
+				"auto_refresh_seconds": 86400,
+				"soa_serial": 2019102400,
+				"created_time": "2019-10-24T17:09:42.883908+01:00",
+				"checked_time": "2019-10-24T17:09:42.883908+01:00",
+				"modified_time": "2019-10-24T17:09:42.883908+01:00"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/secondary_dns", handler)
+	createdOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	checkedOn, _ := time.Parse(time.RFC3339, "2019-10-24T17:09:42.883908+01:00")
+	want := SecondaryDNSZone{
+		ID:   "269d8f4853475ca241c4e730be286b20",
+		Name: "www.example.com.",
+		Primaries: []string{
+			"23ff594956f20c2a721606e94745a8aa",
+			"00920f38ce07c2e2f4df50b1f61d4194",
+		},
+		AutoRefreshSeconds: 86400,
+		SoaSerial:          2019102400,
+		CreatedTime:        createdOn,
+		CheckedTime:        checkedOn,
+		ModifiedTime:       modifiedOn,
+	}
+
+	updatedZone := SecondaryDNSZone{
+		Name: "www.example.com.",
+		Primaries: []string{
+			"23ff594956f20c2a721606e94745a8aa",
+			"00920f38ce07c2e2f4df50b1f61d4194",
+		},
+		AutoRefreshSeconds: 86400,
+	}
+
+	actual, err := client.UpdateSecondaryDNSZone(context.Background(), "01a7362d577a6c3019a474fd6f485823", updatedZone)
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestDeleteSecondaryDNSZone(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodDelete, "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "269d8f4853475ca241c4e730be286b20"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/secondary_dns", handler)
+
+	err := client.DeleteSecondaryDNSZone(context.Background(), "01a7362d577a6c3019a474fd6f485823")
+	assert.NoError(t, err)
+}
+
+func TestForceSecondaryDNSZoneAXFR(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, r.Method, http.MethodPost, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": "OK"
+		}
+		`)
+	}
+
+	mux.HandleFunc("/zones/01a7362d577a6c3019a474fd6f485823/secondary_dns/force_axfr", handler)
+
+	err := client.ForceSecondaryDNSZoneAXFR(context.Background(), "01a7362d577a6c3019a474fd6f485823")
+	assert.NoError(t, err)
+}
+
+func TestValidateRequiredSecondaryDNSZoneValues(t *testing.T) {
+	z1 := SecondaryDNSZone{}
+	err1 := validateRequiredSecondaryDNSZoneValues(z1)
+	assert.EqualError(t, err1, errSecondaryDNSInvalidZoneName)
+
+	z2 := SecondaryDNSZone{Name: "example.com."}
+	err2 := validateRequiredSecondaryDNSZoneValues(z2)
+	assert.EqualError(t, err2, errSecondaryDNSInvalidAutoRefreshValue)
+
+	z3 := SecondaryDNSZone{Name: "example.com.", AutoRefreshSeconds: 1}
+	err3 := validateRequiredSecondaryDNSZoneValues(z3)
+	assert.EqualError(t, err3, errSecondaryDNSInvalidPrimaries)
+
+	z4 := SecondaryDNSZone{Name: "example.com.", AutoRefreshSeconds: 1, Primaries: []string{"a", "b"}}
+	err4 := validateRequiredSecondaryDNSZoneValues(z4)
+	assert.NoError(t, err4)
+}