Skip to content

Commit

Permalink
Autorecover from duplicate project in tests
Browse files Browse the repository at this point in the history
Signed-off-by: Jose Vazquez <jose.vazquez@mongodb.com>
  • Loading branch information
josvazg committed Jul 31, 2023
1 parent 6bef370 commit 8fa3cd1
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 1 deletion.
3 changes: 2 additions & 1 deletion pkg/api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 103 additions & 0 deletions pkg/util/atlastest/project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package atlastest

import (
"context"

"go.mongodb.org/atlas/mongodbatlas"
)

type ProjectsServiceMock struct {
GetAllProjectsFn func(*mongodbatlas.ListOptions) (*mongodbatlas.Projects, *mongodbatlas.Response, error)
DeleteFn func(string) (*mongodbatlas.Response, error)
}

// AddTeamsToProject implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) AddTeamsToProject(context.Context, string, []*mongodbatlas.ProjectTeam) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// Create implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) Create(context.Context, *mongodbatlas.Project, *mongodbatlas.CreateProjectOptions) (*mongodbatlas.Project, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// Delete implements mongodbatlas.ProjectsService.
func (ps *ProjectsServiceMock) Delete(_ context.Context, id string) (*mongodbatlas.Response, error) {
if ps.DeleteFn == nil {
panic("Delete was not set for test")
}
return ps.DeleteFn(id)
}

// DeleteInvitation implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) DeleteInvitation(context.Context, string, string) (*mongodbatlas.Response, error) {
panic("unimplemented")
}

// GetAllProjects implements mongodbatlas.ProjectsService.
func (ps *ProjectsServiceMock) GetAllProjects(_ context.Context, listOptions *mongodbatlas.ListOptions) (*mongodbatlas.Projects, *mongodbatlas.Response, error) {
if ps.GetAllProjectsFn == nil {
panic("GetAllProjects was not set for test")
}
return ps.GetAllProjectsFn(listOptions)
}

// GetOneProject implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) GetOneProject(context.Context, string) (*mongodbatlas.Project, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// GetOneProjectByName implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) GetOneProjectByName(context.Context, string) (*mongodbatlas.Project, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// GetProjectSettings implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) GetProjectSettings(context.Context, string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// GetProjectTeamsAssigned implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) GetProjectTeamsAssigned(context.Context, string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// Invitation implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) Invitation(context.Context, string, string) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// Invitations implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) Invitations(context.Context, string, *mongodbatlas.InvitationOptions) ([]*mongodbatlas.Invitation, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// InviteUser implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) InviteUser(context.Context, string, *mongodbatlas.Invitation) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// RemoveUserFromProject implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) RemoveUserFromProject(context.Context, string, string) (*mongodbatlas.Response, error) {
panic("unimplemented")
}

// Update implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) Update(context.Context, string, *mongodbatlas.ProjectUpdateRequest) (*mongodbatlas.Project, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// UpdateInvitation implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) UpdateInvitation(context.Context, string, *mongodbatlas.Invitation) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// UpdateInvitationByID implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) UpdateInvitationByID(context.Context, string, string, *mongodbatlas.Invitation) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) {
panic("unimplemented")
}

// UpdateProjectSettings implements mongodbatlas.ProjectsService.
func (*ProjectsServiceMock) UpdateProjectSettings(context.Context, string, *mongodbatlas.ProjectSettings) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) {
panic("unimplemented")
}
57 changes: 57 additions & 0 deletions pkg/util/fixtest/remove_duplicates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package fixtest

import (
"context"
"sort"

"go.mongodb.org/atlas/mongodbatlas"
"go.uber.org/zap"
)

// EnsureNoDuplicates removes projects with same name but different ID.
// Atlas sometimes creates duplicate projects, we need our tests to defend
// against that to avoid flaky tests
func EnsureNoDuplicates(client *mongodbatlas.Client, logger *zap.SugaredLogger, projectName string) error {
found, err := listProjectsByName(client, projectName)
if err != nil || len(found) <= 1 {
return err
}
logger.Warnf("Found more than one project with name %q", projectName)
keep, rest := selectProject(found)
logger.Warnf("Will keep project ID %s as %s and remove the rest", keep.ID, projectName)
return removeProjects(client, rest)
}

func listProjectsByName(client *mongodbatlas.Client, projectName string) ([]*mongodbatlas.Project, error) {
projects, _, err := client.Projects.GetAllProjects(
context.Background(),
&mongodbatlas.ListOptions{},
)
if err != nil {
return nil, err
}
found := []*mongodbatlas.Project{}
for _, project := range projects.Results {
if project.Name == projectName {
found = append(found, project)
}
}
return found, nil
}

func selectProject(projects []*mongodbatlas.Project) (*mongodbatlas.Project, []*mongodbatlas.Project) {
sort.Slice(projects, func(i, j int) bool {
return projects[i].ID < projects[j].ID
})
return projects[0], projects[1:]
}

