Skip to content

Commit

Permalink
Adds userdata test
Browse files Browse the repository at this point in the history
  • Loading branch information
bwhaley committed May 9, 2024
1 parent 560751b commit c457ce9
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
**/__pycache__
**/terraform.tfstate*
**/.terraform*
test/.test-data
**/.test-data
3 changes: 2 additions & 1 deletion example/main.tf → examples/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module "alternat" {
#alternat_image_uri = "188238883601.dkr.ecr.us-west-2.amazonaws.com/alternat"
#alternat_image_tag = "v0.4.9"

nat_instance_type = var.alternat_instance_type
nat_instance_type = var.alternat_instance_type
nat_instance_key_name = var.nat_instance_key_name
#connectivity_test_check_urls = ["https://www.google.com", "https://www.example.com"]
}
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions example/variables.tf → examples/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ variable "enable_nat_gateway" {
default = false
}

variable "nat_instance_key_name" {
description = "The name of the key pair to use for the NAT instances."
type = string
default = ""
}

variable "private_subnets" {
description = "List of private subnets to use in the example VPC."
type = list(string)
Expand Down
2 changes: 2 additions & 0 deletions modules/terraform-aws-alternat/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ resource "aws_launch_template" "nat_instance_template" {

instance_type = var.nat_instance_type

key_name = var.nat_instance_key_name

metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
Expand Down
6 changes: 6 additions & 0 deletions modules/terraform-aws-alternat/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ variable "nat_instance_iam_role_name" {
default = ""
}

variable "nat_instance_key_name" {
description = "The name of the key pair to use for the NAT instance. This is primarily used for testing."
type = string
default = ""
}

variable "nat_instance_lifecycle_hook_role_name" {
description = "Name to use for the IAM role used by the NAT instance lifecycle hook. Must be globally unique in this AWS account. Defaults to alternat-lifecycle-hook as a prefix."
type = string
Expand Down
264 changes: 203 additions & 61 deletions test/alternat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,72 @@ package test
import (
"context"
"fmt"
"io"
"net/http"
"os"
"testing"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"

"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"

terraws "github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/logger"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/retry"
"github.com/gruntwork-io/terratest/modules/ssh"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
// "github.com/davecgh/go-spew/spew"
)

func TestAlternat(t *testing.T) {
// os.Setenv("SKIP_setup", "true")
// os.Setenv("SKIP_apply_vpc", "true")
// os.Setenv("SKIP_apply_alternat_basic", "true")
// os.Setenv("SKIP_validate_alternat_basic", "true")
os.Setenv("SKIP_setup", "true")
os.Setenv("SKIP_apply_vpc", "true")
os.Setenv("SKIP_apply_alternat_basic", "true")
os.Setenv("SKIP_validate_alternat_basic", "true")
os.Setenv("SKIP_validate_alternat_setup", "true")
// os.Setenv("SKIP_validate_alternat_replace_route", "true")
// os.Setenv("SKIP_destroy", "true")
// os.Setenv("SKIP_cleanup", "true")

// logger := logger.Logger{}

cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
t.Fatalf("Unable to load SDK config, %v", err)
}
ec2Client := ec2.NewFromConfig(cfg)
exampleFolder := test_structure.CopyTerraformFolderToTemp(t, "..", "examples/")

defer test_structure.RunTestStage(t, "destroy", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, ".")
logger := logger.Logger{}

defer test_structure.RunTestStage(t, "cleanup", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)
awsKeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)
terraws.DeleteEC2KeyPair(t, awsKeyPair)
terraform.Destroy(t, terraformOptions)
})

test_structure.RunTestStage(t, "setup", func() {
//awsRegion := terratestaws.GetRandomStableRegion(t, nil, nil)
awsRegion := "us-east-1"
awsRegion := terraws.GetRandomStableRegion(t, nil, nil)

uniqueID := random.UniqueId()
keyPair := ssh.GenerateRSAKeyPair(t, 2048)
awsKeyPair := terraws.ImportEC2KeyPair(t, awsRegion, uniqueID, keyPair)

terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../example",
TerraformDir: exampleFolder,
Vars: map[string]interface{}{
"aws_region": awsRegion,
"nat_instance_key_name": awsKeyPair.Name,
},
})
test_structure.SaveString(t, ".", "awsRegion", awsRegion)
test_structure.SaveTerraformOptions(t, ".", terraformOptions)

test_structure.SaveString(t, exampleFolder, "awsRegion", awsRegion)
test_structure.SaveEc2KeyPair(t, exampleFolder, awsKeyPair)
test_structure.SaveTerraformOptions(t, exampleFolder, terraformOptions)
})

