Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
91192c2
Extracted users attribute common schema to dsschema/users_schema.go
Jul 9, 2025
83bef3a
Added attribute users to team data source
Jul 9, 2025
b486498
Modified tests
Jul 9, 2025
ac113ed
Modified doc
Jul 9, 2025
ae5fe56
Added changelog
Jul 9, 2025
25b4b63
Created ressource, schema and models
Jul 10, 2025
138c010
WIP - Implementing Create method
Jul 10, 2025
9c29d84
Merge branch 'CLOUDP-320243-dev-2.0.0' into CLOUDP-329984
Jul 11, 2025
776b107
WIP - Added markdown description for attributes in schema and fixed m…
Jul 14, 2025
4c518ef
Fixed variables names
Jul 15, 2025
0865818
WIP - Implement Read, Update, Delete, Import
Jul 15, 2025
6329b76
Fixed model attributes names.
Jul 16, 2025
13ad925
Minor change
Jul 16, 2025
78464a5
Added tests
Jul 16, 2025
bb0aaae
Fix
Jul 16, 2025
11b6f56
Add github
Jul 16, 2025
005b0c8
Fix
Jul 16, 2025
dd638e6
Merge branch 'CLOUDP-320243-dev-2.0.0' into CLOUDP-329984
Jul 16, 2025
a810a32
Fix
Jul 16, 2025
10a3c1f
Changelog
Jul 16, 2025
a54e77c
Fix
Jul 16, 2025
389c42a
Fixed function naming
Jul 16, 2025
d1dd176
Merge branch 'CLOUDP-320243-dev-2.0.0' into CLOUDP-329984
Jul 17, 2025
7c3c11f
Changed test
Jul 17, 2025
7ebd8d2
Changed `project_role_assignments` from List to Set
Jul 18, 2025
3d467d0
Changed the attribute name `project_roles_assignments` to project_rol…
Jul 21, 2025
73b3bfa
Merge branch 'CLOUDP-320243-dev-2.0.0' into CLOUDP-329984
Jul 21, 2025
eba734d
minor changes
Jul 21, 2025
9519315
Fix
Jul 21, 2025
adf5af5
Fix
Jul 21, 2025
bfd3817
Fix
Jul 21, 2025
057f72c
Changed `project_role_assignments` from List to Set
Jul 21, 2025
57c73b3
Removed redundant check
Jul 22, 2025
503bbfc
Code improvements
Jul 23, 2025
a36d888
feat: Adds new singular data source `mongodbatlas_cloud_user_team_ass…
csanx Jul 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/3502.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
resource/mongodbatlas_cloud_user_team_assignment
```
29 changes: 28 additions & 1 deletion .github/workflows/acceptance-tests-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ jobs:
autogen: ${{ steps.filter.outputs.autogen == 'true' || env.mustTrigger == 'true' }}
backup: ${{ steps.filter.outputs.backup == 'true' || env.mustTrigger == 'true' }}
control_plane_ip_addresses: ${{ steps.filter.outputs.control_plane_ip_addresses == 'true' || env.mustTrigger == 'true' }}
cloud_user: ${{ steps.filter.outputs.cloud_user == 'true' || env.mustTrigger == 'true' }}
cluster: ${{ steps.filter.outputs.cluster == 'true' || env.mustTrigger == 'true' }}
cluster_outage_simulation: ${{ steps.filter.outputs.cluster_outage_simulation == 'true' || env.mustTrigger == 'true' }}
config: ${{ steps.filter.outputs.config == 'true' || env.mustTrigger == 'true' }}
Expand Down Expand Up @@ -299,6 +300,8 @@ jobs:
- 'internal/service/onlinearchive/*.go'
control_plane_ip_addresses:
- 'internal/service/controlplaneipaddresses/*.go'
cloud_user:
- 'internal/service/clouduserteamassignment/*.go'
cluster:
- 'internal/service/cluster/*.go'
cluster_outage_simulation:
Expand Down Expand Up @@ -589,7 +592,31 @@ jobs:
MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }}
ACCTEST_PACKAGES: ./internal/service/controlplaneipaddresses
run: make testacc


