Skip to content

Commit 7dddd37

Browse files
authored
Add janitor to clean up Cloud Map integration test resources (#57)
1 parent 98d343f commit 7dddd37

File tree

12 files changed

+372
-41
lines changed

12 files changed

+372
-41
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ generate-mocks: mockgen
9797
$(MOCKGEN) --source pkg/cloudmap/operation_collector.go --destination $(MOCKS_DESTINATION)/pkg/cloudmap/operation_collector_mock.go --package cloudmap
9898
$(MOCKGEN) --source pkg/cloudmap/api.go --destination $(MOCKS_DESTINATION)/pkg/cloudmap/api_mock.go --package cloudmap
9999
$(MOCKGEN) --source pkg/cloudmap/aws_facade.go --destination $(MOCKS_DESTINATION)/pkg/cloudmap/aws_facade_mock.go --package cloudmap
100+
$(MOCKGEN) --source integration/janitor/api.go --destination $(MOCKS_DESTINATION)/integration/janitor/api_mock.go --package janitor
101+
$(MOCKGEN) --source integration/janitor/aws_facade.go --destination $(MOCKS_DESTINATION)/integration/janitor/aws_facade_mock.go --package janitor
100102

101103

102104
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen

integration/janitor/api.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package janitor
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/cloudmap"
6+
"github.com/aws/aws-sdk-go-v2/aws"
7+
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
8+
)
9+
10+
type ServiceDiscoveryJanitorApi interface {
11+
DeleteNamespace(ctx context.Context, namespaceId string) (operationId string, err error)
12+
DeleteService(ctx context.Context, serviceId string) error
13+
cloudmap.ServiceDiscoveryApi
14+
}
15+
16+
type serviceDiscoveryJanitorApi struct {
17+
cloudmap.ServiceDiscoveryApi
18+
janitorFacade SdkJanitorFacade
19+
}
20+
21+
func NewServiceDiscoveryJanitorApiFromConfig(cfg *aws.Config) ServiceDiscoveryJanitorApi {
22+
return &serviceDiscoveryJanitorApi{
23+
ServiceDiscoveryApi: cloudmap.NewServiceDiscoveryApiFromConfig(cfg),
24+
janitorFacade: NewSdkJanitorFacadeFromConfig(cfg),
25+
}
26+
}
27+
28+
func (api *serviceDiscoveryJanitorApi) DeleteNamespace(ctx context.Context, nsId string) (opId string, err error) {
29+
out, err := api.janitorFacade.DeleteNamespace(ctx, &sd.DeleteNamespaceInput{Id: &nsId})
30+
if err != nil {
31+
return "", err
32+
}
33+
34+
return aws.ToString(out.OperationId), nil
35+
}
36+
37+
func (api *serviceDiscoveryJanitorApi) DeleteService(ctx context.Context, svcId string) error {
38+
_, err := api.janitorFacade.DeleteService(ctx, &sd.DeleteServiceInput{Id: &svcId})
39+
return err
40+
}

integration/janitor/api_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package janitor
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/mocks/integration/janitor"
6+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/test"
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
9+
"github.com/golang/mock/gomock"
10+
"github.com/stretchr/testify/assert"
11+
"testing"
12+
)
13+
14+
func TestNewServiceDiscoveryJanitorApiFromConfig(t *testing.T) {
15+
assert.NotNil(t, NewServiceDiscoveryJanitorApiFromConfig(&aws.Config{}))
16+
}
17+
18+
func TestServiceDiscoveryJanitorApi_DeleteNamespace_HappyCase(t *testing.T) {
19+
mockController := gomock.NewController(t)
20+
defer mockController.Finish()
21+
22+
mocksdk := janitor.NewMockSdkJanitorFacade(mockController)
23+
jApi := getJanitorApi(t, mocksdk)
24+
25+
mocksdk.EXPECT().DeleteNamespace(context.TODO(), &sd.DeleteNamespaceInput{Id: aws.String(test.NsId)}).
26+
Return(&sd.DeleteNamespaceOutput{OperationId: aws.String(test.OpId1)}, nil)
27+
28+
opId, err := jApi.DeleteNamespace(context.TODO(), test.NsId)
29+
assert.Nil(t, err, "No error for happy case")
30+
assert.Equal(t, test.OpId1, opId)
31+
}
32+
33+
func TestServiceDiscoveryJanitorApi_DeleteService_HappyCase(t *testing.T) {
34+
mockController := gomock.NewController(t)
35+
defer mockController.Finish()
36+
37+
mocksdk := janitor.NewMockSdkJanitorFacade(mockController)
38+
jApi := getJanitorApi(t, mocksdk)
39+
40+
mocksdk.EXPECT().DeleteService(context.TODO(), &sd.DeleteServiceInput{Id: aws.String(test.SvcId)}).
41+
Return(&sd.DeleteServiceOutput{}, nil)
42+
43+
err := jApi.DeleteService(context.TODO(), test.SvcId)
44+
assert.Nil(t, err, "No error for happy case")
45+
}
46+
47+
func getJanitorApi(t *testing.T, sdk *janitor.MockSdkJanitorFacade) ServiceDiscoveryJanitorApi {
48+
return &serviceDiscoveryJanitorApi{
49+
janitorFacade: sdk,
50+
}
51+
}

