Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Breaking:

### Enhancements:
- feat(dns): add support for DNS Zones and TSIG Keys ([#805](https://github.com/fastly/go-fastly/pull/805))

### Dependencies:
- build(deps): `golang.org/x/crypto` from 0.49.0 to 0.50.0 ([#804](https://github.com/fastly/go-fastly/pull/804))
Expand Down
3 changes: 3 additions & 0 deletions fastly/dns/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package dns contains subpackages that offer maintenance around Fastly
// Domain Name System (DNS) zones and TSIG (Transaction Signature) keys.
package dns
44 changes: 44 additions & 0 deletions fastly/dns/v1/dnszones/api_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dnszones

import (
"context"
"encoding/json"
"fmt"

"github.com/fastly/go-fastly/v14/fastly"
)

// CreateInput specifies the information needed to create a zone.
type CreateInput struct {
// Name is the domain name for the DNS zone (required).
Name *string `json:"name"`
// Type is the type of the DNS zone (required).
Type *string `json:"type"`
// Description is a freeform descriptive note.
Description *string `json:"description,omitempty"`
// XfrConfigInbound contains attributes associated with inbound zone transfers.
XfrConfigInbound *XfrConfigInboundInput `json:"xfr_config_inbound,omitempty"`
}

// Create creates a new DNS Zone.
func Create(ctx context.Context, c *fastly.Client, i *CreateInput) (*Zone, error) {
if i.Name == nil {
return nil, fastly.ErrMissingName
}
if i.Type == nil {
return nil, fastly.ErrMissingType
}

resp, err := c.PostJSON(ctx, "/dns/v1/zones", i, fastly.CreateRequestOptions())
if err != nil {
return nil, err
}
defer resp.Body.Close()

var zone *Zone
if err := json.NewDecoder(resp.Body).Decode(&zone); err != nil {
return nil, fmt.Errorf("failed to decode json response: %w", err)
}

return zone, nil
}
31 changes: 31 additions & 0 deletions fastly/dns/v1/dnszones/api_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dnszones

import (
"context"

"github.com/fastly/go-fastly/v14/fastly"
)

// DeleteInput specifies the information needed for the Delete() function to
// perform the operation.
type DeleteInput struct {
// ZoneID is the Zone Identifier (UUID) (required).
ZoneID *string `json:"-"`
}

// Delete deletes a specified DNS Zone.
func Delete(ctx context.Context, c *fastly.Client, i *DeleteInput) error {
if i.ZoneID == nil {
return fastly.ErrMissingID
}

path := fastly.ToSafeURL("dns", "v1", "zones", *i.ZoneID)

resp, err := c.Delete(ctx, path, fastly.CreateRequestOptions())
if err != nil {
return err
}
defer resp.Body.Close()

return nil
}
37 changes: 37 additions & 0 deletions fastly/dns/v1/dnszones/api_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dnszones

import (
"context"
"encoding/json"
"fmt"

"github.com/fastly/go-fastly/v14/fastly"
)

// GetInput specifies the information needed to get a zone.
type GetInput struct {
// ZoneID is the Zone Identifier (UUID) (required).
ZoneID *string `json:"-"`
}

// Get retrieves a specified DNS Zone.
func Get(ctx context.Context, c *fastly.Client, i *GetInput) (*Zone, error) {
if i.ZoneID == nil {
return nil, fastly.ErrMissingID
}

path := fastly.ToSafeURL("dns", "v1", "zones", *i.ZoneID)

resp, err := c.GetJSON(ctx, path, fastly.CreateRequestOptions())
if err != nil {
return nil, err
}
defer resp.Body.Close()

var zone *Zone
if err := json.NewDecoder(resp.Body).Decode(&zone); err != nil {
return nil, fmt.Errorf("failed to decode json response: %w", err)
}

return zone, nil
}
56 changes: 56 additions & 0 deletions fastly/dns/v1/dnszones/api_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dnszones

import (
"context"
"encoding/json"
"fmt"
"strconv"

"github.com/fastly/go-fastly/v14/fastly"
)

// ListInput specifies the information needed to list zones.
type ListInput struct {
// Cursor is the value from the next_cursor field of a previous
// response, used to retrieve the next page. To request the first
// page, this should be empty.
Cursor *string
Comment thread
kpfleming marked this conversation as resolved.
Outdated
// Limit is how many results are returned.
Limit *int
// Name filters the list to return only zones that contain the provided name.
Name *string
// Sort is the order in which to list the results.
Sort *string
}

// List retrieves a paginated list of DNS Zones.
func List(ctx context.Context, c *fastly.Client, i *ListInput) (*Zones, error) {
path := fastly.ToSafeURL("dns", "v1", "zones")

requestOptions := fastly.CreateRequestOptions()
if i.Cursor != nil {
requestOptions.Params["cursor"] = *i.Cursor
}
if i.Limit != nil {
requestOptions.Params["limit"] = strconv.Itoa(*i.Limit)
}
if i.Name != nil {
requestOptions.Params["name"] = *i.Name
}
if i.Sort != nil {
requestOptions.Params["sort"] = *i.Sort
}

resp, err := c.GetJSON(ctx, path, requestOptions)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var zones *Zones
if err := json.NewDecoder(resp.Body).Decode(&zones); err != nil {
return nil, fmt.Errorf("failed to decode json response: %w", err)
}

return zones, nil
}
75 changes: 75 additions & 0 deletions fastly/dns/v1/dnszones/api_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dnszones

// Zone is the API response structure for the create, update and get operations.
type Zone struct {
// ID is the zone identifier (UUID).
ID *string `json:"id"`
// Name is the domain name for your zone.
Name *string `json:"name"`
// Description is a freeform descriptive note.
Description *string `json:"description"`
// Type is the type of the zone.
Comment thread
jedisct1 marked this conversation as resolved.
Type *string `json:"type"`
// Serial is serial number of the zone's SOA record.
Serial *string `json:"serial"`
// Nameservers is an array of the nameserver hostnames assigned to the zone.
Nameservers []string `json:"nameservers"`
// XfrConfigInbound contains all attributes associated with inbound zone transfers.
XfrConfigInbound *XfrConfigInbound `json:"xfr_config_inbound"`
// CreatedAt is the date and time the zone was created.
CreatedAt *string `json:"created_at"`
// UpdatedAt is the date and time the zone was last updated.
UpdatedAt *string `json:"updated_at"`
}

// XfrConfigInbound contains all attributes associated with inbound zone transfers.
type XfrConfigInbound struct {
// Primaries is an array of the primary DNS server objects associated with inbound zone transfers.
Primaries []Primary `json:"primaries"`
// NotifyIPAddresses are IP addresses where Primary DNS servers can send NOTIFY messages.
NotifyIPAddresses *NotifyIPAddresses `json:"notify_ip_addresses"`
// InboundTSIGKeyID is the ID of the TSIG key used to secure inbound zone transfers.
InboundTSIGKeyID *string `json:"inbound_tsig_key_id"`
}

// XfrConfigInboundInput specifies the inbound zone transfer attributes for create and update requests.
type XfrConfigInboundInput struct {
// Primaries is an array of primary DNS server objects for inbound zone transfers.
Primaries []Primary `json:"primaries,omitempty"`
Comment thread
jedisct1 marked this conversation as resolved.
// InboundTSIGKeyID is the ID of the TSIG key used to secure inbound zone transfers.
InboundTSIGKeyID *string `json:"inbound_tsig_key_id,omitempty"`
Comment thread
jedisct1 marked this conversation as resolved.
Outdated
}

// Primary represents a primary DNS server for inbound zone transfers.
type Primary struct {
// Address is an IPv4 address for the Primary DNS Server.
Address *string `json:"address"`
// Description is a description of the Primary DNS server.
Description *string `json:"description"`
}

// NotifyIPAddresses contains IP addresses where primary DNS servers can send NOTIFY messages.
type NotifyIPAddresses struct {
// IPv4 are IPv4 addresses where Primary DNS servers can send NOTIFY messages.
IPv4 []string `json:"ipv4"`
}

// Zones is the paginated API response for listing zones.
type Zones struct {
// Data is the list of zones.
Data []Zone `json:"data"`
// Meta contains pagination metadata.
Meta MetaZones `json:"meta"`
}

// MetaZones is a subset of the Zone response structure.
type MetaZones struct {
// NextCursor is the cursor value to use in the next request to retrieve the next page.
NextCursor *string `json:"next_cursor"`
// Limit is the maximum number of results returned.
Limit *int `json:"limit"`
// Sort is the order in which the results are listed.
Sort *string `json:"sort"`
// Total is the total number of zones.
Total *int `json:"total"`
}
134 changes: 134 additions & 0 deletions fastly/dns/v1/dnszones/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package dnszones

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/fastly/go-fastly/v14/fastly"
)

func TestZones(t *testing.T) {
ctx := context.TODO()

var err error

// Create dns zone.
var zone *Zone
fastly.Record(t, "create_zone", func(c *fastly.Client) {
zone, err = Create(ctx, c, &CreateInput{
Name: fastly.ToPointer("go-fastly-test.com"),
Type: fastly.ToPointer("secondary"),
Description: fastly.ToPointer("go-fastly test zone"),
})
})
require.NoError(t, err)
require.NotNil(t, zone)
require.NotNil(t, zone.ID)
// The API normalizes zone names to fully-qualified domain names (trailing dot).
require.Equal(t, "go-fastly-test.com.", *zone.Name)
require.Equal(t, "secondary", *zone.Type)
require.Equal(t, "go-fastly test zone", *zone.Description)

defer func() {
fastly.Record(t, "delete_zone", func(c *fastly.Client) {
_ = Delete(ctx, c, &DeleteInput{
ZoneID: zone.ID,
})
})
}()

// Get dns zone.
var got *Zone
fastly.Record(t, "get_zone", func(c *fastly.Client) {
got, err = Get(ctx, c, &GetInput{
ZoneID: zone.ID,
})
})
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, *zone.ID, *got.ID)
require.Equal(t, *zone.Name, *got.Name)

// Update
var updated *Zone
fastly.Record(t, "update_zone", func(c *fastly.Client) {
updated, err = Update(ctx, c, &UpdateInput{
ZoneID: zone.ID,
Description: fastly.ToPointer("updated description"),
})
})
require.NoError(t, err)
require.NotNil(t, updated)
require.Equal(t, *zone.ID, *updated.ID)
require.Equal(t, "updated description", *updated.Description)

// Create a second dns zone for pagination testing.
var zone2 *Zone
fastly.Record(t, "create_zone_2", func(c *fastly.Client) {
zone2, err = Create(ctx, c, &CreateInput{
Name: fastly.ToPointer("go-fastly-test-2.com"),
Type: fastly.ToPointer("secondary"),
Description: fastly.ToPointer("go-fastly test zone 2"),
})
})
require.NoError(t, err)
require.NotNil(t, zone2)

defer func() {
fastly.Record(t, "delete_zone_2", func(c *fastly.Client) {
_ = Delete(ctx, c, &DeleteInput{
ZoneID: zone2.ID,
})
})
}()

// List dns zones — page 1.
var page1 *Zones
fastly.Record(t, "list_zones_page_1", func(c *fastly.Client) {
page1, err = List(ctx, c, &ListInput{
Limit: fastly.ToPointer(1),
})
})
require.NoError(t, err)
require.NotNil(t, page1)
require.Len(t, page1.Data, 1)
require.NotNil(t, page1.Meta.NextCursor, "expected a next_cursor for page 2")

// List dns zones — page 2, using cursor from page 1.
var page2 *Zones
fastly.Record(t, "list_zones_page_2", func(c *fastly.Client) {
page2, err = List(ctx, c, &ListInput{
Limit: fastly.ToPointer(1),
Cursor: page1.Meta.NextCursor,
})
})
require.NoError(t, err)
require.NotNil(t, page2)
require.Len(t, page2.Data, 1)
require.NotEqual(t, *page1.Data[0].ID, *page2.Data[0].ID, "pages should return different zones")
}

func TestZones_validation(t *testing.T) {
ctx := context.TODO()

_, err := Get(ctx, fastly.TestClient, &GetInput{ZoneID: nil})
require.ErrorIs(t, err, fastly.ErrMissingID)

_, err = Create(ctx, fastly.TestClient, &CreateInput{
Type: fastly.ToPointer("primary"),
})
require.ErrorIs(t, err, fastly.ErrMissingName)

_, err = Create(ctx, fastly.TestClient, &CreateInput{
Name: fastly.ToPointer("example.com"),
})
require.ErrorIs(t, err, fastly.ErrMissingType)

_, err = Update(ctx, fastly.TestClient, &UpdateInput{ZoneID: nil})
require.ErrorIs(t, err, fastly.ErrMissingID)

err = Delete(ctx, fastly.TestClient, &DeleteInput{ZoneID: nil})
require.ErrorIs(t, err, fastly.ErrMissingID)
}
Loading
Loading