Skip to content

Commit a4d6027

Browse files
authored
Rework acceptance tests (#175)
- Require approval for running integration and acceptance tests - Add fail handler that prints pod logs. The amount of log lines is configurable with an env var. - Add env vars to control Eventually polling interval and timeout - Run tests in parallel. This requires using SynchronizedBeforeEach to set up MC only once. - Use random name for MC. The name is generated in the Makefile before deploying and running the tests. - Improve cleanup logic. The test cleanup will be performed even if deletion fails. This means that even if an introduced change breaks the deletion loop, no manual cleanup will be necessary.
1 parent cb3f87c commit a4d6027

File tree

6 files changed

+397
-89
lines changed

6 files changed

+397
-89
lines changed

.circleci/config.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,30 @@ workflows:
5252
tags:
5353
only: /^v.*/
5454

55+
- hold:
56+
type: approval
57+
requires:
58+
- unit-tests
59+
5560
- integration-tests:
61+
context: architect
62+
requires:
63+
- hold
5664
filters:
5765
tags:
5866
only: /^v.*/
5967

6068
- acceptance-tests:
69+
context: architect
70+
requires:
71+
- hold
6172
filters:
6273
tags:
6374
only: /^v.*/
6475

6576
- architect/go-build:
77+
requires:
78+
- hold
6679
context: architect
6780
name: go-build
6881
binary: aws-resolver-rules-operator

Makefile.custom.mk

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ IMAGE_REPO = $(word 1,$(subst :, ,$(IMG)))
77
IMAGE_TAG = $(word 2,$(subst :, ,$(IMG)))
88

99
CLUSTER ?= acceptance
10-
MANAGEMENT_CLUSTER_NAME ?= test-mc
11-
MANAGEMENT_CLUSTER_NAMESPACE ?= test
10+
11+
# Basically black magic. This will generate the a new name exactly once. If you
12+
# don't do the eval and subsequent variable expansion, a new name will be
13+
# generated every time you reference it.
14+
# Look up "Deferred Simple Variable Expansion" for more info.
15+
MANAGEMENT_CLUSTER_NAME ?= $(eval MANAGEMENT_CLUSTER_NAME := $$(shell tr -dc a-z0-9 </dev/urandom | head -c 16; echo))$(MANAGEMENT_CLUSTER_NAME)
16+
MANAGEMENT_CLUSTER_NAMESPACE ?= $(eval MANAGEMENT_CLUSTER_NAMESPACE := $$(shell tr -dc a-z0-9 </dev/urandom | head -c 16; echo))$(MANAGEMENT_CLUSTER_NAMESPACE)
1217

1318
DOCKER_COMPOSE = bin/docker-compose
1419

@@ -59,7 +64,7 @@ run-acceptance-tests:
5964
KUBECONFIG="$(KUBECONFIG)" \
6065
MANAGEMENT_CLUSTER_NAME="$(MANAGEMENT_CLUSTER_NAME)" \
6166
MANAGEMENT_CLUSTER_NAMESPACE="$(MANAGEMENT_CLUSTER_NAMESPACE)" \
62-
$(GINKGO) -r -randomize-all --randomize-suites tests/acceptance
67+
$(GINKGO) -p --nodes 4 -r -randomize-all --randomize-suites tests/acceptance
6368

6469
.PHONY: test-acceptance
6570
test-acceptance: KUBECONFIG=$(HOME)/.kube/$(CLUSTER).yml

tests/acceptance/acceptance_suite_test.go

Lines changed: 161 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ package acceptance_test
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"io"
8+
"os"
9+
"strconv"
610
"testing"
11+
"time"
712

13+
awssdk "github.com/aws/aws-sdk-go/aws"
814
"github.com/google/uuid"
915
. "github.com/onsi/ginkgo/v2"
1016
. "github.com/onsi/gomega"
17+
1118
corev1 "k8s.io/api/core/v1"
12-
"k8s.io/client-go/kubernetes/scheme"
19+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"k8s.io/client-go/kubernetes"
21+
"k8s.io/client-go/tools/clientcmd"
22+
"k8s.io/kubectl/pkg/scheme"
1323
capa "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
1424
capi "sigs.k8s.io/cluster-api/api/v1beta1"
1525
controllerruntime "sigs.k8s.io/controller-runtime"
@@ -19,6 +29,12 @@ import (
1929
"github.com/aws-resolver-rules-operator/tests/acceptance/fixture"
2030
)
2131

32+
const (
33+
DefaultMaxLogLines = 300
34+
DefaultPollingInterval = 5 * time.Second
35+
DefaultTimeout = 3 * time.Minute
36+
)
37+
2238
var (
2339
k8sClient client.Client
2440

@@ -28,50 +44,175 @@ var (
2844
testFixture *fixture.Fixture
2945
)
3046

47+
func LogCollectorFailHandler(message string, callerSkip ...int) {
48+
getPodLogs()
49+
Fail(message, callerSkip...)
50+
}
51+
3152
func TestAcceptance(t *testing.T) {
32-
RegisterFailHandler(Fail)
53+
RegisterFailHandler(LogCollectorFailHandler)
3354
RunSpecs(t, "Acceptance Suite")
3455
}
3556

36-
var _ = BeforeSuite(func() {
37-
tests.GetEnvOrSkip("KUBECONFIG")
57+
var _ = SynchronizedBeforeSuite(func() []byte {
58+
setupEventuallyParameters()
59+
k8sClient = setupKubeClient()
3860

39-
config, err := controllerruntime.GetConfig()
61+
fixtureConfig := getFixtureConfig()
62+
testFixture = fixture.NewFixture(k8sClient, fixtureConfig)
63+
64+
DeferCleanup(func() {
65+
Expect(testFixture.Teardown()).To(Succeed())
66+
})
67+
err := testFixture.Setup()
4068
Expect(err).NotTo(HaveOccurred())
4169

42-
err = capa.AddToScheme(scheme.Scheme)
70+
data := fixture.Data{
71+
Config: fixtureConfig,
72+
Network: testFixture.Network,
73+
}
74+
encoded, err := json.Marshal(data)
4375
Expect(err).NotTo(HaveOccurred())
4476

45-
err = capi.AddToScheme(scheme.Scheme)
77+
return encoded
78+
}, func(jsonData []byte) {
79+
data := fixture.Data{}
80+
err := json.Unmarshal(jsonData, &data)
4681
Expect(err).NotTo(HaveOccurred())
4782

48-
k8sClient, err = client.New(config, client.Options{Scheme: scheme.Scheme})
83+
setupEventuallyParameters()
84+
k8sClient = setupKubeClient()
85+
86+
testFixture = fixture.LoadFixture(k8sClient, data)
87+
})
88+
89+
var _ = BeforeEach(func() {
90+
namespace = uuid.New().String()
91+
namespaceObj = &corev1.Namespace{}
92+
namespaceObj.Name = namespace
93+
Expect(k8sClient.Create(context.Background(), namespaceObj)).To(Succeed())
94+
})
95+
96+
var _ = AfterEach(func() {
97+
Expect(k8sClient.Delete(context.Background(), namespaceObj)).To(Succeed())
98+
})
99+
100+
func getDurationFromEnvVar(env string, defaultDuration time.Duration) time.Duration {
101+
stringDuration := os.Getenv(env)
102+
if stringDuration == "" {
103+
return defaultDuration
104+
}
105+
106+
duration, err := time.ParseDuration(stringDuration)
107+
Expect(err).NotTo(HaveOccurred())
108+
109+
return duration
110+
}
111+
112+
func getMaxLogLines() int64 {
113+
maxLogLines := os.Getenv("MAX_LOG_LINES")
114+
if maxLogLines == "" {
115+
return DefaultMaxLogLines
116+
}
117+
118+
maxLogLinesInt, err := strconv.Atoi(maxLogLines)
49119
Expect(err).NotTo(HaveOccurred())
50120

121+
return int64(maxLogLinesInt)
122+
}
123+
124+
func getPodLogs() {
125+
kubeConfigPath := tests.GetEnvOrSkip("KUBECONFIG")
126+
maxLogLines := getMaxLogLines()
127+
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
128+
if err != nil {
129+
fmt.Fprintf(GinkgoWriter, "Failed to build client config: %v", err)
130+
return
131+
}
132+
133+
clientset, err := kubernetes.NewForConfig(config)
134+
if err != nil {
135+
fmt.Fprintf(GinkgoWriter, "Failed to build client: %v", err)
136+
return
137+
}
138+
139+
ctx := context.Background()
140+
podsClient := clientset.CoreV1().Pods("giantswarm")
141+
pods, err := podsClient.List(ctx, metav1.ListOptions{
142+
LabelSelector: "app.kubernetes.io/name=aws-resolver-rules-operator",
143+
})
144+
if err != nil {
145+
fmt.Fprintf(GinkgoWriter, "Failed to list pods: %v", err)
146+
return
147+
}
148+
149+
if len(pods.Items) == 0 {
150+
fmt.Fprintf(GinkgoWriter, "No pods found: %v", err)
151+
return
152+
}
153+
pod := pods.Items[0]
154+
155+
logOptions := corev1.PodLogOptions{TailLines: awssdk.Int64(maxLogLines)}
156+
req := podsClient.GetLogs(pod.Name, &logOptions)
157+
logStream, err := req.Stream(context.Background())
158+
if err != nil {
159+
fmt.Fprintf(GinkgoWriter, "Failed to get log stream: %v", err)
160+
return
161+
}
162+
defer logStream.Close()
163+
164+
logBytes, err := io.ReadAll(logStream)
165+
if err != nil {
166+
fmt.Fprintf(GinkgoWriter, "Failed to read logs: %v", err)
167+
return
168+
}
169+
170+
fmt.Fprintf(GinkgoWriter,
171+
"\n\n---------------- Last %d log lines for %q pod ----------------\n%s\n--------------------------------\n\n",
172+
maxLogLines,
173+
pod.Name,
174+
string(logBytes))
175+
}
176+
177+
func getFixtureConfig() fixture.Config {
51178
awsAccount := tests.GetEnvOrSkip("MC_AWS_ACCOUNT")
52179
iamRoleID := tests.GetEnvOrSkip("AWS_IAM_ROLE_ID")
53180
awsRegion := tests.GetEnvOrSkip("AWS_REGION")
54181
managementClusterName := tests.GetEnvOrSkip("MANAGEMENT_CLUSTER_NAME")
55182
managementClusterNamespace := tests.GetEnvOrSkip("MANAGEMENT_CLUSTER_NAMESPACE")
56183
roleARN := fmt.Sprintf("arn:aws:iam::%s:role/%s", awsAccount, iamRoleID)
57184

58-
fixtureConfig := fixture.Config{
185+
return fixture.Config{
59186
AWSAccount: awsAccount,
60187
AWSIAMRoleARN: roleARN,
61188
AWSRegion: awsRegion,
62189
ManagementClusterName: managementClusterName,
63190
ManagementClusterNamespace: managementClusterNamespace,
64191
}
65-
testFixture = fixture.NewFixture(k8sClient, fixtureConfig)
66-
})
192+
}
67193

68-
var _ = BeforeEach(func() {
69-
namespace = uuid.New().String()
70-
namespaceObj = &corev1.Namespace{}
71-
namespaceObj.Name = namespace
72-
Expect(k8sClient.Create(context.Background(), namespaceObj)).To(Succeed())
73-
})
194+
func setupEventuallyParameters() {
195+
pollingInterval := getDurationFromEnvVar("EVENTUALLY_POLLING_INTERVAL", DefaultPollingInterval)
196+
timeout := getDurationFromEnvVar("EVENTUALLY_TIMEOUT", DefaultTimeout)
74197

75-
var _ = AfterEach(func() {
76-
Expect(k8sClient.Delete(context.Background(), namespaceObj)).To(Succeed())
77-
})
198+
SetDefaultEventuallyPollingInterval(pollingInterval)
199+
SetDefaultEventuallyTimeout(timeout)
200+
}
201+
202+
func setupKubeClient() client.Client {
203+
tests.GetEnvOrSkip("KUBECONFIG")
204+
205+
config, err := controllerruntime.GetConfig()
206+
Expect(err).NotTo(HaveOccurred())
207+
208+
err = capa.AddToScheme(scheme.Scheme)
209+
Expect(err).NotTo(HaveOccurred())
210+
211+
err = capi.AddToScheme(scheme.Scheme)
212+
Expect(err).NotTo(HaveOccurred())
213+
214+
k8sClient, err = client.New(config, client.Options{Scheme: scheme.Scheme})
215+
Expect(err).NotTo(HaveOccurred())
216+
217+
return k8sClient
218+
}

tests/acceptance/fixture/aws.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fixture
22

33
import (
4+
"context"
45
"fmt"
56

67
awssdk "github.com/aws/aws-sdk-go/aws"
@@ -14,6 +15,62 @@ const (
1415
WorkloadClusterCIDR = "172.96.0.0"
1516
)
1617

18+
func DetachTransitGateway(ec2Client *ec2.EC2, gatewayID, vpcID string) error {
19+
if gatewayID == "" || vpcID == "" {
20+
return nil
21+
}
22+
23+
describeTGWattachmentInput := &ec2.DescribeTransitGatewayVpcAttachmentsInput{
24+
Filters: []*ec2.Filter{
25+
{
26+
Name: awssdk.String("transit-gateway-id"),
27+
Values: awssdk.StringSlice([]string{gatewayID}),
28+
},
29+
{
30+
Name: awssdk.String("vpc-id"),
31+
Values: awssdk.StringSlice([]string{vpcID}),
32+
},
33+
},
34+
}
35+
attachments, err := ec2Client.DescribeTransitGatewayVpcAttachments(describeTGWattachmentInput)
36+
if err != nil {
37+
return err
38+
}
39+
40+
for _, attachment := range attachments.TransitGatewayVpcAttachments {
41+
_, err = ec2Client.DeleteTransitGatewayVpcAttachmentWithContext(context.Background(), &ec2.DeleteTransitGatewayVpcAttachmentInput{
42+
TransitGatewayAttachmentId: attachment.TransitGatewayAttachmentId,
43+
})
44+
if err != nil {
45+
return err
46+
}
47+
}
48+
49+
return nil
50+
}
51+
52+
func DeleteTransitGateway(ec2Client *ec2.EC2, gatewayID string) error {
53+
if gatewayID == "" {
54+
return nil
55+
}
56+
57+
_, err := ec2Client.DeleteTransitGateway(&ec2.DeleteTransitGatewayInput{
58+
TransitGatewayId: awssdk.String(gatewayID),
59+
})
60+
return err
61+
}
62+
63+
func DeletePrefixList(ec2Client *ec2.EC2, prefixListID string) error {
64+
if prefixListID == "" {
65+
return nil
66+
}
67+
68+
_, err := ec2Client.DeleteManagedPrefixList(&ec2.DeleteManagedPrefixListInput{
69+
PrefixListId: awssdk.String(prefixListID),
70+
})
71+
return err
72+
}
73+
1774
func DisassociateRouteTable(ec2Client *ec2.EC2, associationID string) error {
1875
if associationID == "" {
1976
return nil

0 commit comments

Comments
 (0)