cloud_user:
needs: [ change-detection, get-provider-version ]
if: ${{ needs.change-detection.outputs.cloud_user == 'true' || inputs.test_group == 'cloud_user' }}
runs-on: ubuntu-latest
permissions: {}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ inputs.ref || github.ref }}
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
with:
go-version-file: 'go.mod'
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
with:
terraform_version: ${{ inputs.terraform_version }}
terraform_wrapper: false
- name: Acceptance Tests
env:
MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }}
MONGODB_ATLAS_PROJECT_OWNER_ID: ${{ inputs.mongodb_atlas_project_owner_id }}
MONGODB_ATLAS_ORG_ID: ${{ inputs.mongodb_atlas_org_id }}
ACCTEST_PACKAGES: ./internal/service/clouduserteamassignment
run: make testacc

cluster:
needs: [ change-detection, get-provider-version ]
if: ${{ needs.change-detection.outputs.cluster == 'true' || inputs.test_group == 'cluster' }}
Expand Down
2 changes: 2 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/alertconfiguration"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/apikeyprojectassignment"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/atlasuser"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/clouduserteamassignment"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/controlplaneipaddresses"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/databaseuser"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/encryptionatrest"
Expand Down Expand Up @@ -484,6 +485,7 @@ func (p *MongodbtlasProvider) Resources(context.Context) []func() resource.Resou
flexcluster.Resource,
resourcepolicy.Resource,
apikeyprojectassignment.Resource,
clouduserteamassignment.Resource,
}
if config.PreviewProviderV2AdvancedCluster() {
resources = append(resources, advancedclustertpf.Resource)
Expand Down
15 changes: 15 additions & 0 deletions internal/service/clouduserteamassignment/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package clouduserteamassignment_test

import (
"os"
"testing"

"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc"
)

func TestMain(m *testing.M) {
cleanup := acc.SetupSharedResources()
exitCode := m.Run()
cleanup()
os.Exit(exitCode)
}
109 changes: 109 additions & 0 deletions internal/service/clouduserteamassignment/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package clouduserteamassignment

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
"go.mongodb.org/atlas-sdk/v20250312005/admin"
)

func NewTFUserTeamAssignmentModel(ctx context.Context, apiResp *admin.OrgUserResponse) (*TFUserTeamAssignmentModel, diag.Diagnostics) {
diags := diag.Diagnostics{}
var rolesObj types.Object
var rolesDiags diag.Diagnostics

if apiResp == nil {
return nil, diags
}

rolesObj, rolesDiags = NewTFRolesModel(ctx, &apiResp.Roles)
diags.Append(rolesDiags...)

userTeamAssignment := TFUserTeamAssignmentModel{
UserId: types.StringValue(apiResp.GetId()),
Username: types.StringValue(apiResp.GetUsername()),
OrgMembershipStatus: types.StringValue(apiResp.GetOrgMembershipStatus()),
Roles: rolesObj,
InvitationCreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.InvitationCreatedAt)),
InvitationExpiresAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.InvitationExpiresAt)),
InviterUsername: types.StringPointerValue(apiResp.InviterUsername),
Country: types.StringPointerValue(apiResp.Country),
FirstName: types.StringPointerValue(apiResp.FirstName),
LastName: types.StringPointerValue(apiResp.LastName),
CreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.CreatedAt)),
LastAuth: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.LastAuth)),
MobileNumber: types.StringPointerValue(apiResp.MobileNumber),
}

userTeamAssignment.TeamIDs = types.SetNull(types.StringType)
if apiResp.TeamIds != nil {
userTeamAssignment.TeamIDs, diags = types.SetValueFrom(ctx, types.StringType, apiResp.TeamIds)
if diags.HasError() {
return nil, diags
}
}

return &userTeamAssignment, nil
}

func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (types.Object, diag.Diagnostics) {
diags := diag.Diagnostics{}

if roles == nil {
return types.ObjectNull(RolesObjectAttrTypes), diags
}

var orgRoles types.Set
if roles.OrgRoles == nil || len(*roles.OrgRoles) == 0 {
orgRoles = types.SetNull(types.StringType)
} else {
orgRoles, _ = types.SetValueFrom(ctx, types.StringType, *roles.OrgRoles)
}

projectRoleAssignmentsList := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments)

rolesObj, _ := types.ObjectValue(
RolesObjectAttrTypes,
map[string]attr.Value{
"project_role_assignments": projectRoleAssignmentsList,
"org_roles": orgRoles,
},
)

return rolesObj, diags
}

