Skip to content

Commit af9ffa5

Browse files
committed
Make VPC creation idempotent to avoid indefinite creation of new VPCs if storage of the ID fails
1 parent 0f0e9b0 commit af9ffa5

File tree

3 files changed

+160
-134
lines changed

3 files changed

+160
-134
lines changed

controllers/awscluster_controller_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,10 @@ func mockedCreateVPCCalls(m *mocks.MockEC2APIMockRecorder) {
654654
}
655655

656656
func mockedCreateMaximumVPCCalls(m *mocks.MockEC2APIMockRecorder) {
657-
m.CreateVpcWithContext(context.TODO(), gomock.AssignableToTypeOf(&ec2.CreateVpcInput{})).Return(nil, errors.New("The maximum number of VPCs has been reached"))
657+
describeVPCByNameCall := m.DescribeVpcsWithContext(context.TODO(), gomock.AssignableToTypeOf(&ec2.DescribeVpcsInput{})).Return(&ec2.DescribeVpcsOutput{
658+
Vpcs: []*ec2.Vpc{},
659+
}, nil)
660+
m.CreateVpcWithContext(context.TODO(), gomock.AssignableToTypeOf(&ec2.CreateVpcInput{})).After(describeVPCByNameCall).Return(nil, errors.New("The maximum number of VPCs has been reached"))
658661
}
659662

660663
func mockedDeleteVPCCallsForNonExistentVPC(m *mocks.MockEC2APIMockRecorder) {

pkg/cloud/services/network/vpc.go

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,24 +103,45 @@ func (s *Service) reconcileVPC() error {
103103
return nil
104104
}
105105

106-
// .spec.vpc.id is nil, Create a new managed vpc.
107-
if !conditions.Has(s.scope.InfraCluster(), infrav1.VpcReadyCondition) {
108-
conditions.MarkFalse(s.scope.InfraCluster(), infrav1.VpcReadyCondition, infrav1.VpcCreationStartedReason, clusterv1.ConditionSeverityInfo, "")
109-
if err := s.scope.PatchObject(); err != nil {
110-
return errors.Wrap(err, "failed to patch conditions")
106+
// .spec.vpc.id is nil. This means no managed VPC exists or we failed to save its ID before. Check if a managed VPC
107+
// with the desired name exists, or if not, create a new managed VPC.
108+
109+
vpc, err := s.describeVPCByName()
110+
if err == nil {
111+
// An VPC already exists with the desired name
112+
113+
if !vpc.Tags.HasOwned(s.scope.Name()) {
114+
return errors.Errorf(
115+
"found VPC %q which cannot be managed by CAPA due to lack of tags (either tag the VPC manually with `%s=%s`, or provide the `vpc.id` field instead if you wish to bring your own VPC as shown in https://cluster-api-aws.sigs.k8s.io/topics/bring-your-own-aws-infrastructure)",
116+
vpc.ID,
117+
infrav1.ClusterTagKey(s.scope.Name()),
118+
infrav1.ResourceLifecycleOwned)
111119
}
120+
} else {
121+
if !awserrors.IsNotFound(err) {
122+
return errors.Wrap(err, "failed to describe VPC resources by name")
123+
}
124+
125+
// VPC with that name does not exist yet. Create it.
126+
vpc, err = s.createVPC()
127+
if err != nil {
128+
return errors.Wrap(err, "failed to create new managed VPC")
129+
}
130+
s.scope.Info("Created VPC", "vpc-id", vpc.ID)
112131
}
113-
vpc, err := s.createVPC()
114-
if err != nil {
115-
return errors.Wrap(err, "failed to create new vpc")
116-
}
117-
s.scope.Info("Created VPC", "vpc-id", vpc.ID)
118132

119133
s.scope.VPC().CidrBlock = vpc.CidrBlock
120134
s.scope.VPC().IPv6 = vpc.IPv6
121135
s.scope.VPC().Tags = vpc.Tags
122136
s.scope.VPC().ID = vpc.ID
123137

138+
if !conditions.Has(s.scope.InfraCluster(), infrav1.VpcReadyCondition) {
139+
conditions.MarkFalse(s.scope.InfraCluster(), infrav1.VpcReadyCondition, infrav1.VpcCreationStartedReason, clusterv1.ConditionSeverityInfo, "")
140+
if err := s.scope.PatchObject(); err != nil {
141+
return errors.Wrap(err, "failed to patch conditions")
142+
}
143+
}
144+
124145
// Make sure attributes are configured
125146
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
126147
if err := s.ensureManagedVPCAttributes(vpc); err != nil {
@@ -573,6 +594,54 @@ func (s *Service) describeVPCByID() (*infrav1.VPCSpec, error) {
573594
return vpc, nil
574595
}
575596

597+
// describeVPCByName finds the VPC by `Name` tag. Use this if the ID is not available yet, either because no
598+
// VPC was created until now or if storing the ID could have failed.
599+
func (s *Service) describeVPCByName() (*infrav1.VPCSpec, error) {
600+
vpcName := *s.getVPCTagParams(services.TemporaryResourceID).Name
601+
602+
input := &ec2.DescribeVpcsInput{
603+
Filters: []*ec2.Filter{
604+
{
605+
Name: aws.String("tag:Name"),
606+
Values: aws.StringSlice([]string{vpcName}),
607+
},
608+
},
609+
}
610+
611+
out, err := s.EC2Client.DescribeVpcsWithContext(context.TODO(), input)
612+
if (err != nil && awserrors.IsNotFound(err)) || (out != nil && len(out.Vpcs) == 0) {
613+
return nil, awserrors.NewNotFound(fmt.Sprintf("could not find VPC by name %q", vpcName))
614+
}
615+
if err != nil {
616+
return nil, errors.Wrapf(err, "failed to query ec2 for VPCs by name %q", vpcName)
617+
}
618+
if len(out.Vpcs) > 1 {
619+
return nil, awserrors.NewConflict(fmt.Sprintf("found %v VPCs with name %q. Only one VPC per cluster name is supported. Ensure duplicate VPCs are deleted for this AWS account and there are no conflicting instances of Cluster API Provider AWS. Filtered VPCs: %v", len(out.Vpcs), vpcName, out.GoString()))
620+
}
621+
622+
switch *out.Vpcs[0].State {
623+
case ec2.VpcStateAvailable, ec2.VpcStatePending:
624+
default:
625+
return nil, awserrors.NewNotFound(fmt.Sprintf("could not find available or pending VPC by name %q", vpcName))
626+
}
627+
628+
vpc := &infrav1.VPCSpec{
629+
ID: *out.Vpcs[0].VpcId,
630+
CidrBlock: *out.Vpcs[0].CidrBlock,
631+
Tags: converters.TagsToMap(out.Vpcs[0].Tags),
632+
}
633+
for _, set := range out.Vpcs[0].Ipv6CidrBlockAssociationSet {
634+
if *set.Ipv6CidrBlockState.State == ec2.SubnetCidrBlockStateCodeAssociated {
635+
vpc.IPv6 = &infrav1.IPv6{
636+
CidrBlock: aws.StringValue(set.Ipv6CidrBlock),
637+
PoolID: aws.StringValue(set.Ipv6Pool),
638+
}
639+
break
640+
}
641+
}
642+
return vpc, nil
643+
}
644+
576645
func (s *Service) getVPCTagParams(id string) infrav1.BuildParams {
577646
name := fmt.Sprintf("%s-vpc", s.scope.Name())
578647

0 commit comments

Comments
 (0)