Skip to content

Commit 104652a

Browse files
committed
ROSANetwork: tests
1 parent 56760b6 commit 104652a

File tree

3 files changed

+460
-1
lines changed

3 files changed

+460
-1
lines changed
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
cnd := conditions.Get(updatedROSANetwork, expinfrav1.ROSANetworkReadyCondition)
155+
g.Expect(cnd).ToNot(BeNil())
156+
g.Expect(cnd.Reason).To(Equal(expinfrav1.ROSANetworkFailedReason))
157+
g.Expect(cnd.Severity).To(Equal(clusterv1.ConditionSeverityError))
158+
})
159+
160+
cleanupObject(g, rosaNetwork)
161+
cleanupObject(g, identity)
162+
}
163+
164+
func TestROSANetworkReconciler_updateROSANetworkResources(t *testing.T) {
165+
g := NewWithT(t)
166+
mockCtrl := gomock.NewController(t)
167+
ctx := context.TODO()
168+
169+
rosaNetwork := &expinfrav1.ROSANetwork{
170+
ObjectMeta: metav1.ObjectMeta{
171+
Name: "test-rosa-network",
172+
Namespace: "test-namespace",
173+
},
174+
Spec: expinfrav1.ROSANetworkSpec{},
175+
Status: expinfrav1.ROSANetworkStatus{},
176+
}
177+
178+
t.Run("Handle cloudformation client error", func(t *testing.T) {
179+
_, mockCFClient, _, reconciler := createMocks(mockCtrl)
180+
181+
describeStackResourcesOutput := &cloudformation.DescribeStackResourcesOutput{}
182+
clientErr := fmt.Errorf("test-error")
183+
184+
mockCFClient.EXPECT().DescribeStackResources(gomock.Any(), gomock.Any(), gomock.Any()).
185+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStackResourcesInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) {
186+
return describeStackResourcesOutput, clientErr
187+
}).Times(1)
188+
189+
err := reconciler.updateROSANetworkResources(ctx, rosaNetwork)
190+
g.Expect(err).To(HaveOccurred())
191+
g.Expect(len(rosaNetwork.Status.Resources)).To(Equal(0))
192+
})
193+
194+
t.Run("Update ROSANetwork.Status.Resources", func(t *testing.T) {
195+
_, mockCFClient, _, reconciler := createMocks(mockCtrl)
196+
197+
logicalResourceID := "logical-resource-id"
198+
resourceStatus := cloudformationtypes.ResourceStatusCreateComplete
199+
resourceType := "resource-type"
200+
resourceStatusReason := "resource-status-reason"
201+
physicalResourceID := "physical-resource-id"
202+
203+
describeStackResourcesOutput := &cloudformation.DescribeStackResourcesOutput{
204+
StackResources: []cloudformationtypes.StackResource{
205+
{
206+
LogicalResourceId: &logicalResourceID,
207+
ResourceStatus: resourceStatus,
208+
ResourceType: &resourceType,
209+
ResourceStatusReason: &resourceStatusReason,
210+
PhysicalResourceId: &physicalResourceID,
211+
},
212+
},
213+
}
214+
215+
mockCFClient.EXPECT().DescribeStackResources(gomock.Any(), gomock.Any(), gomock.Any()).
216+
DoAndReturn(func(_ context.Context, _ *cloudformation.DescribeStackResourcesInput, _ ...func(*cloudformation.Options)) (*cloudformation.DescribeStackResourcesOutput, error) {
217+
return describeStackResourcesOutput, nil
218+
}).Times(1)
219+
220+
err := reconciler.updateROSANetworkResources(ctx, rosaNetwork)
221+
g.Expect(err).ToNot(HaveOccurred())
222+
g.Expect(rosaNetwork.Status.Resources[0].LogicalID).To(Equal(logicalResourceID))
223+
g.Expect(rosaNetwork.Status.Resources[0].Status).To(Equal(string(resourceStatus)))
224+
g.Expect(rosaNetwork.Status.Resources[0].ResourceType).To(Equal(resourceType))
225+
g.Expect(rosaNetwork.Status.Resources[0].Reason).To(Equal(resourceStatusReason))
226+
g.Expect(rosaNetwork.Status.Resources[0].PhysicalID).To(Equal(physicalResourceID))
227+
})
228+
}
229+
230+
func TestROSANetworkReconciler_parseSubnets(t *testing.T) {
231+
g := NewWithT(t)
232+
mockCtrl := gomock.NewController(t)
233+
234+
subnet1Id := "subnet1-physical-id"
235+
subnet2Id := "subnet2-physical-id"
236+
237+
rosaNetwork := &expinfrav1.ROSANetwork{
238+
ObjectMeta: metav1.ObjectMeta{
239+
Name: "test-rosa-network",
240+
Namespace: "test-namespace",
241+
},
242+
Spec: expinfrav1.ROSANetworkSpec{},
243+
Status: expinfrav1.ROSANetworkStatus{
244+
Resources: []expinfrav1.CFResource{
245+
{
246+
ResourceType: "AWS::EC2::Subnet",
247+
LogicalID: "SubnetPrivate",
248+
PhysicalID: subnet1Id,
249+
Status: "subnet1-status",
250+
Reason: "subnet1-reason",
251+
},
252+
{
253+
ResourceType: "AWS::EC2::Subnet",
254+
LogicalID: "SubnetPublic",
255+
PhysicalID: subnet2Id,
256+
Status: "subnet2-status",
257+
Reason: "subnet2-reason",
258+
},
259+
{
260+
ResourceType: "bogus-type",
261+
LogicalID: "bogus-logical-id",
262+
PhysicalID: "bugus-physical-id",
263+
Status: "bogus-status",
264+
Reason: "bogus-reason",
265+
},
266+
},
267+
},
268+
}
269+
270+
t.Run("Handle EC2 client error", func(t *testing.T) {
271+
mockEC2Client, _, _, reconciler := createMocks(mockCtrl)
272+
273+
describeSubnetsOutput := &ec2.DescribeSubnetsOutput{}
274+
275+
mockEC2Client.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).
276+
DoAndReturn(func(_ context.Context, _ *ec2.DescribeSubnetsInput, _ ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) {
277+
return describeSubnetsOutput, fmt.Errorf("test-error")
278+
}).Times(1)
279+
280+
err := reconciler.parseSubnets(rosaNetwork)
281+
g.Expect(err).To(HaveOccurred())
282+
g.Expect(len(rosaNetwork.Status.Subnets)).To(Equal(0))
283+
})
284+
285+
t.Run("Update ROSANetwork.Status.Subnets", func(t *testing.T) {
286+
mockEC2Client, _, _, reconciler := createMocks(mockCtrl)
287+
288+
az := "az01"
289+
290+
describeSubnetsOutput := &ec2.DescribeSubnetsOutput{
291+
Subnets: []ec2Types.Subnet{
292+
{
293+
AvailabilityZone: &az,
294+
},
295+
},
296+
}
297+
298+
mockEC2Client.EXPECT().DescribeSubnets(gomock.Any(), gomock.Any(), gomock.Any()).
299+
DoAndReturn(func(_ context.Context, _ *ec2.DescribeSubnetsInput, _ ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) {
300+
return describeSubnetsOutput, nil
301+
}).Times(2)
302+
303+
err := reconciler.parseSubnets(rosaNetwork)
304+
g.Expect(err).ToNot(HaveOccurred())
305+
g.Expect(rosaNetwork.Status.Subnets[0].AvailabilityZone).To(Equal(az))
306+
g.Expect(rosaNetwork.Status.Subnets[0].PrivateSubnet).To(Equal(subnet1Id))
307+
g.Expect(rosaNetwork.Status.Subnets[0].PublicSubnet).To(Equal(subnet2Id))
308+
})
309+
}
310+
311+
func createMocks(mockCtrl *gomock.Controller) (*rosaMocks.MockEc2ApiClient, *rosaMocks.MockCloudFormationApiClient, *rosaMocks.MockStsApiClient, *ROSANetworkReconciler) {
312+
mockEC2Client := rosaMocks.NewMockEc2ApiClient(mockCtrl)
313+
mockCFClient := rosaMocks.NewMockCloudFormationApiClient(mockCtrl)
314+
mockSTSClient := rosaMocks.NewMockStsApiClient(mockCtrl)
315+
awsClient := rosaAWSClient.New(
316+
awsSdk.Config{},
317+
rosaAWSClient.NewLoggerWrapper(logrus.New(), nil),
318+
rosaMocks.NewMockIamApiClient(mockCtrl),
319+
mockEC2Client,
320+
rosaMocks.NewMockOrganizationsApiClient(mockCtrl),
321+
rosaMocks.NewMockS3ApiClient(mockCtrl),
322+
rosaMocks.NewMockSecretsManagerApiClient(mockCtrl),
323+
mockSTSClient,
324+
mockCFClient,
325+
rosaMocks.NewMockServiceQuotasApiClient(mockCtrl),
326+
rosaMocks.NewMockServiceQuotasApiClient(mockCtrl),
327+
&rosaAWSClient.AccessKey{},
328+
false,
329+
)
330+
331+
reconciler := &ROSANetworkReconciler{
332+
Client: testEnv.Client,
333+
awsClient: awsClient,
334+
}
335+
336+
return mockEC2Client, mockCFClient, mockSTSClient, reconciler
337+
}

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)