Skip to content

Commit f192929

Browse files
authored
Merge pull request #2587 from randomvariable/reconcileVPC
Cleanup ReconcileVPC() and set id early in reconciliation.
2 parents d38fda2 + fea802e commit f192929

File tree

5 files changed

+146
-99
lines changed

5 files changed

+146
-99
lines changed

api/v1alpha4/awscluster_webhook.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ func (r *AWSCluster) ValidateUpdate(old runtime.Object) error {
109109
)
110110
}
111111

112+
// Modifying VPC id is not allowed because it will cause a new VPC creation if set to nil.
113+
if !reflect.DeepEqual(oldC.Spec.NetworkSpec, NetworkSpec{}) &&
114+
!reflect.DeepEqual(oldC.Spec.NetworkSpec.VPC, VPCSpec{}) &&
115+
oldC.Spec.NetworkSpec.VPC.ID != "" {
116+
if reflect.DeepEqual(r.Spec.NetworkSpec, NetworkSpec{}) ||
117+
reflect.DeepEqual(r.Spec.NetworkSpec.VPC, VPCSpec{}) ||
118+
oldC.Spec.NetworkSpec.VPC.ID != r.Spec.NetworkSpec.VPC.ID {
119+
allErrs = append(allErrs,
120+
field.Invalid(field.NewPath("spec", "networkSpec", "vpc", "id"),
121+
r.Spec.IdentityRef, "field cannot be modified once set"))
122+
}
123+
}
124+
112125
// If a identityRef is already set, do not allow removal of it.
113126
if oldC.Spec.IdentityRef != nil && r.Spec.IdentityRef == nil {
114127
allErrs = append(allErrs,

api/v1alpha4/awscluster_webhook_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,38 @@ func TestAWSCluster_ValidateUpdate(t *testing.T) {
200200
},
201201
wantErr: false,
202202
},
203+
{
204+
name: "VPC id is immutable cannot be emptied once set",
205+
oldCluster: &AWSCluster{
206+
Spec: AWSClusterSpec{
207+
NetworkSpec: NetworkSpec{
208+
VPC: VPCSpec{ID: "managed-or-unmanaged-vpc"},
209+
},
210+
},
211+
},
212+
newCluster: &AWSCluster{
213+
Spec: AWSClusterSpec{},
214+
},
215+
wantErr: true,
216+
},
217+
{
218+
name: "VPC id is immutable, cannot be set to a different value once set",
219+
oldCluster: &AWSCluster{
220+
Spec: AWSClusterSpec{
221+
NetworkSpec: NetworkSpec{
222+
VPC: VPCSpec{ID: "managed-or-unmanaged-vpc"},
223+
},
224+
},
225+
},
226+
newCluster: &AWSCluster{
227+
Spec: AWSClusterSpec{
228+
NetworkSpec: NetworkSpec{
229+
VPC: VPCSpec{ID: "a-new-vpc"},
230+
},
231+
},
232+
},
233+
wantErr: true,
234+
},
203235
}
204236
for _, tt := range tests {
205237
t.Run(tt.name, func(t *testing.T) {

pkg/cloud/services/network/network.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,22 @@ func (s *Service) ReconcileNetwork() (err error) {
7272
func (s *Service) DeleteNetwork() (err error) {
7373
s.scope.V(2).Info("Deleting network")
7474

75-
// Search for a previously created and tagged VPC
76-
vpc, err := s.describeVPC()
77-
if err != nil {
78-
if awserrors.IsNotFound(err) {
79-
// If the VPC does not exist, nothing to do
80-
return nil
75+
vpc := &infrav1.VPCSpec{}
76+
// Get VPC used for the cluster
77+
if s.scope.VPC().ID != "" {
78+
var err error
79+
vpc, err = s.describeVPCByID()
80+
if err != nil {
81+
if awserrors.IsNotFound(err) {
82+
// If the VPC does not exist, nothing to do
83+
return nil
84+
}
85+
return err
8186
}
82-
return err
87+
} else {
88+
s.scope.Error(err, "non-fatal: VPC ID is missing, ")
8389
}
90+
8491
vpc.DeepCopyInto(s.scope.VPC())
8592

8693
// Secondary CIDR

pkg/cloud/services/network/vpc.go

Lines changed: 47 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,16 @@ package network
1919
import (
2020
"fmt"
2121

22-
kerrors "k8s.io/apimachinery/pkg/util/errors"
23-
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
24-
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/wait"
25-
2622
"github.com/aws/aws-sdk-go/aws"
2723
"github.com/aws/aws-sdk-go/service/ec2"
2824
"github.com/pkg/errors"
25+
kerrors "k8s.io/apimachinery/pkg/util/errors"
2926
infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1alpha4"
3027
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
3128
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/converters"
3229
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/filter"
30+
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
31+
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/wait"
3332
"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/tags"
3433
"sigs.k8s.io/cluster-api-provider-aws/pkg/record"
3534
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
@@ -43,55 +42,55 @@ const (
4342
func (s *Service) reconcileVPC() error {
4443
s.scope.V(2).Info("Reconciling VPC")
4544

46-
vpc, err := s.describeVPC()
47-
if awserrors.IsNotFound(err) { // nolint:nestif
48-
// Create a new managed vpc.
49-
if !conditions.Has(s.scope.InfraCluster(), infrav1.VpcReadyCondition) {
50-
conditions.MarkFalse(s.scope.InfraCluster(), infrav1.VpcReadyCondition, infrav1.VpcCreationStartedReason, clusterv1.ConditionSeverityInfo, "")
45+
// If the ID is not nil, VPC is either managed or unmanaged but should exist in the AWS.
46+
if s.scope.VPC().ID != "" {
47+
vpc, err := s.describeVPCByID()
48+
if err != nil {
49+
return errors.Wrap(err, ".spec.vpc.id is set but VPC resource is missing in AWS; failed to describe VPC resources. (might be in creation process)")
50+
}
51+
52+
s.scope.VPC().CidrBlock = vpc.CidrBlock
53+
s.scope.VPC().Tags = vpc.Tags
54+
55+
// If VPC is unmanaged, return early.
56+
if vpc.IsUnmanaged(s.scope.Name()) {
57+
s.scope.V(2).Info("Working on unmanaged VPC", "vpc-id", vpc.ID)
5158
if err := s.scope.PatchObject(); err != nil {
52-
return errors.Wrap(err, "failed to patch conditions")
59+
return errors.Wrap(err, "failed to patch unmanaged VPC fields")
5360
}
61+
record.Eventf(s.scope.InfraCluster(), "SuccessfulSetVPCAttributes", "Set managed VPC attributes for %q", vpc.ID)
62+
return nil
5463
}
55-
vpc, err = s.createVPC()
56-
if err != nil {
57-
return errors.Wrap(err, "failed to create new vpc")
58-
}
59-
} else if err != nil {
60-
return errors.Wrap(err, "failed to describe VPCs")
61-
}
6264

63-
// This function creates a new infrav1.VPCSpec, populates it with data from AWS, and then deep copies into the
64-
// AWSCluster's VPC spec (see the DeepCopyInto lines below). This is potentially problematic, as it completely
65-
// overwrites the data for the VPC spec as retrieved from the apiserver. This is a temporary band-aid to restore
66-
// recently-added fields that descripe user intent and do not come from AWS resource descriptions.
67-
//
68-
// FIXME(ncdc): rather than copying these values from the scope to vpc, find a better way to merge AWS information
69-
// with data in the scope retrieved from the apiserver. Could use something like mergo.
70-
//
71-
// NOTE: it may look like we are losing InternetGatewayID because it's not populated by describeVPC/createVPC or
72-
// restored here, but that's ok. It is restored by reconcileInternetGateways, which is invoked after this.
73-
vpc.AvailabilityZoneSelection = s.scope.VPC().AvailabilityZoneSelection
74-
vpc.AvailabilityZoneUsageLimit = s.scope.VPC().AvailabilityZoneUsageLimit
65+
// if the VPC is managed, make managed sure attributes are configured.
66+
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
67+
if err := s.ensureManagedVPCAttributes(vpc); err != nil {
68+
return false, err
69+
}
70+
return true, nil
71+
}, awserrors.VPCNotFound); err != nil {
72+
return errors.Wrapf(err, "failed to to set vpc attributes for %q", vpc.ID)
73+
}
7574

76-
if vpc.IsUnmanaged(s.scope.Name()) {
77-
vpc.DeepCopyInto(s.scope.VPC())
78-
s.scope.V(2).Info("Working on unmanaged VPC", "vpc-id", vpc.ID)
7975
return nil
8076
}
8177

82-
// Make sure attributes are configured
83-
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
84-
buildParams := s.getVPCTagParams(vpc.ID)
85-
tagsBuilder := tags.New(&buildParams, tags.WithEC2(s.EC2Client))
86-
if err := tagsBuilder.Ensure(vpc.Tags); err != nil {
87-
return false, err
78+
// .spec.vpc.id is nil, Create a new managed vpc.
79+
if !conditions.Has(s.scope.InfraCluster(), infrav1.VpcReadyCondition) {
80+
conditions.MarkFalse(s.scope.InfraCluster(), infrav1.VpcReadyCondition, infrav1.VpcCreationStartedReason, clusterv1.ConditionSeverityInfo, "")
81+
if err := s.scope.PatchObject(); err != nil {
82+
return errors.Wrap(err, "failed to patch conditions")
8883
}
89-
return true, nil
90-
}, awserrors.VPCNotFound); err != nil {
91-
record.Warnf(s.scope.InfraCluster(), "FailedTagVPC", "Failed to tag managed VPC %q: %v", vpc.ID, err)
92-
return errors.Wrapf(err, "failed to tag vpc %q", vpc.ID)
84+
}
85+
vpc, err := s.createVPC()
86+
if err != nil {
87+
return errors.Wrap(err, "failed to create new vpc")
9388
}
9489

90+
s.scope.VPC().CidrBlock = vpc.CidrBlock
91+
s.scope.VPC().Tags = vpc.Tags
92+
s.scope.VPC().ID = vpc.ID
93+
9594
// Make sure attributes are configured
9695
if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {
9796
if err := s.ensureManagedVPCAttributes(vpc); err != nil {
@@ -102,8 +101,6 @@ func (s *Service) reconcileVPC() error {
102101
return errors.Wrapf(err, "failed to to set vpc attributes for %q", vpc.ID)
103102
}
104103

105-
vpc.DeepCopyInto(s.scope.VPC())
106-
s.scope.V(2).Info("Working on managed VPC", "vpc-id", vpc.ID)
107104
return nil
108105
}
109106

@@ -165,10 +162,6 @@ func (s *Service) ensureManagedVPCAttributes(vpc *infrav1.VPCSpec) error {
165162
}
166163

167164
func (s *Service) createVPC() (*infrav1.VPCSpec, error) {
168-
if s.scope.VPC().IsUnmanaged(s.scope.Name()) {
169-
return nil, errors.Errorf("cannot create a managed vpc in unmanaged mode")
170-
}
171-
172165
if s.scope.VPC().CidrBlock == "" {
173166
s.scope.VPC().CidrBlock = defaultVPCCidr
174167
}
@@ -189,15 +182,6 @@ func (s *Service) createVPC() (*infrav1.VPCSpec, error) {
189182
record.Eventf(s.scope.InfraCluster(), "SuccessfulCreateVPC", "Created new managed VPC %q", *out.Vpc.VpcId)
190183
s.scope.V(2).Info("Created new VPC with cidr", "vpc-id", *out.Vpc.VpcId, "cidr-block", *out.Vpc.CidrBlock)
191184

192-
// TODO: we should attempt to record the VPC ID as soon as possible by setting s.scope.VPC().ID
193-
// however, the logic used for determining managed vs unmanaged VPCs relies on the tags and will
194-
// need to be updated to accommodate for the recording of the VPC ID prior to the tagging.
195-
196-
wReq := &ec2.DescribeVpcsInput{VpcIds: []*string{out.Vpc.VpcId}}
197-
if err := s.EC2Client.WaitUntilVpcAvailable(wReq); err != nil {
198-
return nil, errors.Wrapf(err, "failed to wait for vpc %q", *out.Vpc.VpcId)
199-
}
200-
201185
return &infrav1.VPCSpec{
202186
ID: *out.Vpc.VpcId,
203187
CidrBlock: *out.Vpc.CidrBlock,
@@ -232,19 +216,18 @@ func (s *Service) deleteVPC() error {
232216
return nil
233217
}
234218

235-
func (s *Service) describeVPC() (*infrav1.VPCSpec, error) {
219+
func (s *Service) describeVPCByID() (*infrav1.VPCSpec, error) {
220+
if s.scope.VPC().ID == "" {
221+
return nil, errors.New("VPC ID is not set, failed to describe VPCs by ID")
222+
}
223+
236224
input := &ec2.DescribeVpcsInput{
237225
Filters: []*ec2.Filter{
238226
filter.EC2.VPCStates(ec2.VpcStatePending, ec2.VpcStateAvailable),
239227
},
240228
}
241229

242-
if s.scope.VPC().ID == "" {
243-
// Try to find a previously created and tagged VPC
244-
input.Filters = append(input.Filters, filter.EC2.Cluster(s.scope.Name()))
245-
} else {
246-
input.VpcIds = []*string{aws.String(s.scope.VPC().ID)}
247-
}
230+
input.VpcIds = []*string{aws.String(s.scope.VPC().ID)}
248231

249232
out, err := s.EC2Client.DescribeVpcs(input)
250233
if err != nil {

pkg/cloud/services/network/vpc_test.go

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ import (
2121
"reflect"
2222
"testing"
2323

24+
. "github.com/onsi/gomega"
25+
2426
"github.com/aws/aws-sdk-go/aws"
27+
"github.com/aws/aws-sdk-go/aws/awserr"
2528
"github.com/aws/aws-sdk-go/service/ec2"
2629
"github.com/golang/mock/gomock"
30+
"github.com/pkg/errors"
2731
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2832
"k8s.io/apimachinery/pkg/runtime"
2933
"k8s.io/utils/diff"
@@ -68,13 +72,14 @@ func TestReconcileVPC(t *testing.T) {
6872
selection := infrav1.AZSelectionSchemeOrdered
6973

7074
testCases := []struct {
71-
name string
72-
input *infrav1.VPCSpec
73-
expected *infrav1.VPCSpec
74-
expect func(m *mock_ec2iface.MockEC2APIMockRecorder)
75+
name string
76+
input *infrav1.VPCSpec
77+
expected *infrav1.VPCSpec
78+
expect func(m *mock_ec2iface.MockEC2APIMockRecorder)
79+
expectError bool
7580
}{
7681
{
77-
name: "managed vpc exists",
82+
name: "if unmanaged vpc exists, updates tags with aws VPC resource tags",
7883
input: &infrav1.VPCSpec{ID: "vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
7984
expected: &infrav1.VPCSpec{
8085
ID: "vpc-exists",
@@ -87,6 +92,7 @@ func TestReconcileVPC(t *testing.T) {
8792
AvailabilityZoneUsageLimit: &usageLimit,
8893
AvailabilityZoneSelection: &selection,
8994
},
95+
expectError: false,
9096
expect: func(m *mock_ec2iface.MockEC2APIMockRecorder) {
9197
m.DescribeVpcs(gomock.Eq(&ec2.DescribeVpcsInput{
9298
VpcIds: []*string{
@@ -128,8 +134,9 @@ func TestReconcileVPC(t *testing.T) {
128134
},
129135
},
130136
{
131-
name: "managed vpc does not exist",
132-
input: &infrav1.VPCSpec{AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
137+
name: "if managed vpc does not exist, creates a new VPC",
138+
input: &infrav1.VPCSpec{AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
139+
expectError: false,
133140
expected: &infrav1.VPCSpec{
134141
ID: "vpc-new",
135142
CidrBlock: "10.1.0.0/16",
@@ -142,20 +149,6 @@ func TestReconcileVPC(t *testing.T) {
142149
AvailabilityZoneSelection: &selection,
143150
},
144151
expect: func(m *mock_ec2iface.MockEC2APIMockRecorder) {
145-
m.DescribeVpcs(gomock.Eq(&ec2.DescribeVpcsInput{
146-
Filters: []*ec2.Filter{
147-
{
148-
Name: aws.String("state"),
149-
Values: aws.StringSlice([]string{ec2.VpcStatePending, ec2.VpcStateAvailable}),
150-
},
151-
{
152-
Name: aws.String("tag-key"),
153-
Values: aws.StringSlice([]string{"sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"}),
154-
},
155-
},
156-
})).
157-
Return(&ec2.DescribeVpcsOutput{}, nil)
158-
159152
m.CreateVpc(gomock.AssignableToTypeOf(&ec2.CreateVpcInput{})).
160153
Return(&ec2.CreateVpcOutput{
161154
Vpc: &ec2.Vpc{
@@ -184,11 +177,25 @@ func TestReconcileVPC(t *testing.T) {
184177

185178
m.ModifyVpcAttribute(gomock.AssignableToTypeOf(&ec2.ModifyVpcAttributeInput{})).
186179
Return(&ec2.ModifyVpcAttributeOutput{}, nil).Times(2)
187-
188-
m.WaitUntilVpcAvailable(gomock.Eq(&ec2.DescribeVpcsInput{
189-
VpcIds: []*string{aws.String("vpc-new")},
180+
},
181+
},
182+
{
183+
name: "managed vpc id exists, but vpc resource is missing",
184+
input: &infrav1.VPCSpec{ID: "vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
185+
expectError: true,
186+
expect: func(m *mock_ec2iface.MockEC2APIMockRecorder) {
187+
m.DescribeVpcs(gomock.Eq(&ec2.DescribeVpcsInput{
188+
VpcIds: []*string{
189+
aws.String("vpc-exists"),
190+
},
191+
Filters: []*ec2.Filter{
192+
{
193+
Name: aws.String("state"),
194+
Values: aws.StringSlice([]string{ec2.VpcStatePending, ec2.VpcStateAvailable}),
195+
},
196+
},
190197
})).
191-
Return(nil)
198+
Return(nil, awserr.New("404", "http not found err", errors.New("err")))
192199
},
193200
},
194201
}
@@ -226,9 +233,14 @@ func TestReconcileVPC(t *testing.T) {
226233

227234
s := NewService(clusterScope)
228235
s.EC2Client = ec2Mock
229-
230-
if err := s.reconcileVPC(); err != nil {
231-
t.Fatalf("got an unexpected error: %v", err)
236+
g := NewWithT(t)
237+
238+
err = s.reconcileVPC()
239+
if tc.expectError {
240+
g.Expect(err).ToNot(BeNil())
241+
return
242+
} else {
243+
g.Expect(err).To(BeNil())
232244
}
233245

234246
if !reflect.DeepEqual(tc.expected, &clusterScope.AWSCluster.Spec.NetworkSpec.VPC) {

0 commit comments

Comments
 (0)