Skip to content

Commit

Permalink
feat: support the attachment of an EC2 to an auto-generated private h…
Browse files Browse the repository at this point in the history
…osted zone (#128)
  • Loading branch information
purpleclay authored Oct 20, 2022
1 parent 0a37b1c commit f802e83
Show file tree
Hide file tree
Showing 6 changed files with 580 additions and 12 deletions.
70 changes: 69 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Built using Bubbletea 🧋`
# Launch the TUI using a chosen PHZ, effectively skipping the wizard
dns53 --phz-id Z000000000ABCDEFGHIJK
# Launch the TUI, automatically creating and attaching to a default
# PHZ. This will also skip the wizard
dns53 --auto-attach
# Launch the TUI with a given domain name
dns53 --domain-name custom.domain
Expand All @@ -79,6 +83,15 @@ var (
type options struct {
phzID string
domainName string
autoAttach bool
}

type autoAttachment struct {
phzID string
vpc string
region string
createdPhz bool
associatedPhz bool
}

func Execute(out io.Writer) error {
Expand Down Expand Up @@ -118,8 +131,20 @@ func Execute(out io.Writer) error {
return err
},
RunE: func(cmd *cobra.Command, args []string) error {
r53Client := r53.NewFromAPI(awsr53.NewFromConfig(cfg))

if opts.autoAttach {
attachment, err := autoAttachToZone(r53Client, "dns53", metadata.VPC, metadata.Region)
if err != nil {
return err
}
opts.phzID = attachment.phzID

defer removeAttachmentToZone(r53Client, attachment)
}

model := tui.Dashboard(tui.DashboardOptions{
R53Client: r53.NewFromAPI(awsr53.NewFromConfig(cfg)),
R53Client: r53Client,
Metadata: metadata,
Version: version,
PhzID: opts.phzID,
Expand All @@ -135,6 +160,7 @@ func Execute(out io.Writer) error {
pf.StringVar(&globalOpts.AWSProfile, "profile", "", "the AWS named profile to use when loading credentials")

f := rootCmd.Flags()
f.BoolVar(&opts.autoAttach, "auto-attach", false, "automatically create and attach a record set to a default private hosted zone")
f.StringVar(&opts.domainName, "domain-name", "", "assign a custom domain name when generating a record set")
f.StringVar(&opts.phzID, "phz-id", "", "an ID of a Route53 private hosted zone to use when generating a record set")

Expand Down Expand Up @@ -202,3 +228,45 @@ https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#allow-access

return dmn, nil
}

func autoAttachToZone(client *r53.Client, name, vpc, region string) (autoAttachment, error) {
attachment := autoAttachment{
vpc: vpc,
region: region,
}

zone, err := client.ByName(context.Background(), "dns53")
if err != nil {
return attachment, err
}

if zone == nil {
newZone, err := client.CreatePrivateHostedZone(context.Background(), "dns53", vpc, region)
if err != nil {
return attachment, err
}

zone = &newZone

// Record that this PHZ was created during auto-attachment
attachment.createdPhz = true
} else {
if err := client.AssociateVPCWithZone(context.Background(), zone.ID, vpc, region); err != nil {
return attachment, err
}

// An explicit association has been made between the EC2 VPC and the PHZ during auto-attachment
attachment.associatedPhz = true
}

attachment.phzID = zone.ID
return attachment, nil
}

func removeAttachmentToZone(client *r53.Client, attach autoAttachment) error {
if attach.createdPhz {
return client.DeletePrivateHostedZone(context.Background(), attach.phzID)
}

return client.DisassociateVPCWithZone(context.Background(), attach.phzID, attach.vpc, attach.region)
}
94 changes: 94 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ SOFTWARE.
package cmd

import (
"errors"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
awsr53 "github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/aws/aws-sdk-go-v2/service/route53/types"
"github.com/purpleclay/dns53/internal/imds"
"github.com/purpleclay/dns53/internal/r53"
"github.com/purpleclay/dns53/internal/r53/r53mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -154,3 +161,90 @@ func TestCleanTagsAppendsToMap(t *testing.T) {
assert.Equal(t, v, tags[k])
}
}

func TestAutoAttachToZone(t *testing.T) {
m := r53mock.New(t)
m.On("ListHostedZonesByName", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.ListHostedZonesByNameOutput{}, nil)
m.On("CreateHostedZone", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.CreateHostedZoneOutput{
HostedZone: &types.HostedZone{
Id: aws.String("/hostedzone/Z00000000001"),
Name: aws.String("dns53."),
},
}, nil)

attachment, err := autoAttachToZone(r53.NewFromAPI(m), "dns53", "vpc-12345", "eu-west-2")

require.NoError(t, err)
assert.Equal(t, "Z00000000001", attachment.phzID)
assert.True(t, attachment.createdPhz)
assert.False(t, attachment.associatedPhz)
}

func TestAutoAttachToZoneExisting(t *testing.T) {
m := r53mock.New(t)
m.On("ListHostedZonesByName", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.ListHostedZonesByNameOutput{
HostedZones: []types.HostedZone{
{
Id: aws.String("/hostedzone/Z00000000002"),
Name: aws.String("dns53"),
Config: &types.HostedZoneConfig{
PrivateZone: true,
},
},
},
}, nil)
m.On("AssociateVPCWithHostedZone", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.AssociateVPCWithHostedZoneOutput{}, nil)

attachment, err := autoAttachToZone(r53.NewFromAPI(m), "dns53", "vpc-12345", "eu-west-2")

require.NoError(t, err)
assert.Equal(t, "Z00000000002", attachment.phzID)
assert.False(t, attachment.createdPhz)
assert.True(t, attachment.associatedPhz)
}

func TestAutoAttachToZoneSearchError(t *testing.T) {
errMsg := "failed to search"

m := r53mock.New(t)
m.On("ListHostedZonesByName", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.ListHostedZonesByNameOutput{}, errors.New(errMsg))

_, err := autoAttachToZone(r53.NewFromAPI(m), "dns53", "vpc-12345", "eu-west-2")

require.EqualError(t, err, errMsg)
m.AssertNotCalled(t, "AssociateVPCWithHostedZone")
}

func TestAutoAttachToZoneCreationError(t *testing.T) {
errMsg := "failed to create"

m := r53mock.New(t)
m.On("ListHostedZonesByName", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.ListHostedZonesByNameOutput{}, nil)
m.On("CreateHostedZone", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.CreateHostedZoneOutput{}, errors.New(errMsg))

_, err := autoAttachToZone(r53.NewFromAPI(m), "dns53", "vpc-12345", "eu-west-2")

require.EqualError(t, err, errMsg)
}

func TestAutoAttachToZoneAssociationError(t *testing.T) {
errMsg := "failed to associate"

m := r53mock.New(t)
m.On("ListHostedZonesByName", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.ListHostedZonesByNameOutput{
HostedZones: []types.HostedZone{
{
Id: aws.String("/hostedzone/Z00000000003"),
Name: aws.String("dns53"),
Config: &types.HostedZoneConfig{
PrivateZone: true,
},
},
},
}, nil)
m.On("AssociateVPCWithHostedZone", mock.Anything, mock.Anything, mock.Anything).Return(&awsr53.AssociateVPCWithHostedZoneOutput{}, errors.New(errMsg))

_, err := autoAttachToZone(r53.NewFromAPI(m), "dns53", "vpc-12345", "eu-west-2")

require.EqualError(t, err, errMsg)
}
Loading

0 comments on commit f802e83

Please sign in to comment.