test_structure.RunTestStage(t, "apply_vpc", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, ".")
terraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)
terraformOptionsVpcOnly, err := terraformOptions.Clone()
if err != nil {
t.Fatal(err)
Expand All @@ -63,17 +77,21 @@ func TestAlternat(t *testing.T) {
terraform.InitAndApply(t, terraformOptionsVpcOnly)

vpcID := terraform.Output(t, terraformOptions, "vpc_id")
test_structure.SaveString(t, ".", "vpcID", vpcID)
test_structure.SaveString(t, exampleFolder, "vpcID", vpcID)
})

test_structure.RunTestStage(t, "apply_alternat_basic", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, ".")
terraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)
terraform.InitAndApply(t, terraformOptions)
assert.Equal(t, 0, terraform.InitAndPlanWithExitCode(t, terraformOptions))
sgId := terraform.Output(t, terraformOptions, "nat_instance_security_group_id")
test_structure.SaveString(t, exampleFolder, "sgId", sgId)
})

test_structure.RunTestStage(t, "validate_alternat_basic", func() {
vpcID := test_structure.LoadString(t, ".", "vpcID")
vpcID := test_structure.LoadString(t, exampleFolder, "vpcID")
awsRegion := test_structure.LoadString(t, exampleFolder, "awsRegion")
ec2Client := getEc2Client(t, awsRegion)
routeTables, err := getRouteTables(t, ec2Client, vpcID)
require.NoError(t, err)

Expand All @@ -89,13 +107,67 @@ func TestAlternat(t *testing.T) {
}
})

test_structure.RunTestStage(t, "validate_alternat_setup", func() {
sgId := aws.String(test_structure.LoadString(t, exampleFolder, "sgId"))
ec2Client := getEc2Client(t, test_structure.LoadString(t, exampleFolder, "awsRegion"))
authorizeSshIngress(t, ec2Client, sgId)
ip, err := getNatInstancePublicIp(ec2Client)
require.NoError(t, err)
awsKeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)

natInstance := ssh.Host{
Hostname: ip,
SshUserName: "ec2-user",
SshKeyPair: awsKeyPair.KeyPair,
}

maxRetries := 6
waitTime := 10 * time.Second
retry.DoWithRetry(t, fmt.Sprintf("Check SSH connection to %s", ip), maxRetries, waitTime, func() (string, error) {
return "", ssh.CheckSshConnectionE(t, natInstance)
},
)
command := "/usr/sbin/sysctl net.ipv4.ip_forward net.ipv4.conf.eth0.send_redirects net.ipv4.ip_local_port_range"

expectedText := `net.ipv4.ip_forward = 1
net.ipv4.conf.eth0.send_redirects = 0
net.ipv4.ip_local_port_range = 1024 65535
`

maxRetries = 1
waitTime = 10 * time.Second
retry.DoWithRetry(t, fmt.Sprintf("SSH to NAT instance at IP %s", ip), maxRetries, waitTime, func() (string, error) {
actualText, err := ssh.CheckSshCommandE(t, natInstance, command)
require.NoError(t, err)
if actualText != expectedText {
return "", fmt.Errorf("Expected SSH command to return '%s' but got '%s'", expectedText, actualText)
}
return "", nil
})

userdataLogFile := "/var/log/user-data.log"
output := retry.DoWithRetry(
t,
fmt.Sprintf("Check contents of file %s", userdataLogFile),
10,
30*time.Second,
func() (string, error) {
return ssh.FetchContentsOfFileE(t, natInstance, false, userdataLogFile)
},
)
assert.Contains(t, output, "Configuration completed successfully!", "Success string not found in user-data log: %s", output)
logger.Logf(t, "Userdata log: %s", output)
})

// Delete the egress rules that allow access to the Internet from the instance, then
// validate that Alternat has updated the route to use the NAT Gateway
// validate that Alternat has updated the route to use the NAT Gateway.
test_structure.RunTestStage(t, "validate_alternat_replace_route", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, ".")
vpcID := test_structure.LoadString(t, ".", "vpcID")
sgId := aws.String(test_structure.LoadString(t, exampleFolder, "sgId"))
vpcID := test_structure.LoadString(t, exampleFolder, "vpcID")
awsRegion := test_structure.LoadString(t, exampleFolder, "awsRegion")
ec2Client := getEc2Client(t, awsRegion)

revokeInternetEgress(ec2Client, t, terraformOptions)
updateEgress(t, ec2Client, sgId, true)

// Validate that private route tables have routes to the Internet via NAT Gateway
maxRetries := 12
Expand All @@ -112,44 +184,46 @@ func TestAlternat(t *testing.T) {
}
return "All private route tables route through NAT Gateway", nil
})
updateEgress(t, ec2Client, sgId, false)
})
}