integration/janitor/aws_facade.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package janitor
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/cloudmap"
6+
"github.com/aws/aws-sdk-go-v2/aws"
7+
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
8+
)
9+
10+
// SdkJanitorFacade extends the minimal surface area of ServiceDiscovery API calls of the client
11+
// for integration test janitor operations.
12+
type SdkJanitorFacade interface {
13+
// DeleteNamespace provides ServiceDiscovery DeleteNamespace wrapper interface.
14+
DeleteNamespace(context.Context, *sd.DeleteNamespaceInput, ...func(*sd.Options)) (*sd.DeleteNamespaceOutput, error)
15+
16+
// DeleteService provides ServiceDiscovery DeleteService wrapper interface.
17+
DeleteService(context.Context, *sd.DeleteServiceInput, ...func(*sd.Options)) (*sd.DeleteServiceOutput, error)
18+
19+
cloudmap.AwsFacade
20+
}
21+
22+
type sdkJanitorFacade struct {
23+
*sd.Client
24+
}
25+
26+
// NewSdkJanitorFacadeFromConfig creates a new AWS facade from an AWS client config
27+
// extended for integration test janitor operations.
28+
func NewSdkJanitorFacadeFromConfig(cfg *aws.Config) SdkJanitorFacade {
29+
return &sdkJanitorFacade{sd.NewFromConfig(*cfg)}
30+
}

integration/janitor/janitor.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package janitor
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/cloudmap"
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
"github.com/aws/aws-sdk-go-v2/config"
9+
"os"
10+
)
11+
12+
// CloudMapJanitor handles AWS Cloud Map resource cleanup during integration tests.
13+
type CloudMapJanitor interface {
14+
// Cleanup removes all instances, services and the namespace from AWS Cloud Map for a given namespace name.
15+
Cleanup(ctx context.Context, nsName string)
16+
}
17+
18+
type cloudMapJanitor struct {
19+
sdApi ServiceDiscoveryJanitorApi
20+
fail func()
21+
}
22+
23+
// NewDefaultJanitor returns a new janitor object.
24+
func NewDefaultJanitor() CloudMapJanitor {
25+
awsCfg, err := config.LoadDefaultConfig(context.TODO(),
26+
config.WithRegion(os.Getenv("AWS_REGION")),
27+
)
28+
29+
if err != nil {
30+
fmt.Printf("unable to configure AWS session: %s", err.Error())
31+
os.Exit(1)
32+
}
33+
34+
return &cloudMapJanitor{
35+
sdApi: NewServiceDiscoveryJanitorApiFromConfig(&awsCfg),
36+
fail: func() { os.Exit(1) },
37+
}
38+
}
39+
40+
func (j *cloudMapJanitor) Cleanup(ctx context.Context, nsName string) {
41+
fmt.Printf("Cleaning up all test resources in Cloud Map for namespace : %s\n", nsName)
42+
43+
nsList, err := j.sdApi.ListNamespaces(ctx)
44+
j.checkOrFail(err, "", "could not find namespace to clean")
45+
46+
var nsId string
47+
for _, ns := range nsList {
48+
if ns.Name == nsName {
49+
nsId = ns.Id
50+
}
51+
}
52+
53+
if nsId == "" {
54+
fmt.Println("namespace does not exist in account, nothing to clean")
55+
return
56+
}
57+
58+
fmt.Printf("found namespace to clean: %s\n", nsId)
59+
60+
svcs, err := j.sdApi.ListServices(ctx, nsId)
61+
j.checkOrFail(err,
62+
fmt.Sprintf("namespace has %d services to clean", len(svcs)),
63+
"could not find services to clean")
64+
65+
for _, svc := range svcs {
66+
fmt.Printf("found service to clean: %s\n", svc.Id)
67+
j.deregisterInstances(ctx, svc.Id)
68+
69+
delSvcErr := j.sdApi.DeleteService(ctx, svc.Id)
70+
j.checkOrFail(delSvcErr, "service deleted", "could not cleanup service")
71+
}
72+
73+
opId, err := j.sdApi.DeleteNamespace(ctx, nsId)
74+
if err == nil {
75+
fmt.Println("namespace delete in progress")
76+
_, err = j.sdApi.PollNamespaceOperation(ctx, opId)
77+
}
78+
j.checkOrFail(err, "clean up successful", "could not cleanup namespace")
79+
}
80+
81+
func (j *cloudMapJanitor) deregisterInstances(ctx context.Context, svcId string) {
82+
insts, err := j.sdApi.ListInstances(ctx, svcId)
83+
j.checkOrFail(err,
84+
fmt.Sprintf("service has %d instances to clean", len(insts)),
85+
"could not list instances to cleanup")
86+
87+
opColl := cloudmap.NewOperationCollector()
88+
for _, inst := range insts {
89+
instId := aws.ToString(inst.Id)
90+
fmt.Printf("found instance to clean: %s\n", instId)
91+
opColl.Add(func() (opId string, err error) {
92+
return j.sdApi.DeregisterInstance(ctx, svcId, instId)
93+
})
94+
}
95+
96+
opErr := cloudmap.NewDeregisterInstancePoller(j.sdApi, svcId, opColl.Collect(), opColl.GetStartTime()).Poll(ctx)
97+
j.checkOrFail(opErr, "instances de-registered", "could not cleanup instances")
98+
}
99+
100+
func (j *cloudMapJanitor) checkOrFail(err error, successMsg string, failMsg string) {
101+
if err != nil {
102+
fmt.Printf("%s: %s\n", failMsg, err.Error())
103+
j.fail()
104+
}
105+
106+
if successMsg != "" {
107+
fmt.Println(successMsg)
108+
}
109+
}

