readysite / pkg / platform / providers / aws / dns.go
3.7 KB
dns.go
package aws

import (
	"fmt"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/route53"
	route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
	"github.com/readysite/readysite/pkg/platform"
)

// CreateDNSZone creates a Route 53 hosted zone
func (b *backend) CreateDNSZone(domain string) (*platform.DNSZone, error) {
	input := &route53.CreateHostedZoneInput{
		Name:            aws.String(domain),
		CallerReference: aws.String(fmt.Sprintf("%s-%d", domain, time.Now().Unix())),
	}

	result, err := b.route53.CreateHostedZone(b.ctx, input)
	if err != nil {
		return nil, fmt.Errorf("create hosted zone: %w", err)
	}

	return &platform.DNSZone{
		Name: *result.HostedZone.Name,
		TTL:  300,
	}, nil
}

// GetDNSZone retrieves a Route 53 hosted zone
func (b *backend) GetDNSZone(domain string) (*platform.DNSZone, error) {
	input := &route53.ListHostedZonesByNameInput{
		DNSName: aws.String(domain),
	}

	result, err := b.route53.ListHostedZonesByName(b.ctx, input)
	if err != nil {
		return nil, fmt.Errorf("list hosted zones: %w", err)
	}

	for _, zone := range result.HostedZones {
		if strings.TrimSuffix(*zone.Name, ".") == domain {
			return &platform.DNSZone{
				Name: strings.TrimSuffix(*zone.Name, "."),
				TTL:  300,
			}, nil
		}
	}

	return nil, platform.ErrNotFound
}

// AddDNSRecord adds a Route 53 record
func (b *backend) AddDNSRecord(zone string, record platform.DNSRecord) error {
	zoneID, err := b.getZoneID(zone)
	if err != nil {
		return err
	}

	name := record.Name
	if name == "@" {
		name = zone
	} else {
		name = record.Name + "." + zone
	}

	input := &route53.ChangeResourceRecordSetsInput{
		HostedZoneId: aws.String(zoneID),
		ChangeBatch: &route53types.ChangeBatch{
			Changes: []route53types.Change{
				{
					Action: route53types.ChangeActionUpsert,
					ResourceRecordSet: &route53types.ResourceRecordSet{
						Name: aws.String(name),
						Type: route53types.RRType(record.Type),
						TTL:  aws.Int64(int64(record.TTL)),
						ResourceRecords: []route53types.ResourceRecord{
							{Value: aws.String(record.Value)},
						},
					},
				},
			},
		},
	}

	_, err = b.route53.ChangeResourceRecordSets(b.ctx, input)
	if err != nil {
		return fmt.Errorf("change record sets: %w", err)
	}
	return nil
}

// DeleteDNSRecord removes a Route 53 record
func (b *backend) DeleteDNSRecord(zone, recordID string) error {
	// recordID format: "TYPE:NAME:VALUE"
	parts := strings.SplitN(recordID, ":", 3)
	if len(parts) != 3 {
		return fmt.Errorf("invalid record ID format")
	}

	zoneID, err := b.getZoneID(zone)
	if err != nil {
		return err
	}

	input := &route53.ChangeResourceRecordSetsInput{
		HostedZoneId: aws.String(zoneID),
		ChangeBatch: &route53types.ChangeBatch{
			Changes: []route53types.Change{
				{
					Action: route53types.ChangeActionDelete,
					ResourceRecordSet: &route53types.ResourceRecordSet{
						Name: aws.String(parts[1]),
						Type: route53types.RRType(parts[0]),
						TTL:  aws.Int64(300),
						ResourceRecords: []route53types.ResourceRecord{
							{Value: aws.String(parts[2])},
						},
					},
				},
			},
		},
	}

	_, err = b.route53.ChangeResourceRecordSets(b.ctx, input)
	if err != nil {
		return fmt.Errorf("delete record: %w", err)
	}
	return nil
}

func (b *backend) getZoneID(domain string) (string, error) {
	input := &route53.ListHostedZonesByNameInput{
		DNSName: aws.String(domain),
	}

	result, err := b.route53.ListHostedZonesByName(b.ctx, input)
	if err != nil {
		return "", fmt.Errorf("list hosted zones: %w", err)
	}

	for _, zone := range result.HostedZones {
		if strings.TrimSuffix(*zone.Name, ".") == domain {
			// Zone ID comes as /hostedzone/XXXXX, extract just the ID
			return strings.TrimPrefix(*zone.Id, "/hostedzone/"), nil
		}
	}

	return "", platform.ErrNotFound
}
← Back