Skip to content
This repository has been archived by the owner on Sep 4, 2021. It is now read-only.

Commit

Permalink
Merge pull request #346 from colhom/existing-vpc
Browse files Browse the repository at this point in the history
Support for deploying to existing VPC
  • Loading branch information
colhom committed Apr 4, 2016
2 parents 7b61e37 + 454d363 commit 52c51de
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 107 deletions.
106 changes: 88 additions & 18 deletions multi-node/aws/pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/ec2"

"github.com/coreos/coreos-kubernetes/multi-node/aws/pkg/config"
)
Expand Down Expand Up @@ -42,38 +43,104 @@ func New(cfg *config.Cluster, awsDebug bool) *Cluster {
}

return &Cluster{
clusterName: aws.String(cfg.ClusterName),
svc: cloudformation.New(session.New(awsConfig)),
Cluster: *cfg,
session: session.New(awsConfig),
}
}

type Cluster struct {
clusterName *string
svc *cloudformation.CloudFormation
config.Cluster
session *session.Session
}

func (c *Cluster) ValidateStack(stackBody string) (string, error) {
input := &cloudformation.ValidateTemplateInput{
validateInput := cloudformation.ValidateTemplateInput{
TemplateBody: &stackBody,
}

validationReport, err := c.svc.ValidateTemplate(input)
cfSvc := cloudformation.New(c.session)
validationReport, err := cfSvc.ValidateTemplate(&validateInput)
if err != nil {
return "", fmt.Errorf("Invalid cloudformation stack: %v", err)
return "", fmt.Errorf("invalid cloudformation stack: %v", err)
}

return validationReport.String(), err
return validationReport.String(), nil
}

type ec2Service interface {
DescribeVpcs(*ec2.DescribeVpcsInput) (*ec2.DescribeVpcsOutput, error)
DescribeSubnets(*ec2.DescribeSubnetsInput) (*ec2.DescribeSubnetsOutput, error)
}

func (c *Cluster) validateExistingVPCState(ec2Svc ec2Service) error {
if c.VPCID == "" {
//The VPC will be created. No existing state to validate
return nil
}

describeVpcsInput := ec2.DescribeVpcsInput{
VpcIds: []*string{aws.String(c.VPCID)},
}
vpcOutput, err := ec2Svc.DescribeVpcs(&describeVpcsInput)
if err != nil {
return fmt.Errorf("error describing existing vpc: %v", err)
}
if len(vpcOutput.Vpcs) == 0 {
return fmt.Errorf("could not find vpc %s in region %s", c.VPCID, c.Region)
}
if len(vpcOutput.Vpcs) > 1 {
//Theoretically this should never happen. If it does, we probably want to know.
return fmt.Errorf("found more than one vpc with id %s. this is NOT NORMAL.", c.VPCID)
}

existingVPC := vpcOutput.Vpcs[0]

if *existingVPC.CidrBlock != c.VPCCIDR {
//If this is the case, our network config validation cannot be trusted and we must abort
return fmt.Errorf("configured vpcCidr (%s) does not match actual existing vpc cidr (%s)", c.VPCCIDR, *existingVPC.CidrBlock)
}

describeSubnetsInput := ec2.DescribeSubnetsInput{
Filters: []*ec2.Filter{
{
Name: aws.String("vpc-id"),
Values: []*string{existingVPC.VpcId},
},
},
}

subnetOutput, err := ec2Svc.DescribeSubnets(&describeSubnetsInput)
if err != nil {
return fmt.Errorf("error describing subnets for vpc: %v", err)
}

subnetCIDRS := make([]string, len(subnetOutput.Subnets))
for i, existingSubnet := range subnetOutput.Subnets {
subnetCIDRS[i] = *existingSubnet.CidrBlock
}

if err := c.ValidateExistingVPC(*existingVPC.CidrBlock, subnetCIDRS); err != nil {
return fmt.Errorf("error validating existing VPC: %v", err)
}

return nil
}

func (c *Cluster) Create(stackBody string) error {
ec2Svc := ec2.New(c.session)
if err := c.validateExistingVPCState(ec2Svc); err != nil {
return err
}

cfSvc := cloudformation.New(c.session)
creq := &cloudformation.CreateStackInput{
StackName: c.clusterName,
StackName: aws.String(c.ClusterName),
OnFailure: aws.String("DO_NOTHING"),
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
TemplateBody: &stackBody,
}

resp, err := c.svc.CreateStack(creq)
resp, err := cfSvc.CreateStack(creq)
if err != nil {
return err
}
Expand All @@ -82,7 +149,7 @@ func (c *Cluster) Create(stackBody string) error {
StackName: resp.StackId,
}
for {
resp, err := c.svc.DescribeStacks(&req)
resp, err := cfSvc.DescribeStacks(&req)
if err != nil {
return err
}
Expand All @@ -106,21 +173,22 @@ func (c *Cluster) Create(stackBody string) error {
}

func (c *Cluster) Update(stackBody string) (string, error) {
cfSvc := cloudformation.New(c.session)
input := &cloudformation.UpdateStackInput{
Capabilities: []*string{aws.String(cloudformation.CapabilityCapabilityIam)},
StackName: c.clusterName,
StackName: aws.String(c.ClusterName),
TemplateBody: &stackBody,
}

updateOutput, err := c.svc.UpdateStack(input)
updateOutput, err := cfSvc.UpdateStack(input)
if err != nil {
return "", fmt.Errorf("error updating cloudformation stack: %v", err)
}
req := cloudformation.DescribeStacksInput{
StackName: updateOutput.StackId,
}
for {
resp, err := c.svc.DescribeStacks(&req)
resp, err := cfSvc.DescribeStacks(&req)
if err != nil {
return "", err
}
Expand All @@ -146,10 +214,11 @@ func (c *Cluster) Update(stackBody string) (string, error) {
func (c *Cluster) Info() (*ClusterInfo, error) {
resources := make([]cloudformation.StackResourceSummary, 0)
req := cloudformation.ListStackResourcesInput{
StackName: c.clusterName,
StackName: aws.String(c.ClusterName),
}
cfSvc := cloudformation.New(c.session)
for {
resp, err := c.svc.ListStackResources(&req)
resp, err := cfSvc.ListStackResources(&req)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -178,9 +247,10 @@ func (c *Cluster) Info() (*ClusterInfo, error) {
}

func (c *Cluster) Destroy() error {
cfSvc := cloudformation.New(c.session)
dreq := &cloudformation.DeleteStackInput{
StackName: c.clusterName,
StackName: aws.String(c.ClusterName),
}
_, err := c.svc.DeleteStack(dreq)
_, err := cfSvc.DeleteStack(dreq)
return err
}
156 changes: 156 additions & 0 deletions multi-node/aws/pkg/cluster/cluster_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package cluster

import (
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/coreos/coreos-kubernetes/multi-node/aws/pkg/config"
)

type dummyEC2Service map[string]struct {
cidr string
subnetCidrs []string
}

func (svc dummyEC2Service) DescribeVpcs(input *ec2.DescribeVpcsInput) (*ec2.DescribeVpcsOutput, error) {
output := ec2.DescribeVpcsOutput{}
for _, vpcId := range input.VpcIds {
if vpc, ok := svc[*vpcId]; ok {
output.Vpcs = append(output.Vpcs, &ec2.Vpc{
VpcId: vpcId,
CidrBlock: aws.String(vpc.cidr),
})
}
}

return &output, nil
}

func (svc dummyEC2Service) DescribeSubnets(input *ec2.DescribeSubnetsInput) (*ec2.DescribeSubnetsOutput, error) {
output := ec2.DescribeSubnetsOutput{}

var vpcIds []string
for _, filter := range input.Filters {
if *filter.Name == "vpc-id" {
for _, value := range filter.Values {
vpcIds = append(vpcIds, *value)
}
}
}

for _, vpcId := range vpcIds {
if vpc, ok := svc[vpcId]; ok {
for _, subnetCidr := range vpc.subnetCidrs {
output.Subnets = append(
output.Subnets,
&ec2.Subnet{CidrBlock: aws.String(subnetCidr)},
)
}
}
}

return &output, nil
}

func TestExistingVPCValidation(t *testing.T) {
minimalConfigYaml := `
externalDNSName: test-external-dns-name
keyName: test-key-name
region: us-west-1
availabilityZone: us-west-1c
clusterName: test-cluster-name
kmsKeyArn: "arn:aws:kms:us-west-1:xxxxxxxxx:key/xxxxxxxxxxxxxxxxxxx"
`

goodExistingVPCConfigs := []string{
``, //Tests default create VPC mode, which bypasses existing VPC validation
`
vpcCIDR: 10.5.0.0/16
vpcId: vpc-xxx1
routeTableId: rtb-xxxxxx
instanceCIDR: 10.5.11.0/24
controllerIP: 10.5.11.10
`, `
vpcCIDR: 192.168.1.0/24
vpcId: vpc-xxx2
instanceCIDR: 192.168.1.50/28
controllerIP: 192.168.1.50
`,
}

badExistingVPCConfigs := []string{
`
vpcCIDR: 10.0.0.0/16
vpcId: vpc-xxx3 #vpc does not exist
instanceCIDR: 10.0.0.0/24
controllerIP: 10.0.0.50
routeTableId: rtb-xxxxxx
`, `
vpcCIDR: 10.10.0.0/16 #vpc cidr does match existing vpc-xxx1
vpcId: vpc-xxx1
instanceCIDR: 10.10.0.0/24
controllerIP: 10.10.0.50
routeTableId: rtb-xxxxxx
`, `
vpcCIDR: 10.5.0.0/16
instanceCIDR: 10.5.2.0/28 #instance cidr conflicts with existing subnet
controllerIP: 10.5.2.10
vpcId: vpc-xxx1
routeTableId: rtb-xxxxxx
`, `
vpcCIDR: 192.168.1.0/24
instanceCIDR: 192.168.1.100/26 #instance cidr conflicts with existing subnet
controllerIP: 192.168.1.80
vpcId: vpc-xxx2
routeTableId: rtb-xxxxxx
`,
}

ec2Service := dummyEC2Service{
"vpc-xxx1": {
cidr: "10.5.0.0/16",
subnetCidrs: []string{
"10.5.1.0/24",
"10.5.2.0/24",
"10.5.10.100/29",
},
},
"vpc-xxx2": {
cidr: "192.168.1.0/24",
subnetCidrs: []string{
"192.168.1.100/28",
"192.168.1.150/28",
"192.168.1.200/28",
},
},
}

validateCluster := func(networkConfig string) error {
configBody := minimalConfigYaml + networkConfig
clusterConfig, err := config.ClusterFromBytes([]byte(configBody))
if err != nil {
t.Errorf("could not get valid cluster config: %v", err)
return nil
}

cluster := &Cluster{
Cluster: *clusterConfig,
}

return cluster.validateExistingVPCState(ec2Service)
}

for _, networkConfig := range goodExistingVPCConfigs {
if err := validateCluster(networkConfig); err != nil {
t.Errorf("Correct config tested invalid: %s\n%s", err, networkConfig)
}
}

for _, networkConfig := range badExistingVPCConfigs {
if err := validateCluster(networkConfig); err == nil {
t.Errorf("Incorrect config tested valid, expected error:\n%s", networkConfig)
}
}

}
Loading

0 comments on commit 52c51de

Please sign in to comment.