integration/janitor/janitor_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package janitor
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/mocks/integration/janitor"
6+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/model"
7+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/test"
8+
"github.com/aws/aws-sdk-go-v2/aws"
9+
"github.com/aws/aws-sdk-go-v2/service/servicediscovery/types"
10+
"github.com/golang/mock/gomock"
11+
"github.com/stretchr/testify/assert"
12+
"testing"
13+
)
14+
15+
type testJanitor struct {
16+
janitor *cloudMapJanitor
17+
mockApi *janitor.MockServiceDiscoveryJanitorApi
18+
failed *bool
19+
close func()
20+
}
21+
22+
func TestNewDefaultJanitor(t *testing.T) {
23+
assert.NotNil(t, NewDefaultJanitor())
24+
}
25+
26+
func TestCleanupHappyCase(t *testing.T) {
27+
tj := getTestJanitor(t)
28+
defer tj.close()
29+
30+
tj.mockApi.EXPECT().ListNamespaces(context.TODO()).
31+
Return([]*model.Namespace{{Id: test.NsId, Name: test.NsName}}, nil)
32+
tj.mockApi.EXPECT().ListServices(context.TODO(), test.NsId).
33+
Return([]*model.Resource{{Id: test.SvcId, Name: test.SvcName}}, nil)
34+
tj.mockApi.EXPECT().ListInstances(context.TODO(), test.SvcId).
35+
Return([]types.InstanceSummary{{Id: aws.String(test.EndptId1)}}, nil)
36+
37+
tj.mockApi.EXPECT().DeregisterInstance(context.TODO(), test.SvcId, test.EndptId1).
38+
Return(test.OpId1, nil)
39+
tj.mockApi.EXPECT().ListOperations(context.TODO(), gomock.Any()).
40+
Return(map[string]types.OperationStatus{test.OpId1: types.OperationStatusSuccess}, nil)
41+
tj.mockApi.EXPECT().DeleteService(context.TODO(), test.SvcId).
42+
Return(nil)
43+
tj.mockApi.EXPECT().DeleteNamespace(context.TODO(), test.NsId).
44+
Return(test.OpId2, nil)
45+
tj.mockApi.EXPECT().PollNamespaceOperation(context.TODO(), test.OpId2).
46+
Return(test.NsId, nil)
47+
48+
tj.janitor.Cleanup(context.TODO(), test.NsName)
49+
assert.False(t, *tj.failed)
50+
}
51+
52+
func TestCleanupNothingToClean(t *testing.T) {
53+
tj := getTestJanitor(t)
54+
defer tj.close()
55+
56+
tj.mockApi.EXPECT().ListNamespaces(context.TODO()).
57+
Return([]*model.Namespace{}, nil)
58+
59+
tj.janitor.Cleanup(context.TODO(), test.NsName)
60+
assert.False(t, *tj.failed)
61+
}
62+
63+
func getTestJanitor(t *testing.T) *testJanitor {
64+
mockController := gomock.NewController(t)
65+
api := janitor.NewMockServiceDiscoveryJanitorApi(mockController)
66+
failed := false
67+
return &testJanitor{
68+
janitor: &cloudMapJanitor{
69+
sdApi: api,
70+
fail: func() { failed = true },
71+
},
72+
mockApi: api,
73+
failed: &failed,
74+
close: func() { mockController.Finish() },
75+
}
76+
}

integration/janitor/runner/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/integration/janitor"
6+
)
7+
8+
const (
9+
e2eNs = "aws-cloud-map-mcs-e2e"
10+
)
11+
12+
func main() {
13+
j := janitor.NewDefaultJanitor()
14+
j.Cleanup(context.TODO(), e2eNs)
15+
}

0 commit comments

Comments
 (0)