Repository: cloudflare/cloudflare-go
Pull request #1304 (2023-06-08 19:00:24)

feat: add account level custom nameservers
## Description

Added support for the APIs documented:

- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-list-account-custom-nameservers
- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-add-account-custom-nameserver
- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-get-eligible-zones-for-account-custom-nameservers
- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-verify-account-custom-nameserver-glue-records
- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-delete-account-custom-nameserver
- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-usage-for-a-zone-get-account-custom-nameserver-related-zone-metadata
- https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-usage-for-a-zone-set-account-custom-nameserver-related-zone-metadata

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.




Patch:
diff --git a/.changelog/1304.txt b/.changelog/1304.txt
new file mode 100644
index 000000000..f3e8693e2
--- /dev/null
+++ b/.changelog/1304.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+custom_nameservers: add support for managing custom nameservers
+```
diff --git a/custom_nameservers.go b/custom_nameservers.go
new file mode 100644
index 000000000..45150a211
--- /dev/null
+++ b/custom_nameservers.go
@@ -0,0 +1,209 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+type CustomNameserverRecord struct {
+	Type  string `json:"type"`
+	Value string `json:"value"`
+}
+
+type CustomNameserver struct {
+	NSName string `json:"ns_name"`
+	NSSet  int    `json:"ns_set"`
+}
+
+type CustomNameserverResult struct {
+	DNSRecords []CustomNameserverRecord `json:"dns_records"`
+	NSName     string                   `json:"ns_name"`
+	NSSet      int                      `json:"ns_set"`
+	Status     string                   `json:"status"`
+	ZoneTag    string                   `json:"zone_tag"`
+}
+
+type CustomNameserverZoneMetadata struct {
+	Type    string `json:"type"`
+	NSSet   string `json:"ns_set"`
+	Enabled bool   `json:"enabled"`
+}
+
+type customNameserverListResponse struct {
+	Response
+	Result []CustomNameserverResult `json:"result"`
+}
+
+type customNameserverCreateResponse struct {
+	Response
+	Result CustomNameserverResult `json:"result"`
+}
+
+type getEligibleZonesAccountCustomNameserversResponse struct {
+	Result []string `json:"result"`
+}
+
+type customNameserverZoneMetadata struct {
+	Response
+	Result CustomNameserverZoneMetadata
+}
+
+type GetCustomNameserversParams struct{}
+
+type CreateCustomNameserversParams struct {
+	NSName string `json:"ns_name"`
+	NSSet  int    `json:"ns_set"`
+}
+
+type DeleteCustomNameserversParams struct {
+	NSName string
+}
+
+type GetEligibleZonesAccountCustomNameserversParams struct{}
+
+type GetCustomNameserverZoneMetadataParams struct{}
+
+type UpdateCustomNameserverZoneMetadataParams struct {
+	Type    string `json:"type"`
+	NSSet   string `json:"ns_set"`
+	Enabled bool   `json:"enabled"`
+}
+
+// GetCustomNameservers lists custom nameservers.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-list-account-custom-nameservers
+func (api *API) GetCustomNameservers(ctx context.Context, rc *ResourceContainer, params GetCustomNameserversParams) ([]CustomNameserverResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return []CustomNameserverResult{}, ErrRequiredAccountLevelResourceContainer
+	}
+	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var response customNameserverListResponse
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return response.Result, nil
+}
+
+// CreateCustomNameservers adds a custom nameserver.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-add-account-custom-nameserver
+func (api *API) CreateCustomNameservers(ctx context.Context, rc *ResourceContainer, params CreateCustomNameserversParams) (CustomNameserverResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return CustomNameserverResult{}, ErrRequiredAccountLevelResourceContainer
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return CustomNameserverResult{}, err
+	}
+
+	response := &customNameserverCreateResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return CustomNameserverResult{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return response.Result, nil
+}
+
+// DeleteCustomNameservers removes a custom nameserver.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-delete-account-custom-nameserver
+func (api *API) DeleteCustomNameservers(ctx context.Context, rc *ResourceContainer, params DeleteCustomNameserversParams) error {
+	if rc.Level != AccountRouteLevel {
+		return ErrRequiredAccountLevelResourceContainer
+	}
+
+	if params.NSName == "" {
+		return errors.New("missing required NSName parameter")
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_ns/%s", rc.Level, rc.Identifier, params.NSName)
+
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// GetEligibleZonesAccountCustomNameservers lists zones eligible for custom nameservers.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-get-eligible-zones-for-account-custom-nameservers
+func (api *API) GetEligibleZonesAccountCustomNameservers(ctx context.Context, rc *ResourceContainer, params GetEligibleZonesAccountCustomNameserversParams) ([]string, error) {
+	if rc.Level != AccountRouteLevel {
+		return []string{}, ErrRequiredAccountLevelResourceContainer
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_ns/availability", rc.Level, rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var response getEligibleZonesAccountCustomNameserversResponse
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return response.Result, nil
+}
+
+// GetCustomNameserverZoneMetadata get metadata for custom nameservers on a zone.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-usage-for-a-zone-get-account-custom-nameserver-related-zone-metadata
+func (api *API) GetCustomNameserverZoneMetadata(ctx context.Context, rc *ResourceContainer, params GetCustomNameserverZoneMetadataParams) (CustomNameserverZoneMetadata, error) {
+	if rc.Level != ZoneRouteLevel {
+		return CustomNameserverZoneMetadata{}, ErrRequiredZoneLevelResourceContainer
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return CustomNameserverZoneMetadata{}, err
+	}
+
+	var response customNameserverZoneMetadata
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return CustomNameserverZoneMetadata{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return response.Result, nil
+}
+
+// UpdateCustomNameserverZoneMetadata set metadata for custom nameservers on a zone.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-usage-for-a-zone-set-account-custom-nameserver-related-zone-metadata
+func (api *API) UpdateCustomNameserverZoneMetadata(ctx context.Context, rc *ResourceContainer, params UpdateCustomNameserverZoneMetadataParams) error {
+	if rc.Level != ZoneRouteLevel {
+		return ErrRequiredZoneLevelResourceContainer
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
+
+	params.Type = ""
+	_, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}


Test patch:
diff --git a/custom_nameservers_test.go b/custom_nameservers_test.go
new file mode 100644
index 000000000..e1b51fa16
--- /dev/null
+++ b/custom_nameservers_test.go
@@ -0,0 +1,225 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAccountCustomNameserver_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"ns_name": "ns1.example.com",
+					"ns_set": 1,
+					"dns_records": [
+						{
+							"type": "A",
+							"value": "192.0.2.1"
+						},
+						{
+							"type": "AAAA",
+							"value": "2400:cb00:2049:1::ffff:ffee"
+						}
+					]
+				},
+				{
+					"ns_name": "ns2.example.com",
+					"ns_set": 1,
+					"dns_records": [
+						{
+							"type": "A",
+							"value": "192.0.2.2"
+						},
+						{
+							"type": "AAAA",
+							"value": "2400:cb00:2049:1::ffff:fffe"
+						}
+					]
+				}
+			]
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/custom_ns", handler)
+	want := []CustomNameserverResult{
+		{
+			DNSRecords: []CustomNameserverRecord{
+				{
+					Type:  "A",
+					Value: "192.0.2.1",
+				},
+				{
+					Type:  "AAAA",
+					Value: "2400:cb00:2049:1::ffff:ffee",
+				},
+			},
+			NSName: "ns1.example.com",
+			NSSet:  1,
+		},
+		{
+			DNSRecords: []CustomNameserverRecord{
+				{
+					Type:  "A",
+					Value: "192.0.2.2",
+				},
+				{
+					Type:  "AAAA",
+					Value: "2400:cb00:2049:1::ffff:fffe",
+				},
+			},
+			NSName: "ns2.example.com",
+			NSSet:  1,
+		},
+	}
+
+	actual, err := client.GetCustomNameservers(context.Background(), AccountIdentifier(testAccountID), GetCustomNameserversParams{})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestAccountCustomNameserver_Create(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"ns_name": "ns1.example.com",
+				"ns_set": 1,
+				"dns_records": [
+					{
+						"type": "A",
+						"value": "192.0.2.1"
+					},
+					{
+						"type": "AAAA",
+						"value": "2400:cb00:2049:1::ffff:ffee"
+					}
+				]
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/custom_ns", handler)
+	want := CustomNameserverResult{
+		DNSRecords: []CustomNameserverRecord{
+			{
+				Type:  "A",
+				Value: "192.0.2.1",
+			},
+			{
+				Type:  "AAAA",
+				Value: "2400:cb00:2049:1::ffff:ffee",
+			},
+		},
+		NSName: "ns1.example.com",
+		NSSet:  1,
+	}
+
+	actual, err := client.CreateCustomNameservers(
+		context.Background(),
+		AccountIdentifier(testAccountID),
+		CreateCustomNameserversParams{
+			NSName: "ns1.example.com",
+			NSSet:  1,
+		},
+	)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestAccountCustomNameserver_GetEligibleZones(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+    "result": [
+        "example.com",
+        "example2.com",
+        "example3.com"
+    ],
+    "success": true,
+    "errors": [],
+    "messages": []
+}`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/custom_ns/availability", handler)
+	want := []string{
+		"example.com",
+		"example2.com",
+		"example3.com",
+	}
+
+	actual, err := client.GetEligibleZonesAccountCustomNameservers(
+		context.Background(),
+		AccountIdentifier(testAccountID),
+		GetEligibleZonesAccountCustomNameserversParams{},
+	)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestAccountCustomNameserver_GetAccountCustomNameserverZoneMetadata(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"result": {
+				"type": "account",
+				"ns_set": "1",
+				"enabled": true
+			},
+			"success": true,
+			"errors": [],
+			"messages": []
+		}`)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/custom_ns", handler)
+	want := CustomNameserverZoneMetadata{
+		Type:    "account",
+		NSSet:   "1",
+		Enabled: true,
+	}
+
+	actual, err := client.GetCustomNameserverZoneMetadata(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		GetCustomNameserverZoneMetadataParams{},
+	)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}