func removeProjects(client *mongodbatlas.Client, projects []*mongodbatlas.Project) error {
for _, project := range projects {
_, err := client.Projects.Delete(context.Background(), project.ID)
if err != nil {
return err
}
}
return nil
}
109 changes: 109 additions & 0 deletions pkg/util/fixtest/remove_duplicates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package fixtest_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/atlas/mongodbatlas"
"go.uber.org/zap"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/atlastest"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/fixtest"
)

const (
fakeDomain = "fake-atlas.local"
fakeProject = "fake-project"
)

func TestEnsureNoDuplicates(t *testing.T) {
testCases := []struct {
title string
duplicates int
expectedRemovedIds []string
}{
{
title: "Triplet projects on same name remove the highest ids",
duplicates: 3,
expectedRemovedIds: []string{"2", "3"},
},
{
title: "one projects is respected",
duplicates: 1,
expectedRemovedIds: []string{},
},
{
title: "zero projects are ignored",
duplicates: 0,
expectedRemovedIds: []string{},
},
}
logger, err := zap.NewDevelopment()
require.NoError(t, err)
for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
numberOfProjects := tc.duplicates
projectTriplets := genProjects(fakeProject, numberOfProjects)
removed := []string{}
client := &mongodbatlas.Client{
Projects: &atlastest.ProjectsServiceMock{
GetAllProjectsFn: func(listOptions *mongodbatlas.ListOptions) (*mongodbatlas.Projects, *mongodbatlas.Response, error) {
return &mongodbatlas.Projects{
Results: projectTriplets,
TotalCount: numberOfProjects,
}, &mongodbatlas.Response{}, nil
},
DeleteFn: func(id string) (*mongodbatlas.Response, error) {
removed = append(removed, id)
return &mongodbatlas.Response{}, nil
},
},
}

err := fixtest.EnsureNoDuplicates(client, logger.Sugar(), fakeProject)
require.NoError(t, err)
assert.Equal(t, tc.expectedRemovedIds, removed)
})
}
}

func genProjects(projectName string, max int) []*mongodbatlas.Project {
projects := []*mongodbatlas.Project{}
// generate ids in reverese order N -> 1 to test re-ordering
for i := max; i > 0; i-- {
prj := &mongodbatlas.Project{
ID: fmt.Sprintf("%d", i),
Name: projectName,
}
projects = append(projects, prj)
}
return projects
}

func TestEnsureNoDuplicatesIgnoresOne(t *testing.T) {
numberOfProjects := 1
projectTriplets := genProjects(fakeProject, numberOfProjects)
removed := []string{}
client := &mongodbatlas.Client{
Projects: &atlastest.ProjectsServiceMock{
GetAllProjectsFn: func(listOptions *mongodbatlas.ListOptions) (*mongodbatlas.Projects, *mongodbatlas.Response, error) {
return &mongodbatlas.Projects{
Results: projectTriplets,
TotalCount: numberOfProjects,
}, &mongodbatlas.Response{}, nil
},
DeleteFn: func(id string) (*mongodbatlas.Response, error) {
removed = append(removed, id)
return &mongodbatlas.Response{}, nil
},
},
}
logger, err := zap.NewDevelopment()
require.NoError(t, err)

err = fixtest.EnsureNoDuplicates(client, logger.Sugar(), fakeProject)
require.NoError(t, err)
assert.Equal(t, []string{}, removed)
}
20 changes: 20 additions & 0 deletions test/e2e/actions/deploy/deploy_operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/fixtest"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/kube"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/api/atlas"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/config"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/k8s"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model"
Expand Down Expand Up @@ -45,14 +49,30 @@ func MultiNamespaceOperator(data *model.TestDataProvider, watchNamespace []strin
})
}

func ginkgoZapLogger() *zap.SugaredLogger {
zcore := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.Lock(zapcore.AddSync(GinkgoWriter)),
zap.NewAtomicLevel(),
)
return zap.New(zcore).Sugar()
}

func CreateProject(testData *model.TestDataProvider) {
if testData.Project.GetNamespace() == "" {
testData.Project.Namespace = testData.Resources.Namespace
}
By(fmt.Sprintf("Deploy Project %s", testData.Project.GetName()), func() {
aClient := atlas.GetClientOrFail()
err := testData.K8SClient.Create(testData.Context, testData.Project)
Expect(err).ShouldNot(HaveOccurred(), "Project %s was not created", testData.Project.GetName())
Eventually(func(g Gomega) {
// We reported Atlas creating duplicates of a project with the same name
// See https://jira.mongodb.org/browse/CLOUDP-187749
// this fix in our tests allows them to automatically fix this issue
// and thus avoid a flaky failure when this duplicates happens
g.Expect(fixtest.EnsureNoDuplicates(aClient.Client, ginkgoZapLogger(), testData.Project.GetName())).ToNot(HaveOccurred())

condition, _ := k8s.GetProjectStatusCondition(
testData.Context,
testData.K8SClient,
Expand Down

0 comments on commit 8fa3cd1

Please sign in to comment.