Skip to content

Commit feb799e

Browse files
committed
ROSANetwork: tests
1 parent 56760b6 commit feb799e

File tree

3 files changed

+504
-1
lines changed

3 files changed

+504
-1
lines changed
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
/*
2+
Copyright The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package controllers
15+
16+
import (
17+
"context"
18+
"fmt"
19+
"testing"
20+
"time"
21+
22+
awsSdk "github.com/aws/aws-sdk-go-v2/aws"
23+
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
24+
cloudformationtypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
25+
"github.com/aws/aws-sdk-go-v2/service/ec2"
26+
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
27+
stsv2 "github.com/aws/aws-sdk-go-v2/service/sts"
28+
"github.com/aws/smithy-go"
29+
. "github.com/onsi/gomega"
30+
rosaAWSClient "github.com/openshift/rosa/pkg/aws"
31+
rosaMocks "github.com/openshift/rosa/pkg/aws/mocks"
32+
"github.com/sirupsen/logrus"
33+
gomock "go.uber.org/mock/gomock"
34+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35+
"k8s.io/apimachinery/pkg/types"
36+
ctrl "sigs.k8s.io/controller-runtime"
37+
38+
infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
39+
expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2"
40+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
41+
"sigs.k8s.io/cluster-api/util/conditions"
42+
)
43+
44+
func TestROSANetworkReconciler_Reconcile(t *testing.T) {
45+
g := NewWithT(t)
46+
ns, err := testEnv.CreateNamespace(ctx, "test-namespace")
47+
g.Expect(err).ToNot(HaveOccurred())
48+
49+
mockCtrl := gomock.NewController(t)
50+
ctx := context.TODO()
51+
52+
identity := &infrav1.AWSClusterControllerIdentity{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Name: "default",
55+
},
56+
Spec: infrav1.AWSClusterControllerIdentitySpec{
57+
AWSClusterIdentitySpec: infrav1.AWSClusterIdentitySpec{
58+
AllowedNamespaces: &infrav1.AllowedNamespaces{},
59+
},
60+
},
61+
}
62+
identity.SetGroupVersionKind(infrav1.GroupVersion.WithKind("AWSClusterControllerIdentity"))
63+
64+
rosaNetwork := &expinfrav1.ROSANetwork{
65+
ObjectMeta: metav1.ObjectMeta{
66+
Name: "test-rosa-network",
67+
Namespace: ns.Name},
68+
Spec: expinfrav1.ROSANetworkSpec{
69+
StackName: "test-rosa-network",
70+
CIDRBlock: "10.0.0.0/8",
71+
AvailabilityZoneCount: 1,
72+
Region: "test-region",
73+
IdentityRef: &infrav1.AWSIdentityReference{
74+
Name: identity.Name,
75+
Kind: infrav1.ControllerIdentityKind,
76+
},
77+
},
78+
}
79+
80+
t.Run("Empty result when ROSANetwork object not found", func(t *testing.T) {
81+
_, _, _, reconciler := createMocks(mockCtrl)
82+
83+
req := ctrl.Request{}
84+
req.NamespacedName = types.NamespacedName{Name: rosaNetwork.Name, Namespace: rosaNetwork.Namespace}
85+
reqReconcile, errReconcile := reconciler.Reconcile(ctx, req)
86+
87+
g.Expect(reqReconcile.Requeue).To(BeFalse())
88+
g.Expect(reqReconcile.RequeueAfter).To(Equal(time.Duration(0)))
89+
g.Expect(errReconcile).ToNot(HaveOccurred())
90+
})
91+
92+
t.Run("Error result when CF stack GET returns error", func(t *testing.T) {
93+
_, mockCFClient, mockSTSClient, reconciler := createMocks(mockCtrl)
94+
95+
createObject(g, rosaNetwork, ns.Name)
96+
createObject(g, identity, ns.Name)
97+
98+
describeStacksOutput := &cloudformation.DescribeStacksOutput{}
99+
clientErr := fmt.Errorf("test-error")
100+
101+
mockCFClient.EXPECT().DescribeStacks(gomock.Any(), gomock.Any(), gomock.Any()).
102+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStacksInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
103+
return describeStacksOutput, clientErr
104+
}).Times(1)
105+
106+
getCallerIdentityResult := &stsv2.GetCallerIdentityOutput{Account: awsSdk.String("foo"), Arn: awsSdk.String("arn:aws:iam::123456789012:rosa/foo")}
107+
mockSTSClient.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(getCallerIdentityResult, nil).AnyTimes()
108+
109+
req := ctrl.Request{}
110+
req.NamespacedName = types.NamespacedName{Name: rosaNetwork.Name, Namespace: rosaNetwork.Namespace}
111+
reqReconcile, errReconcile := reconciler.Reconcile(ctx, req)
112+
113+
g.Expect(reqReconcile.Requeue).To(BeFalse())
114+
g.Expect(reqReconcile.RequeueAfter).To(Equal(time.Duration(0)))
115+
g.Expect(errReconcile).To(MatchError(ContainSubstring("error fetching CF stack details:")))
116+
})
117+
118+
t.Run("Initial CF stack creation fails", func(t *testing.T) {
119+
_, mockCFClient, mockSTSClient, reconciler := createMocks(mockCtrl)
120+
121+
describeStacksOutput := &cloudformation.DescribeStacksOutput{}
122+
validationErr := &smithy.GenericAPIError{
123+
Code: "ValidationError",
124+
Message: "ValidationError",
125+
Fault: smithy.FaultServer,
126+
}
127+
128+
mockCFClient.EXPECT().DescribeStacks(gomock.Any(), gomock.Any(), gomock.Any()).
129+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStacksInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
130+
return describeStacksOutput, validationErr
131+
}).AnyTimes()
132+
133+
createStackOutput := &cloudformation.CreateStackOutput{}
134+
createStackErr := fmt.Errorf("test-error")
135+
136+
mockCFClient.EXPECT().CreateStack(gomock.Any(), gomock.Any(), gomock.Any()).
137+
DoAndReturn(func(_ context.Context, _ *cloudformation.CreateStackInput, _ ...func(*cloudformation.Options)) (*cloudformation.CreateStackOutput, error) {
138+
return createStackOutput, createStackErr
139+
}).Times(1)
140+
141+
getCallerIdentityResult := &stsv2.GetCallerIdentityOutput{Account: awsSdk.String("foo"), Arn: awsSdk.String("arn:aws:iam::123456789012:rosa/foo")}
142+
mockSTSClient.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(getCallerIdentityResult, nil).AnyTimes()
143+
144+
req := ctrl.Request{}
145+
req.NamespacedName = types.NamespacedName{Name: rosaNetwork.Name, Namespace: rosaNetwork.Namespace}
146+
reqReconcile, errReconcile := reconciler.Reconcile(ctx, req)
147+
148+
g.Expect(reqReconcile.Requeue).To(BeFalse())
149+
g.Expect(reqReconcile.RequeueAfter).To(Equal(time.Duration(0)))
150+
g.Expect(errReconcile).To(MatchError(ContainSubstring("failed to start CF stack creation:")))
151+
152+
updatedROSANetwork := &expinfrav1.ROSANetwork{}
153+
err = reconciler.Client.Get(ctx, req.NamespacedName, updatedROSANetwork)
154+
g.Expect(err).ToNot(HaveOccurred())
155+
cnd := conditions.Get(updatedROSANetwork, expinfrav1.ROSANetworkReadyCondition)
156+
g.Expect(cnd).ToNot(BeNil())
157+
g.Expect(cnd.Reason).To(Equal(expinfrav1.ROSANetworkFailedReason))
158+
g.Expect(cnd.Severity).To(Equal(clusterv1.ConditionSeverityError))
159+
g.Expect(cnd.Message).To(Equal("test-error"))
160+
})
161+
162+
t.Run("Initial CF stack creation succeeds", func(t *testing.T) {
163+
_, mockCFClient, mockSTSClient, reconciler := createMocks(mockCtrl)
164+
165+
describeStacksOutput := &cloudformation.DescribeStacksOutput{}
166+
validationErr := &smithy.GenericAPIError{
167+
Code: "ValidationError",
168+
Message: "ValidationError",
169+
Fault: smithy.FaultServer,
170+
}
171+
172+
mockCFClient.EXPECT().DescribeStacks(gomock.Any(), gomock.Any(), gomock.Any()).
173+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStacksInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
174+
return describeStacksOutput, validationErr
175+
}).AnyTimes()
176+
177+
createStackOutput := &cloudformation.CreateStackOutput{}
178+
179+
mockCFClient.EXPECT().CreateStack(gomock.Any(), gomock.Any(), gomock.Any()).
180+
DoAndReturn(func(_ context.Context, _ *cloudformation.CreateStackInput, _ ...func(*cloudformation.Options)) (*cloudformation.CreateStackOutput, error) {
181+
return createStackOutput, nil
182+
}).Times(1)
183+
184+
getCallerIdentityResult := &stsv2.GetCallerIdentityOutput{Account: awsSdk.String("foo"), Arn: awsSdk.String("arn:aws:iam::123456789012:rosa/foo")}
185+
mockSTSClient.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(getCallerIdentityResult, nil).AnyTimes()
186+
187+
req := ctrl.Request{}
188+
req.NamespacedName = types.NamespacedName{Name: rosaNetwork.Name, Namespace: rosaNetwork.Namespace}
189+
reqReconcile, errReconcile := reconciler.Reconcile(ctx, req)
190+
191+
g.Expect(reqReconcile.Requeue).To(BeFalse())
192+
g.Expect(reqReconcile.RequeueAfter).To(Equal(time.Duration(0)))
193+
g.Expect(errReconcile).ToNot(HaveOccurred())
194+
195+
updatedROSANetwork := &expinfrav1.ROSANetwork{}
196+
err = reconciler.Client.Get(ctx, req.NamespacedName, updatedROSANetwork)
197+
g.Expect(err).ToNot(HaveOccurred())
198+
cnd := conditions.Get(updatedROSANetwork, expinfrav1.ROSANetworkReadyCondition)
199+
g.Expect(cnd).ToNot(BeNil())
200+
g.Expect(cnd.Reason).To(Equal(expinfrav1.ROSANetworkCreatingReason))
201+
g.Expect(cnd.Severity).To(Equal(clusterv1.ConditionSeverityInfo))
202+
})
203+
204+
cleanupObject(g, rosaNetwork)
205+
cleanupObject(g, identity)
206+
}
207+
208+
func TestROSANetworkReconciler_updateROSANetworkResources(t *testing.T) {
209+
g := NewWithT(t)
210+
mockCtrl := gomock.NewController(t)
211+
ctx := context.TODO()
212+
213+
rosaNetwork := &expinfrav1.ROSANetwork{
214+
ObjectMeta: metav1.ObjectMeta{
215+
Name: "test-rosa-network",
216+
Namespace: "test-namespace",
217+
},
218+
Spec: expinfrav1.ROSANetworkSpec{},
219+
Status: expinfrav1.ROSANetworkStatus{},
220+
}
221+
222+
t.Run("Handle cloudformation client error", func(t *testing.T) {
223+
_, mockCFClient, _, reconciler := createMocks(mockCtrl)
224+
225+
describeStackResourcesOutput := &cloudformation.DescribeStackResourcesOutput{}
226+
clientErr := fmt.Errorf("test-error")
227+
228+
mockCFClient.EXPECT().DescribeStackResources(gomock.Any(), gomock.Any(), gomock.Any()).
229+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStackResourcesInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) {
230+
return describeStackResourcesOutput, clientErr
231+
}).Times(1)
232+
233+
err := reconciler.updateROSANetworkResources(ctx, rosaNetwork)
234+
g.Expect(err).To(HaveOccurred())
235+
g.Expect(len(rosaNetwork.Status.Resources)).To(Equal(0))
236+
})
237+
238+
t.Run("Update ROSANetwork.Status.Resources", func(t *testing.T) {
239+
_, mockCFClient, _, reconciler := createMocks(mockCtrl)
240+
241+
logicalResourceID := "logical-resource-id"
242+
resourceStatus := cloudformationtypes.ResourceStatusCreateComplete
243+
resourceType := "resource-type"
244+
resourceStatusReason := "resource-status-reason"
245+
physicalResourceID := "physical-resource-id"
246+
247+
describeStackResourcesOutput := &cloudformation.DescribeStackResourcesOutput{
248+
StackResources: []cloudformationtypes.StackResource{
249+
{
250+
LogicalResourceId: &logicalResourceID,
251+
ResourceStatus: resourceStatus,
252+
ResourceType: &resourceType,
253+
ResourceStatusReason: &resourceStatusReason,
254+
PhysicalResourceId: &physicalResourceID,
255+
},
256+
},
257+
}
258+
259+
mockCFClient.EXPECT().DescribeStackResources(gomock.Any(), gomock.Any(), gomock.Any()).
260+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStackResourcesInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) {
261+
return describeStackResourcesOutput, nil
262+
}).Times(1)
263+
264+
err := reconciler.updateROSANetworkResources(ctx, rosaNetwork)
265+
g.Expect(err).ToNot(HaveOccurred())
266+
g.Expect(rosaNetwork.Status.Resources[0].LogicalID).To(Equal(logicalResourceID))
267+
g.Expect(rosaNetwork.Status.Resources[0].Status).To(Equal(string(resourceStatus)))
268+
g.Expect(rosaNetwork.Status.Resources[0].ResourceType).To(Equal(resourceType))
269+
g.Expect(rosaNetwork.Status.Resources[0].Reason).To(Equal(resourceStatusReason))
270+
g.Expect(rosaNetwork.Status.Resources[0].PhysicalID).To(Equal(physicalResourceID))
271+
})
272+
}
273+
274+
func TestROSANetworkReconciler_parseSubnets(t *testing.T) {
275+
g := NewWithT(t)
276+
mockCtrl := gomock.NewController(t)
277+
278+
subnet1Id := "subnet1-physical-id"
279+
subnet2Id := "subnet2-physical-id"
280+
281+
rosaNetwork := &expinfrav1.ROSANetwork{
282+
ObjectMeta: metav1.ObjectMeta{
283+
Name: "test-rosa-network",
284+
Namespace: "test-namespace",
285+
},
286+
Spec: expinfrav1.ROSANetworkSpec{},
287+
Status: expinfrav1.ROSANetworkStatus{
288+
Resources: []expinfrav1.CFResource{
289+
{
290+
ResourceType: "AWS::EC2::Subnet",
291+
LogicalID: "SubnetPrivate",
292+
PhysicalID: subnet1Id,
293+
Status: "subnet1-status",
294+
Reason: "subnet1-reason",
295+
},
296+
{
297+
ResourceType: "AWS::EC2::Subnet",
298+
LogicalID: "SubnetPublic",
299+
PhysicalID: subnet2Id,
300+
Status: "subnet2-status",
301+
Reason: "subnet2-reason",
302+
},
303+
{
304+
ResourceType: "bogus-type",
305+
LogicalID: "bogus-logical-id",
306+
PhysicalID: "bugus-physical-id",
307+
Status: "bogus-status",
308+
Reason: "bogus-reason",
309+
},
310+
},
311+
},
312+
}
313+
314+
t.Run("Handle EC2 client error", func(t *testing.T) {
315+
mockEC2Client, _, _, reconciler := createMocks(mockCtrl)
316+
317+
describeSubnetsOutput := &ec2.DescribeSubnetsOutput{}
318+
319+
mockEC2Client.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).
320+
DoAndReturn(func(_ context.Context, _ *ec2.DescribeSubnetsInput, _ ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) {
321+
return describeSubnetsOutput, fmt.Errorf("test-error")
322+
}).Times(1)
323+
324+
err := reconciler.parseSubnets(rosaNetwork)
325+
g.Expect(err).To(HaveOccurred())
326+
g.Expect(len(rosaNetwork.Status.Subnets)).To(Equal(0))
327+
})
328+
329+
t.Run("Update ROSANetwork.Status.Subnets", func(t *testing.T) {
330+
mockEC2Client, _, _, reconciler := createMocks(mockCtrl)
331+
332+
az := "az01"
333+
334+
describeSubnetsOutput := &ec2.DescribeSubnetsOutput{
335+
Subnets: []ec2Types.Subnet{
336+
{
337+
AvailabilityZone: &az,
338+
},
339+
},
340+
}
341+
342+
mockEC2Client.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).
343+
DoAndReturn(func(_ context.Context, _ *ec2.DescribeSubnetsInput, _ ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) {
344+
return describeSubnetsOutput, nil
345+
}).Times(2)
346+
347+
err := reconciler.parseSubnets(rosaNetwork)
348+
g.Expect(err).ToNot(HaveOccurred())
349+
g.Expect(rosaNetwork.Status.Subnets[0].AvailabilityZone).To(Equal(az))
350+
g.Expect(rosaNetwork.Status.Subnets[0].PrivateSubnet).To(Equal(subnet1Id))
351+
g.Expect(rosaNetwork.Status.Subnets[0].PublicSubnet).To(Equal(subnet2Id))
352+
})
353+
}
354+
355+
func createMocks(mockCtrl *gomock.Controller) (*rosaMocks.MockEc2ApiClient, *rosaMocks.MockCloudFormationApiClient, *rosaMocks.MockStsApiClient, *ROSANetworkReconciler) {
356+
mockEC2Client := rosaMocks.NewMockEc2ApiClient(mockCtrl)
357+
mockCFClient := rosaMocks.NewMockCloudFormationApiClient(mockCtrl)
358+
mockSTSClient := rosaMocks.NewMockStsApiClient(mockCtrl)
359+
awsClient := rosaAWSClient.New(
360+
awsSdk.Config{},
361+
rosaAWSClient.NewLoggerWrapper(logrus.New(), nil),
362+
rosaMocks.NewMockIamApiClient(mockCtrl),
363+
mockEC2Client,
364+
rosaMocks.NewMockOrganizationsApiClient(mockCtrl),
365+
rosaMocks.NewMockS3ApiClient(mockCtrl),
366+
rosaMocks.NewMockSecretsManagerApiClient(mockCtrl),
367+
mockSTSClient,
368+
mockCFClient,
369+
rosaMocks.NewMockServiceQuotasApiClient(mockCtrl),
370+
rosaMocks.NewMockServiceQuotasApiClient(mockCtrl),
371+
&rosaAWSClient.AccessKey{},
372+
false,
373+
)
374+
375+
reconciler := &ROSANetworkReconciler{
376+
Client: testEnv.Client,
377+
awsClient: awsClient,
378+
}
379+
380+
return mockEC2Client, mockCFClient, mockSTSClient, reconciler
381+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
github.com/spf13/cobra v1.9.1
5050
github.com/spf13/pflag v1.0.6
5151
github.com/zgalor/weberr v0.8.2
52+
go.uber.org/mock v0.5.2
5253
golang.org/x/crypto v0.36.0
5354
golang.org/x/net v0.38.0
5455
golang.org/x/text v0.23.0
@@ -77,7 +78,6 @@ require (
7778
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
7879
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
7980
github.com/robfig/cron/v3 v3.0.1 // indirect
80-
go.uber.org/mock v0.5.2 // indirect
8181
)
8282

8383
require (

0 commit comments

Comments
 (0)