func revokeInternetEgress(ec2Client *ec2.Client, t *testing.T, terraformOptions *terraform.Options) {
sgId := aws.String(terraform.Output(t, terraformOptions, "nat_instance_security_group_id"))
_, err := ec2Client.RevokeSecurityGroupEgress(context.TODO(), &ec2.RevokeSecurityGroupEgressInput{
GroupId: sgId,
IpPermissions: []ec2types.IpPermission{
{
FromPort: aws.Int32(0),
ToPort: aws.Int32(0),
IpProtocol: aws.String("-1"),
IpRanges: []ec2types.IpRange{
{
CidrIp: aws.String("0.0.0.0/0"),
},
},
},
func updateEgress(t *testing.T, ec2Client *ec2.Client, sgId *string, revoke bool) {
basePermission := ec2types.IpPermission{
FromPort: aws.Int32(0),
ToPort: aws.Int32(0),
IpProtocol: aws.String("-1"),
}
ipv4Permission := basePermission
ipv4Permission.IpRanges = []ec2types.IpRange{
{
CidrIp: aws.String("0.0.0.0/0"),
},
})
require.NoError(t, err)

_, err = ec2Client.RevokeSecurityGroupEgress(context.TODO(), &ec2.RevokeSecurityGroupEgressInput{
GroupId: sgId,
IpPermissions: []ec2types.IpPermission{
{
FromPort: aws.Int32(0),
ToPort: aws.Int32(0),
IpProtocol: aws.String("-1"),
Ipv6Ranges: []ec2types.Ipv6Range{
{
CidrIpv6: aws.String("::/0"),
},
},
},
}
ipv6Permission := basePermission
ipv6Permission.Ipv6Ranges = []ec2types.Ipv6Range{
{
CidrIpv6: aws.String("::/0"),
},
})
require.NoError(t, err)
}
allPermissions := []ec2types.IpPermission{ipv4Permission, ipv6Permission}

var err error
if revoke {
_, err = ec2Client.RevokeSecurityGroupEgress(context.TODO(), &ec2.RevokeSecurityGroupEgressInput{
GroupId: sgId,
IpPermissions: allPermissions,
},
)
require.NoError(t, err)
} else {
_, err = ec2Client.AuthorizeSecurityGroupEgress(context.TODO(), &ec2.AuthorizeSecurityGroupEgressInput{
GroupId: sgId,
IpPermissions: allPermissions,
},
)
require.NoError(t, err)
}
}

func getRouteTables(t *testing.T, client *ec2.Client, vpcID string) ([]ec2types.RouteTable, error) {
Expand All @@ -170,3 +244,71 @@ func getRouteTables(t *testing.T, client *ec2.Client, vpcID string) ([]ec2types.

return result.RouteTables, nil
}

func getNatInstancePublicIp(ec2Client *ec2.Client) (string, error) {
namePrefix := "alternat-"
input := &ec2.DescribeInstancesInput{
Filters: []ec2types.Filter{
{
Name: aws.String("tag:Name"),
Values: []string{namePrefix + "*"},
},
},
}

result, err := ec2Client.DescribeInstances(context.TODO(), input)
if err != nil {
return "", err
}

return aws.ToString(result.Reservations[0].Instances[0].PublicIpAddress), nil
}

func getThisPublicIp() (string, error) {
url := "https://api.ipify.org"
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("Error fetching IP: %v\n", err)
}
defer resp.Body.Close()

ip, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("Error reading response: %v", err)
}

return string(ip), nil
}

func authorizeSshIngress(t *testing.T, ec2Client *ec2.Client, sgId *string) {
ip, err := getThisPublicIp()
require.NoError(t, err)

ipPermission := []ec2types.IpPermission{
{
FromPort: aws.Int32(22),
ToPort: aws.Int32(22),
IpProtocol: aws.String("tcp"),
IpRanges: []ec2types.IpRange{
{
CidrIp: aws.String(ip + "/32"),
},
},
},
}

_, err = ec2Client.AuthorizeSecurityGroupIngress(context.TODO(), &ec2.AuthorizeSecurityGroupIngressInput{
GroupId: sgId,
IpPermissions: ipPermission,
},
)
require.NoError(t, err)
}

func getEc2Client(t *testing.T, awsRegion string) *ec2.Client {
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(awsRegion))
if err != nil {
t.Fatalf("Unable to load SDK config, %v", err)
}
return ec2.NewFromConfig(cfg)
}
Loading

0 comments on commit c457ce9

Please sign in to comment.