diff --git a/pkg/api/condition.go b/pkg/api/condition.go index 74c469cf76..11451937f7 100644 --- a/pkg/api/condition.go +++ b/pkg/api/condition.go @@ -89,6 +89,11 @@ const ( SearchIndexesNotReady = "SearchIndexesNotReady" ) +// Atlas Teams condition types +const ( + TeamUnmanaged ConditionType = "TeamUnmanaged" +) + // Generic condition type const ( ResourceVersionStatus ConditionType = "ResourceVersionIsValid" diff --git a/pkg/controller/atlasproject/project_test.go b/pkg/controller/atlasproject/project_test.go index a1e422a6f6..62ba11bc4a 100644 --- a/pkg/controller/atlasproject/project_test.go +++ b/pkg/controller/atlasproject/project_test.go @@ -935,7 +935,7 @@ func TestDelete(t *testing.T) { &akov2.AtlasTeam{ObjectMeta: metav1.ObjectMeta{Name: teamName}}, }, }, - "should delete team from Atlas when AtlasProject with finalizer is deleted": { + "should update team status when project is deleted": { atlasClientMocker: func() *mongodbatlas.Client { projectsMock := &atlasmocks.ProjectsClientMock{ GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { @@ -956,10 +956,6 @@ func TestDelete(t *testing.T) { }, } teamsMock := &atlasmocks.TeamsClientMock{ - RemoveTeamFromOrganizationFunc: func(orgID string, teamID string) (*mongodbatlas.Response, error) { - return nil, nil - }, - RemoveTeamFromOrganizationRequests: map[string]struct{}{}, ListFunc: func(orgID string) ([]mongodbatlas.Team, *mongodbatlas.Response, error) { return []mongodbatlas.Team{ { @@ -1073,6 +1069,7 @@ func TestDelete(t *testing.T) { k8sClient := fake.NewClientBuilder(). WithScheme(testScheme). WithObjects(tt.objects...). + WithStatusSubresource(tt.objects...). WithIndex( instancesIndexer.Object(), instancesIndexer.Name(), @@ -1091,6 +1088,7 @@ func TestDelete(t *testing.T) { }, }, projectService: tt.projectServiceMocker(), + EventRecorder: record.NewFakeRecorder(1), } ctx := &workflow.Context{ Context: context.Background(), diff --git a/pkg/controller/atlasproject/teams.go b/pkg/controller/atlasproject/teams.go index 8fcb44c209..6dd746cccc 100644 --- a/pkg/controller/atlasproject/teams.go +++ b/pkg/controller/atlasproject/teams.go @@ -188,26 +188,19 @@ func (r *AtlasProjectReconciler) updateTeamState(ctx *workflow.Context, project conditions := akov2.InitCondition(team, api.FalseCondition(api.ReadyType)) teamCtx := workflow.NewContext(log, conditions, ctx.Context) - atlasClient, orgID, err := r.AtlasProvider.Client(teamCtx.Context, project.ConnectionSecretObjectKey(), log) - if err != nil { - return err - } - teamCtx.Client = atlasClient - if len(assignedProjects) == 0 { - if customresource.IsResourcePolicyKeepOrDefault(project, r.ObjectDeletionProtection) { - log.Debug("team %s has no project associated, "+ - "skipping deletion from Atlas due to ObjectDeletionProtection being set", team.Spec.Name) - } else { - log.Debugf("team %s has no project associated to it. removing from atlas.", team.Spec.Name) - _, err = teamCtx.Client.Teams.RemoveTeamFromOrganization(ctx.Context, orgID, team.Status.ID) - if err != nil { - return err - } + if err = customresource.ManageFinalizer(ctx.Context, r.Client, team, customresource.UnsetFinalizer); err != nil { + return err + } + + teamCtx.SetConditionTrueMsg(api.TeamUnmanaged, "This resource is only reconciled when associated to a project") + } else { + if err = customresource.ManageFinalizer(ctx.Context, r.Client, team, customresource.SetFinalizer); err != nil { + return err } - teamCtx.EnsureStatusOption(status.AtlasTeamUnsetID()) } + teamCtx.SetConditionTrue(api.ReadyType) teamCtx.EnsureStatusOption(status.AtlasTeamSetProjects(assignedProjects)) statushandler.Update(teamCtx, r.Client, r.EventRecorder, team) diff --git a/pkg/controller/atlasproject/teams_test.go b/pkg/controller/atlasproject/teams_test.go index 4305d67e88..29e6a6dc0a 100644 --- a/pkg/controller/atlasproject/teams_test.go +++ b/pkg/controller/atlasproject/teams_test.go @@ -6,251 +6,362 @@ import ( "github.com/stretchr/testify/assert" "go.mongodb.org/atlas/mongodbatlas" - "go.uber.org/zap" "go.uber.org/zap/zaptest" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" + atlasmocks "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/customresource" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" ) -func TestUpdateTeamState(t *testing.T) { - t.Run("should not duplicate projects listed", func(t *testing.T) { - logger := zaptest.NewLogger(t).Sugar() - workflowCtx := defaultTestWorkflow(logger) - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-secret", - }, - Data: map[string][]byte{ - "orgId": []byte("0987654321"), - "publicApiKey": []byte("api-pub-key"), - "privateApiKey": []byte("api-priv-key"), - }, - Type: "Opaque", - } - project := &akov2.AtlasProject{ - Spec: akov2.AtlasProjectSpec{ - Name: "projectName", - ConnectionSecret: &common.ResourceRefNamespaced{ - Name: "my-secret", +func TestSyncAssignedTeams(t *testing.T) { + tests := map[string]struct { + teamsToAssign map[string]*akov2.Team + expectedErr error + }{ + "should sync teams assigned": { + teamsToAssign: map[string]*akov2.Team{ + "teamID_1": { + TeamRef: common.ResourceRefNamespaced{ + Name: "teamName_1", + }, + Roles: []akov2.TeamRole{"GROUP_OWNER"}, }, - }, - Status: status.AtlasProjectStatus{ - ID: "projectID", - }, - } - team := &akov2.AtlasTeam{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testTeam", - Namespace: "testNS", - }, - Status: status.TeamStatus{ - ID: "testTeamStatus", - Projects: []status.TeamProject{ - { - ID: project.Status.ID, - Name: project.Spec.Name, + "teamID_2": { + TeamRef: common.ResourceRefNamespaced{ + Name: "teamName_2", }, + Roles: []akov2.TeamRole{"GROUP_READ_ONLY"}, }, }, - } - atlasProvider := &atlas.TestProvider{ - ClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*mongodbatlas.Client, string, error) { - return &mongodbatlas.Client{}, "0987654321", nil - }, - } - k8sClient := buildFakeKubernetesClient(secret, project, team) - reconciler := &AtlasProjectReconciler{ - Client: k8sClient, - Log: logger, - AtlasProvider: atlasProvider, - } - // check we have exactly 1 project in status - assert.Equal(t, 1, len(team.Status.Projects)) + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + project := &akov2.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "projectName", + }, + Spec: akov2.AtlasProjectSpec{ + Name: "projectName", + Teams: []akov2.Team{ + { + TeamRef: common.ResourceRefNamespaced{Name: "teamName_1"}, + Roles: []akov2.TeamRole{"GROUP_OWNER"}, + }, + { + TeamRef: common.ResourceRefNamespaced{Name: "teamName_2"}, + Roles: []akov2.TeamRole{"GROUP_READ_ONLY"}, + }, + }, + }, + Status: status.AtlasProjectStatus{ + ID: "projectID", + Teams: []status.ProjectTeamStatus{ + { + ID: "teamID_1", + TeamRef: common.ResourceRefNamespaced{ + Name: "teamName_1", + }, + }, + { + ID: "teamID_2", + TeamRef: common.ResourceRefNamespaced{ + Name: "teamName_2", + }, + }, + { + ID: "teamID_3", + TeamRef: common.ResourceRefNamespaced{ + Name: "teamName_3", + }, + }, + }, + }, + } + team1 := &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "teamName_1", + }, + Spec: akov2.TeamSpec{ + Name: "teamName_1", + }, + Status: status.TeamStatus{ + ID: "teamID_1", + Projects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", + }, + }, + }, + } + team2 := &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "teamName_2", + }, + Spec: akov2.TeamSpec{ + Name: "teamName_2", + }, + Status: status.TeamStatus{ + ID: "teamID_2", + Projects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", + }, + }, + }, + } + team3 := &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "teamName_3", + }, + Spec: akov2.TeamSpec{ + Name: "teamName_3", + }, + Status: status.TeamStatus{ + ID: "teamID_3", + Projects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", + }, + }, + }, + } - // "reconcile" the team state and check we still have 1 project in status - err := reconciler.updateTeamState(workflowCtx, project, reference(team), false) - assert.NoError(t, err) - k8sClient.Get(context.Background(), types.NamespacedName{Name: team.ObjectMeta.Name, Namespace: team.ObjectMeta.Namespace}, team) - assert.Equal(t, 1, len(team.Status.Projects)) - }) + testScheme := runtime.NewScheme() + assert.NoError(t, akov2.AddToScheme(testScheme)) + assert.NoError(t, corev1.AddToScheme(testScheme)) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(project, team1, team2, team3). + WithStatusSubresource(project, team1, team2, team3). + Build() - t.Run("must remove a team from Atlas is a team is unassigned", func(t *testing.T) { - logger := zaptest.NewLogger(t).Sugar() - workflowCtx := defaultTestWorkflow(logger) - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-secret", - }, - Data: map[string][]byte{ - "orgId": []byte("0987654321"), - "publicApiKey": []byte("api-pub-key"), - "privateApiKey": []byte("api-priv-key"), + atlasClient := &mongodbatlas.Client{ + Projects: &atlasmocks.ProjectsClientMock{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{ + Links: nil, + Results: []*mongodbatlas.Result{ + { + Links: nil, + RoleNames: []string{"GROUP_OWNER"}, + TeamID: "teamID_1", + }, + { + Links: nil, + RoleNames: []string{"GROUP_OWNER"}, + TeamID: "teamID_2", + }, + { + Links: nil, + RoleNames: []string{"GROUP_READ_ONLY"}, + TeamID: "teamID_3", + }, + }, + TotalCount: 0, + }, nil, nil + }, + AddTeamsToProjectFunc: func(projectId string, teams []*mongodbatlas.ProjectTeam) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{}, nil, nil + }, + }, + Teams: &atlasmocks.TeamsClientMock{ + ListFunc: func(orgID string) ([]mongodbatlas.Team, *mongodbatlas.Response, error) { + return []mongodbatlas.Team{ + { + ID: "teamID_1", + Name: "teamName_1", + Usernames: nil, + }, + { + ID: "teamID_2", + Name: "teamName_2", + Usernames: nil, + }, + { + ID: "teamID_3", + Name: "teamName_3", + Usernames: nil, + }, + }, nil, nil + }, + RemoveTeamFromProjectFunc: func(projectID string, teamID string) (*mongodbatlas.Response, error) { + return nil, nil + }, + }, + } + + logger := zaptest.NewLogger(t).Sugar() + ctx := &workflow.Context{ + Log: logger, + Client: atlasClient, + } + r := &AtlasProjectReconciler{ + Client: k8sClient, + EventRecorder: record.NewFakeRecorder(10), + Log: logger, + } + + err := r.syncAssignedTeams(ctx, "projectID", project, tt.teamsToAssign) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestUpdateTeamState(t *testing.T) { + tests := map[string]struct { + team *akov2.AtlasTeam + isRemoval bool + expectedAssignedProjects []status.TeamProject + }{ + "should add project to team status": { + team: &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testTeam", + Namespace: "testNS", + }, + Status: status.TeamStatus{ + ID: "testTeamStatus", + }, }, - Type: "Opaque", - } - project := &akov2.AtlasProject{ - Spec: akov2.AtlasProjectSpec{ - Name: "projectName", - ConnectionSecret: &common.ResourceRefNamespaced{ - Name: "my-secret", + isRemoval: false, + expectedAssignedProjects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", }, }, - Status: status.AtlasProjectStatus{ - ID: "projectID", + }, + "should not duplicate projects already listed on status": { + team: &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testTeam", + Namespace: "testNS", + }, + Status: status.TeamStatus{ + ID: "testTeamStatus", + Projects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", + }, + }, + }, }, - } - team := &akov2.AtlasTeam{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testTeam", - Namespace: "testNS", + isRemoval: false, + expectedAssignedProjects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", + }, }, - Status: status.TeamStatus{ - ID: "testTeamStatus", - Projects: []status.TeamProject{}, + }, + "should remove project from team status": { + team: &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testTeam", + Namespace: "testNS", + }, + Status: status.TeamStatus{ + ID: "testTeamStatus", + }, }, - } - teamsMock := &atlas.TeamsClientMock{ - RemoveTeamFromOrganizationFunc: func(orgID string, teamID string) (*mongodbatlas.Response, error) { - return nil, nil + isRemoval: true, + expectedAssignedProjects: nil, + }, + "should not modify status of other assigned projects": { + team: &akov2.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testTeam", + Namespace: "testNS", + }, + Status: status.TeamStatus{ + ID: "testTeamStatus", + Projects: []status.TeamProject{ + { + ID: "existingProjectID", + Name: "existingProjectName", + }, + }, + }, }, - RemoveTeamFromOrganizationRequests: map[string]struct{}{}, - } - atlasProvider := &atlas.TestProvider{ - ClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*mongodbatlas.Client, string, error) { - return &mongodbatlas.Client{ - Teams: teamsMock, - }, "0987654321", nil + isRemoval: false, + expectedAssignedProjects: []status.TeamProject{ + { + ID: "projectID", + Name: "projectName", + }, + { + ID: "existingProjectID", + Name: "existingProjectName", + }, }, - } - k8sClient := buildFakeKubernetesClient(secret, project, team) - reconciler := &AtlasProjectReconciler{ - Client: k8sClient, - Log: logger, - AtlasProvider: atlasProvider, - } - - err := reconciler.updateTeamState(workflowCtx, project, reference(team), true) - assert.NoError(t, err) - k8sClient.Get(context.Background(), types.NamespacedName{Name: team.ObjectMeta.Name, Namespace: team.ObjectMeta.Namespace}, team) - assert.Len(t, teamsMock.RemoveTeamFromOrganizationRequests, 1) - }) + }, + } - t.Run("must honor deletion protection flag for Teams", func(t *testing.T) { - for _, tc := range []struct { - title string - deletionProtection bool - keepFlag bool - expectRemoval bool - }{ - { - title: "with deletion protection unassigned teams are not removed", - deletionProtection: true, - keepFlag: false, - expectRemoval: false, - }, - { - title: "without deletion protection unassigned teams are removed", - deletionProtection: false, - keepFlag: false, - expectRemoval: true, - }, - { - title: "with deletion protection & keep flag teams are not removed", - deletionProtection: false, - keepFlag: true, - expectRemoval: false, - }, - { - title: "without deletion protection but keep flag teams are not removed", - deletionProtection: true, - keepFlag: true, - expectRemoval: false, - }, - } { - t.Run(tc.title, func(t *testing.T) { - logger := zaptest.NewLogger(t).Sugar() - workflowCtx := defaultTestWorkflow(logger) - project := &akov2.AtlasProject{ - Spec: akov2.AtlasProjectSpec{ - Name: "projectName", - }, - } - team := &akov2.AtlasTeam{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testTeam", - Namespace: "testNS", - }, - } - teamsMock := &atlas.TeamsClientMock{ - RemoveTeamFromOrganizationFunc: func(orgID string, teamID string) (*mongodbatlas.Response, error) { - return nil, nil - }, - RemoveTeamFromOrganizationRequests: map[string]struct{}{}, - } - atlasProvider := &atlas.TestProvider{ - ClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*mongodbatlas.Client, string, error) { - return &mongodbatlas.Client{ - Teams: teamsMock, - }, "0987654321", nil + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + }, + Data: map[string][]byte{ + "orgId": []byte("0987654321"), + "publicApiKey": []byte("api-pub-key"), + "privateApiKey": []byte("api-priv-key"), + }, + Type: "Opaque", + } + project := &akov2.AtlasProject{ + Spec: akov2.AtlasProjectSpec{ + Name: "projectName", + ConnectionSecret: &common.ResourceRefNamespaced{ + Name: "my-secret", }, - } - reconciler := &AtlasProjectReconciler{ - Client: buildFakeKubernetesClient(project, team), - Log: logger, - AtlasProvider: atlasProvider, - ObjectDeletionProtection: tc.deletionProtection, - } - if tc.keepFlag { - customresource.SetAnnotation(project, - customresource.ResourcePolicyAnnotation, customresource.ResourcePolicyKeep) - } - err := reconciler.updateTeamState(workflowCtx, project, reference(team), true) - assert.NoError(t, err) - expectedRemovals := 0 - if tc.expectRemoval { - expectedRemovals = 1 - } - assert.Len(t, teamsMock.RemoveTeamFromOrganizationRequests, expectedRemovals) - }) - } - }) -} + }, + Status: status.AtlasProjectStatus{ + ID: "projectID", + }, + } -func defaultTestWorkflow(logger *zap.SugaredLogger) *workflow.Context { - return &workflow.Context{ - Context: context.Background(), - Log: logger, - } -} + testScheme := runtime.NewScheme() + assert.NoError(t, akov2.AddToScheme(testScheme)) + assert.NoError(t, corev1.AddToScheme(testScheme)) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(secret, project, tt.team). + WithStatusSubresource(tt.team). + Build() -func defaultTestScheme() *runtime.Scheme { - scheme := runtime.NewScheme() - akov2.AddToScheme(scheme) - corev1.AddToScheme(scheme) - return scheme -} + logger := zaptest.NewLogger(t).Sugar() + ctx := &workflow.Context{ + Context: context.Background(), + Log: logger, + } + r := &AtlasProjectReconciler{ + Client: k8sClient, + EventRecorder: record.NewFakeRecorder(1), + Log: logger, + } -func buildFakeKubernetesClient(objects ...client.Object) client.WithWatch { - return fake.NewClientBuilder(). - WithScheme(defaultTestScheme()). - WithObjects(objects...). - Build() -} + err := r.updateTeamState(ctx, project, &common.ResourceRefNamespaced{Name: tt.team.Name, Namespace: tt.team.Namespace}, tt.isRemoval) + assert.NoError(t, err) -func reference(obj client.Object) *common.ResourceRefNamespaced { - return &common.ResourceRefNamespaced{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), + actualTeam := &akov2.AtlasTeam{} + assert.NoError(t, k8sClient.Get(context.Background(), client.ObjectKeyFromObject(tt.team), actualTeam)) + assert.Equal(t, tt.expectedAssignedProjects, actualTeam.Status.Projects) + }) } } diff --git a/test/e2e/teams_test.go b/test/e2e/teams_test.go index 6630b2660b..69ee42f5a4 100644 --- a/test/e2e/teams_test.go +++ b/test/e2e/teams_test.go @@ -1,14 +1,10 @@ package e2e_test import ( - "context" - "errors" - "fmt" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "go.mongodb.org/atlas-sdk/v20231115008/admin" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,6 +32,7 @@ var _ = Describe("Teams", Label("teams"), func() { Expect(actions.SaveTeamsToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed()) } By("Delete Resources", func() { + actions.DeleteTestDataProject(testData) actions.AfterEachFinalCleanup([]model.TestDataProvider{*testData}) }) }) @@ -53,7 +50,7 @@ var _ = Describe("Teams", Label("teams"), func() { model.NewEmptyAtlasKeyType().UseDefaultFullAccess(), 40000, []func(*model.TestDataProvider){}, - ).WithProject(data.DefaultProject()).WithObjectDeletionProtection(true), + ).WithProject(data.DefaultProject()), []akov2.Team{ { TeamRef: common.ResourceRefNamespaced{ @@ -82,13 +79,13 @@ func projectTeamsFlow(userData *model.TestDataProvider, teams []akov2.Team) { userData.Project.Spec.Teams = teams Expect(userData.K8SClient.Update(userData.Context, userData.Project)).Should(Succeed()) Eventually(func(g Gomega) bool { - return ensureTeamsStatus(g, *userData, teams, teamWasCreated) - }).WithTimeout(10*time.Minute).WithPolling(20*time.Second).Should(BeTrue(), "Teams were not created") + return ensureTeamsStatus(g, *userData, teams, teamWasAssigned) + }).WithTimeout(5*time.Minute).WithPolling(10*time.Second).Should(BeTrue(), "Teams were not assigned") actions.WaitForConditionsToBecomeTrue(userData, api.ProjectTeamsReadyType, api.ReadyType) }) - By("Remove one team from the project", func() { + By("De-assign one team from the project", func() { Expect(userData.K8SClient.Get(userData.Context, types.NamespacedName{Name: userData.Project.Name, Namespace: userData.Project.Namespace}, userData.Project)).Should(Succeed()) @@ -97,24 +94,26 @@ func projectTeamsFlow(userData *model.TestDataProvider, teams []akov2.Team) { Expect(userData.K8SClient.Update(userData.Context, userData.Project)).Should(Succeed()) Eventually(func(g Gomega) bool { - return ensureTeamsStatus(g, *userData, teams[1:], teamWasRemoved) - }).WithTimeout(10*time.Minute).WithPolling(20*time.Second).Should(BeTrue(), "Team were not removed") + return ensureTeamsStatus(g, *userData, teams[1:], teamWasDeAssigned) + }).WithTimeout(5*time.Minute).WithPolling(10*time.Second).Should(BeTrue(), "Team were not removed") actions.WaitForConditionsToBecomeTrue(userData, api.ProjectTeamsReadyType, api.ReadyType) }) By("Update team role in the project", func() { + Expect(userData.K8SClient.Get(userData.Context, types.NamespacedName{Name: userData.Project.Name, + Namespace: userData.Project.Namespace}, userData.Project)).Should(Succeed()) userData.Project.Spec.Teams[0].Roles = []akov2.TeamRole{akov2.TeamRoleReadOnly} Expect(userData.K8SClient.Update(userData.Context, userData.Project)).Should(Succeed()) Eventually(func(g Gomega) bool { - return ensureTeamsStatus(g, *userData, userData.Project.Spec.Teams, teamWasCreated) - }).WithTimeout(10*time.Minute).WithPolling(20*time.Second).Should(BeTrue(), "Teams were not created") + return ensureTeamsStatus(g, *userData, userData.Project.Spec.Teams, teamWasAssigned) + }).WithTimeout(5*time.Minute).WithPolling(10*time.Second).Should(BeTrue(), "Teams were not assigned") actions.WaitForConditionsToBecomeTrue(userData, api.ProjectTeamsReadyType, api.ReadyType) }) - By("Remove all teams from the project", func() { + By("De-assign all teams from the project", func() { Expect(userData.K8SClient.Get(userData.Context, types.NamespacedName{Name: userData.Project.Name, Namespace: userData.Project.Namespace}, userData.Project)).Should(Succeed()) @@ -122,93 +121,62 @@ func projectTeamsFlow(userData *model.TestDataProvider, teams []akov2.Team) { Expect(userData.K8SClient.Update(userData.Context, userData.Project)).Should(Succeed()) Eventually(func(g Gomega) bool { - return ensureTeamsStatus(g, *userData, teams, teamWasRemoved) - }).WithTimeout(10*time.Minute).WithPolling(20*time.Second).Should(BeTrue(), "Teams were not removed") + return ensureTeamsStatus(g, *userData, teams, teamWasDeAssigned) + }).WithTimeout(5*time.Minute).WithPolling(10*time.Second).Should(BeTrue(), "Teams were not de-assigned") actions.CheckProjectConditionsNotSet(userData, api.ProjectTeamsReadyType) }) - if userData.ObjectDeletionProtection { + By("Cleanup Atlas Teams", func() { aClient := atlas.GetClientOrFail() - By("Cleanup Atlas Teams: which should have not been removed due to deletion protection", func() { - atlasTeams, err := listAtLeastNTeams(userData.Context, aClient, len(teams)) - Expect(err).Should(Succeed()) - Expect(clearAtlasTeams(teams, atlasTeams, aClient, userData)).Should(Succeed()) - }) - By("Cleanup Atlas Project: as deletion protection will skip that", func() { - Expect(userData.K8SClient.Get(userData.Context, types.NamespacedName{ - Name: userData.Project.Name, - Namespace: userData.Project.Namespace, - }, userData.Project)).Should(Succeed()) - projectID := userData.Project.Status.ID - Expect(userData.K8SClient.Delete(userData.Context, userData.Project)).Should(Succeed()) - _, _, err := aClient.Client.ProjectsApi.DeleteProject(userData.Context, projectID).Execute() - Expect(err).Should(Succeed()) - }) - } else { - actions.DeleteTestDataTeams(userData) - actions.DeleteTestDataProject(userData) - } + for _, AssociatedTeam := range teams { + team := &akov2.AtlasTeam{} + Expect(userData.K8SClient.Get(userData.Context, types.NamespacedName{Name: AssociatedTeam.TeamRef.Name, Namespace: userData.Resources.Namespace}, team)).Should(Succeed()) + _, _, err := aClient.Client.TeamsApi.DeleteTeam(userData.Context, aClient.OrgID, team.Status.ID).Execute() + Expect(err).ToNot(HaveOccurred()) + Expect(userData.K8SClient.Delete(userData.Context, team)).To(Succeed()) + } + }) } -func ensureTeamsStatus(g Gomega, testData model.TestDataProvider, teams []akov2.Team, check func(res *akov2.AtlasTeam) bool) bool { +func ensureTeamsStatus(g Gomega, testData model.TestDataProvider, teams []akov2.Team, check func(team *akov2.AtlasTeam, project *akov2.AtlasProject) bool) bool { for _, team := range teams { resource := &akov2.AtlasTeam{} g.Expect(testData.K8SClient.Get(testData.Context, types.NamespacedName{Name: team.TeamRef.Name, Namespace: testData.Resources.Namespace}, resource)).Should(Succeed()) - if !check(resource) { + if !check(resource, testData.Project) { return false } } return true } -func teamWasCreated(team *akov2.AtlasTeam) bool { - return team.Status.ID != "" -} - -func teamWasRemoved(team *akov2.AtlasTeam) bool { - return team.Status.ID == "" -} - -func listAtLeastNTeams(ctx context.Context, aClient *atlas.Atlas, minTeams int) ([]admin.TeamResponse, error) { - results := []admin.TeamResponse{} - teamsReply, _, err := aClient.Client.TeamsApi.ListOrganizationTeams(ctx, aClient.OrgID).Execute() - if err != nil { - return results, fmt.Errorf("failed to list teams: %w", err) +func teamWasAssigned(team *akov2.AtlasTeam, project *akov2.AtlasProject) bool { + if team.Status.ID == "" { + return false } - total, ok := teamsReply.GetTotalCountOk() - if !ok { - return results, errors.New("no results") - } - if *total < minTeams { - return results, fmt.Errorf("not enough teams: expected %d but got %d", minTeams, *total) - } - return teamsReply.GetResults(), nil -} -func clearAtlasTeams(teams []akov2.Team, atlasTeams []admin.TeamResponse, aClient *atlas.Atlas, userData *model.TestDataProvider) error { - var errs error - for _, team := range teams { - foundAtlasTeam := findTeam(atlasTeams, team.TeamRef.Name) - if foundAtlasTeam == nil { - errs = errors.Join(errs, fmt.Errorf("failed to find expected Atlas team %s (was it wrongly removed?)", team.TeamRef.Name)) - } - _, _, err := aClient.Client.TeamsApi.DeleteTeam(userData.Context, aClient.OrgID, foundAtlasTeam.GetId()).Execute() - if err != nil { - errs = errors.Join(errs, err) + for _, p := range team.Status.Projects { + if p.ID == project.ID() { + return true } } - return errs + + return len(team.Finalizers) > 0 } -func findTeam(atlasTeams []admin.TeamResponse, teamName string) *admin.TeamResponse { - for _, atlasTeam := range atlasTeams { - if teamName == atlasTeam.GetName() { - return &atlasTeam +func teamWasDeAssigned(team *akov2.AtlasTeam, project *akov2.AtlasProject) bool { + if team.Status.ID == "" { + return false + } + + for _, p := range team.Status.Projects { + if p.ID == project.ID() { + return false } } - return nil + + return len(team.Finalizers) == 0 }