func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]admin.GroupRoleAssignment) types.List {
if groupRoleAssignments == nil {
return types.ListNull(ProjectRoleAssignmentsAttrType)
}

var projectRoleAssignments []TFProjectRoleAssignmentsModel

for _, pra := range *groupRoleAssignments {
projectID := types.StringPointerValue(pra.GroupId)
var projectRoles types.Set
if pra.GroupRoles == nil || len(*pra.GroupRoles) == 0 {
projectRoles = types.SetNull(types.StringType)
} else {
projectRoles, _ = types.SetValueFrom(ctx, types.StringType, pra.GroupRoles)
}
projectRoleAssignments = append(projectRoleAssignments, TFProjectRoleAssignmentsModel{
ProjectId: projectID,
ProjectRoles: projectRoles,
})
}

praList, _ := types.ListValueFrom(ctx, ProjectRoleAssignmentsAttrType.ElemType.(types.ObjectType), projectRoleAssignments)
return praList
}

func NewUserTeamAssignmentReq(ctx context.Context, plan *TFUserTeamAssignmentModel) (*admin.AddOrRemoveUserFromTeam, diag.Diagnostics) {
addOrRemoveUserFromTeam := admin.AddOrRemoveUserFromTeam{
Id: plan.UserId.ValueString(),
}
return &addOrRemoveUserFromTeam, nil
}
186 changes: 186 additions & 0 deletions internal/service/clouduserteamassignment/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package clouduserteamassignment_test

import (
"context"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/clouduserteamassignment"
"github.com/stretchr/testify/assert"
"go.mongodb.org/atlas-sdk/v20250312005/admin"
)

const (
testUserID = "user-123"
testUsername = "jdoe"
testFirstName = "John"
testLastName = "Doe"
testCountry = "CA"
testMobile = "+1555123456"
testInviter = "admin"
testOrgMembershipStatus = "ACTIVE"
testInviterUsername = ""

testOrgRoleOwner = "ORG_OWNER"
testOrgRoleMember = "ORG_MEMBER"
testProjectRoleOwner = "PROJECT_OWNER"
testProjectRoleRead = "PROJECT_READ_ONLY"
testProjectRoleMember = "PROJECT_MEMBER"

testTeamID1 = "team1"
testTeamID2 = "team2"
testProjectID1 = "project-123"
testOrgID = "org-123"
)

var (
when = time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
testCreatedAt = when.Format(time.RFC3339)
testInvitationCreatedAt = when.Add(-24 * time.Hour).Format(time.RFC3339)
testInvitationExpiresAt = when.Add(24 * time.Hour).Format(time.RFC3339)
testLastAuth = when.Add(-2 * time.Hour).Format(time.RFC3339)

testTeamIDs = []string{"team1", "team2"}
testOrgRoles = []string{"owner", "readWrite"}
)

type sdkToTFModelTestCase struct {
SDKResp *admin.OrgUserResponse
expectedTFModel *clouduserteamassignment.TFUserTeamAssignmentModel
}

func TestUserTeamAssignmentSDKToTFModel(t *testing.T) {
ctx := t.Context()

fullResp := &admin.OrgUserResponse{
Id: testUserID,
Username: testUsername,
FirstName: admin.PtrString(testFirstName),
LastName: admin.PtrString(testLastName),
Country: admin.PtrString(testCountry),
MobileNumber: admin.PtrString(testMobile),
OrgMembershipStatus: testOrgMembershipStatus,
CreatedAt: admin.PtrTime(when),
LastAuth: admin.PtrTime(when.Add(-2 * time.Hour)),
InvitationCreatedAt: admin.PtrTime(when.Add(-24 * time.Hour)),
InvitationExpiresAt: admin.PtrTime(when.Add(24 * time.Hour)),
InviterUsername: admin.PtrString(testInviterUsername),
TeamIds: &testTeamIDs,
Roles: admin.OrgUserRolesResponse{
OrgRoles: &testOrgRoles,
},
}

orgRolesSet, _ := types.SetValueFrom(ctx, types.StringType, testOrgRoles)
expectedRoles, _ := types.ObjectValue(clouduserteamassignment.RolesObjectAttrTypes, map[string]attr.Value{
"org_roles": orgRolesSet,
"project_role_assignments": types.ListNull(clouduserteamassignment.ProjectRoleAssignmentsAttrType),
})
expectedTeams, _ := types.SetValueFrom(ctx, types.StringType, testTeamIDs)
expectedFullModel := &clouduserteamassignment.TFUserTeamAssignmentModel{
UserId: types.StringValue(testUserID),
Username: types.StringValue(testUsername),
FirstName: types.StringValue(testFirstName),
LastName: types.StringValue(testLastName),
Country: types.StringValue(testCountry),
MobileNumber: types.StringValue(testMobile),
OrgMembershipStatus: types.StringValue(testOrgMembershipStatus),
CreatedAt: types.StringValue(testCreatedAt),
LastAuth: types.StringValue(testLastAuth),
InvitationCreatedAt: types.StringValue(testInvitationCreatedAt),
InvitationExpiresAt: types.StringValue(testInvitationExpiresAt),
InviterUsername: types.StringValue(testInviterUsername),
OrgId: types.StringNull(),
TeamId: types.StringNull(),
Roles: expectedRoles,
TeamIDs: expectedTeams,
}

testCases := map[string]sdkToTFModelTestCase{
"nil SDK response": {
SDKResp: nil,
expectedTFModel: nil,
},
"Complete SDK response": {
SDKResp: fullResp,
expectedTFModel: expectedFullModel,
},
}

for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
resultModel, diags := clouduserteamassignment.NewTFUserTeamAssignmentModel(t.Context(), tc.SDKResp)
assert.False(t, diags.HasError(), "expected no diagnostics")
assert.Equal(t, tc.expectedTFModel, resultModel, "created terraform model did not match expected output")
})
}
}

func createRolesObject(ctx context.Context, orgRoles []string, projectAssignments []clouduserteamassignment.TFProjectRoleAssignmentsModel) types.Object {
orgRolesSet, _ := types.SetValueFrom(ctx, types.StringType, orgRoles)

var projectRoleAssignmentsList types.List
if len(projectAssignments) == 0 {
projectRoleAssignmentsList = types.ListNull(clouduserteamassignment.ProjectRoleAssignmentsAttrType.ElemType.(types.ObjectType))
} else {
projectRoleAssignmentsList, _ = types.ListValueFrom(ctx, clouduserteamassignment.ProjectRoleAssignmentsAttrType.ElemType.(types.ObjectType), projectAssignments)
}

obj, _ := types.ObjectValue(
clouduserteamassignment.RolesObjectAttrTypes,
map[string]attr.Value{
"org_roles": orgRolesSet,
"project_role_assignments": projectRoleAssignmentsList,
},
)
return obj
}

func TestNewUserTeamAssignmentReq(t *testing.T) {
ctx := t.Context()
projectAssignment := clouduserteamassignment.TFProjectRoleAssignmentsModel{
ProjectId: types.StringValue(testProjectID1),
ProjectRoles: types.SetValueMust(types.StringType, []attr.Value{types.StringValue(testProjectRoleOwner)}),
}
teams, _ := types.SetValueFrom(ctx, types.StringType, testTeamIDs)
testCases := map[string]struct {
plan *clouduserteamassignment.TFUserTeamAssignmentModel
expected *admin.AddOrRemoveUserFromTeam
}{
"Complete model": {
plan: &clouduserteamassignment.TFUserTeamAssignmentModel{
OrgId: types.StringValue(testOrgID),
TeamId: types.StringValue(testTeamID1),
UserId: types.StringValue(testUserID),
Username: types.StringValue(testUsername),
OrgMembershipStatus: types.StringValue(testOrgMembershipStatus),
Roles: createRolesObject(ctx, testOrgRoles, []clouduserteamassignment.TFProjectRoleAssignmentsModel{
projectAssignment,
}),
TeamIDs: teams,
InvitationCreatedAt: types.StringValue(testInvitationCreatedAt),
InvitationExpiresAt: types.StringValue(testInvitationExpiresAt),
InviterUsername: types.StringValue(testInviterUsername),
Country: types.StringValue(testCountry),
FirstName: types.StringValue(testFirstName),
LastName: types.StringValue(testLastName),
CreatedAt: types.StringValue(testCreatedAt),
LastAuth: types.StringValue(testLastAuth),
MobileNumber: types.StringValue(testMobile),
},
expected: &admin.AddOrRemoveUserFromTeam{
Id: testUserID,
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
req, diags := clouduserteamassignment.NewUserTeamAssignmentReq(ctx, tc.plan)
assert.False(t, diags.HasError(), "expected no diagnostics")
assert.Equal(t, tc.expected, req)
})
}
}
Loading
Loading