From 91192c2d7aec39c8a9fee444939e66ff2686c4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 9 Jul 2025 11:53:01 +0200 Subject: [PATCH 01/30] Extracted users attribute common schema to dsschema/users_schema.go --- internal/common/dsschema/users_schema.go | 153 ++++++++++++++++++ .../organization/data_source_organization.go | 150 +---------------- .../organization/data_source_organizations.go | 5 +- 3 files changed, 158 insertions(+), 150 deletions(-) create mode 100644 internal/common/dsschema/users_schema.go diff --git a/internal/common/dsschema/users_schema.go b/internal/common/dsschema/users_schema.go new file mode 100644 index 0000000000..dd8155e7a8 --- /dev/null +++ b/internal/common/dsschema/users_schema.go @@ -0,0 +1,153 @@ +package dsschema + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "go.mongodb.org/atlas-sdk/v20250312005/admin" +) + +var ( + DSOrgUsersSchema = schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "org_membership_status": { + Type: schema.TypeString, + Computed: true, + }, + "roles": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "org_roles": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "project_roles_assignments": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Computed: true, + }, + "project_roles": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + "team_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "username": { + Type: schema.TypeString, + Computed: true, + }, + "invitation_created_at": { + Type: schema.TypeString, + Computed: true, + }, + "invitation_expires_at": { + Type: schema.TypeString, + Computed: true, + }, + "inviter_username": { + Type: schema.TypeString, + Computed: true, + }, + "country": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + "first_name": { + Type: schema.TypeString, + Computed: true, + }, + "last_auth": { + Type: schema.TypeString, + Computed: true, + }, + "last_name": { + Type: schema.TypeString, + Computed: true, + }, + "mobile_number": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + } +) + +func FlattenUsers(users []admin.OrgUserResponse) []map[string]any { + ret := make([]map[string]any, len(users)) + for i := range users { + user := &users[i] + ret[i] = map[string]any{ + "id": user.GetId(), + "org_membership_status": user.GetOrgMembershipStatus(), + "roles": FlattenUserRoles(user.GetRoles()), + "team_ids": user.GetTeamIds(), + "username": user.GetUsername(), + "invitation_created_at": user.GetInvitationCreatedAt().Format(time.RFC3339), + "invitation_expires_at": user.GetInvitationExpiresAt().Format(time.RFC3339), + "inviter_username": user.GetInviterUsername(), + "country": user.GetCountry(), + "created_at": user.GetCreatedAt().Format(time.RFC3339), + "first_name": user.GetFirstName(), + "last_auth": user.GetLastAuth().Format(time.RFC3339), + "last_name": user.GetLastName(), + "mobile_number": user.GetMobileNumber(), + } + } + return ret +} + +func FlattenUserRoles(roles admin.OrgUserRolesResponse) []map[string]any { + ret := make([]map[string]any, 0) + roleMap := map[string]any{ + "org_roles": []string{}, + "project_roles_assignments": []map[string]any{}, + } + if roles.HasOrgRoles() { + roleMap["org_roles"] = roles.GetOrgRoles() + } + if roles.HasGroupRoleAssignments() { + roleMap["project_roles_assignments"] = FlattenProjectRolesAssignments(roles.GetGroupRoleAssignments()) + } + ret = append(ret, roleMap) + return ret +} + +func FlattenProjectRolesAssignments(assignments []admin.GroupRoleAssignment) []map[string]any { + ret := make([]map[string]any, 0, len(assignments)) + for _, assignment := range assignments { + ret = append(ret, map[string]any{ + "project_id": assignment.GetGroupId(), + "project_roles": assignment.GetGroupRoles(), + }) + } + return ret +} diff --git a/internal/service/organization/data_source_organization.go b/internal/service/organization/data_source_organization.go index 027aea6dbb..6ef658af62 100644 --- a/internal/service/organization/data_source_organization.go +++ b/internal/service/organization/data_source_organization.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -15,100 +14,6 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) -var ( - DSOrgUsersSchema = schema.Schema{ - Type: schema.TypeSet, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - }, - "org_membership_status": { - Type: schema.TypeString, - Computed: true, - }, - "roles": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "org_roles": { - Type: schema.TypeSet, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "project_roles_assignments": { - Type: schema.TypeSet, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeString, - Computed: true, - }, - "project_roles": { - Type: schema.TypeSet, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - }, - }, - }, - }, - }, - }, - "team_ids": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "username": { - Type: schema.TypeString, - Computed: true, - }, - "invitation_created_at": { - Type: schema.TypeString, - Computed: true, - }, - "invitation_expires_at": { - Type: schema.TypeString, - Computed: true, - }, - "inviter_username": { - Type: schema.TypeString, - Computed: true, - }, - "country": { - Type: schema.TypeString, - Computed: true, - }, - "created_at": { - Type: schema.TypeString, - Computed: true, - }, - "first_name": { - Type: schema.TypeString, - Computed: true, - }, - "last_auth": { - Type: schema.TypeString, - Computed: true, - }, - "last_name": { - Type: schema.TypeString, - Computed: true, - }, - "mobile_number": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - } -) - func DataSource() *schema.Resource { return &schema.Resource{ ReadContext: dataSourceRead, @@ -141,7 +46,7 @@ func DataSource() *schema.Resource { }, }, }, - "users": &DSOrgUsersSchema, + "users": &dsschema.DSOrgUsersSchema, "api_access_list_required": { Type: schema.TypeBool, Computed: true, @@ -170,57 +75,6 @@ func DataSource() *schema.Resource { } } -func flattenUsers(users []admin.OrgUserResponse) []map[string]any { - ret := make([]map[string]any, len(users)) - for i := range users { - user := &users[i] - ret[i] = map[string]any{ - "id": user.GetId(), - "org_membership_status": user.GetOrgMembershipStatus(), - "roles": flattenUserRoles(user.GetRoles()), - "team_ids": user.GetTeamIds(), - "username": user.GetUsername(), - "invitation_created_at": user.GetInvitationCreatedAt().Format(time.RFC3339), - "invitation_expires_at": user.GetInvitationExpiresAt().Format(time.RFC3339), - "inviter_username": user.GetInviterUsername(), - "country": user.GetCountry(), - "created_at": user.GetCreatedAt().Format(time.RFC3339), - "first_name": user.GetFirstName(), - "last_auth": user.GetLastAuth().Format(time.RFC3339), - "last_name": user.GetLastName(), - "mobile_number": user.GetMobileNumber(), - } - } - return ret -} - -func flattenUserRoles(roles admin.OrgUserRolesResponse) []map[string]any { - ret := make([]map[string]any, 0) - roleMap := map[string]any{ - "org_roles": []string{}, - "project_roles_assignments": []map[string]any{}, - } - if roles.HasOrgRoles() { - roleMap["org_roles"] = roles.GetOrgRoles() - } - if roles.HasGroupRoleAssignments() { - roleMap["project_roles_assignments"] = flattenProjectRolesAssignments(roles.GetGroupRoleAssignments()) - } - ret = append(ret, roleMap) - return ret -} - -func flattenProjectRolesAssignments(assignments []admin.GroupRoleAssignment) []map[string]any { - ret := make([]map[string]any, 0, len(assignments)) - for _, assignment := range assignments { - ret = append(ret, map[string]any{ - "project_id": assignment.GetGroupId(), - "project_roles": assignment.GetGroupRoles(), - }) - } - return ret -} - func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { conn := meta.(*config.MongoDBClient).AtlasV2 orgID := d.Get("org_id").(string) @@ -251,7 +105,7 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. if err != nil { return diag.FromErr(fmt.Errorf("error getting organization users: %s", err)) } - if err := d.Set("users", flattenUsers(users)); err != nil { + if err := d.Set("users", dsschema.FlattenUsers(users)); err != nil { return diag.FromErr(fmt.Errorf("error setting `users`: %s", err)) } diff --git a/internal/service/organization/data_source_organizations.go b/internal/service/organization/data_source_organizations.go index 8a51357634..e39b8c92b3 100644 --- a/internal/service/organization/data_source_organizations.go +++ b/internal/service/organization/data_source_organizations.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/dsschema" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) @@ -63,7 +64,7 @@ func PluralDataSource() *schema.Resource { }, }, }, - "users": &DSOrgUsersSchema, + "users": &dsschema.DSOrgUsersSchema, "api_access_list_required": { Type: schema.TypeBool, Computed: true, @@ -153,7 +154,7 @@ func flattenOrganizations(ctx context.Context, conn *admin.APIClient, organizati "skip_default_alerts_settings": organization.SkipDefaultAlertsSettings, "is_deleted": organization.IsDeleted, "links": conversion.FlattenLinks(organization.GetLinks()), - "users": flattenUsers(users), + "users": dsschema.FlattenUsers(users), "api_access_list_required": settings.ApiAccessListRequired, "multi_factor_auth_required": settings.MultiFactorAuthRequired, "restrict_employee_access": settings.RestrictEmployeeAccess, From 83bef3a8a2d00689d50a1aa99fe0b4e65efa4fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 9 Jul 2025 12:56:34 +0200 Subject: [PATCH 02/30] Added attribute users to team data source --- internal/service/team/data_source_team.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/service/team/data_source_team.go b/internal/service/team/data_source_team.go index 927a4c5eed..979455dd62 100644 --- a/internal/service/team/data_source_team.go +++ b/internal/service/team/data_source_team.go @@ -4,14 +4,17 @@ import ( "context" "errors" "fmt" + "net/http" admin20241113 "go.mongodb.org/atlas-sdk/v20241113005/admin" + "go.mongodb.org/atlas-sdk/v20250312005/admin" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/dsschema" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) @@ -42,6 +45,7 @@ func DataSource() *schema.Resource { Type: schema.TypeString, }, }, + "users": &dsschema.DSOrgUsersSchema, }, } } @@ -55,6 +59,7 @@ func LegacyTeamsDataSource() *schema.Resource { func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var ( connV2 = meta.(*config.MongoDBClient).AtlasV220241113 + conn = meta.(*config.MongoDBClient).AtlasV2 orgID = d.Get("org_id").(string) teamID, teamIDOk = d.GetOk("team_id") name, nameOk = d.GetOk("name") @@ -85,7 +90,7 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. return diag.FromErr(fmt.Errorf(errorTeamSetting, "name", d.Id(), err)) } - teamUsers, err := listAllTeamUsers(ctx, connV2, orgID, team.GetId()) + teamUsers, err := listAllTeamUsersDS(ctx, conn, orgID, team.GetId()) if err != nil { return diag.FromErr(fmt.Errorf(errorTeamRead, err)) @@ -100,6 +105,10 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. return diag.FromErr(fmt.Errorf(errorTeamSetting, "usernames", d.Id(), err)) } + if err := d.Set("users", dsschema.FlattenUsers(teamUsers)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `users`: %s", err)) + } + d.SetId(conversion.EncodeStateID(map[string]string{ "org_id": orgID, "id": team.GetId(), @@ -107,3 +116,11 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag. return nil } + +func listAllTeamUsersDS(ctx context.Context, conn *admin.APIClient, orgID, teamID string) ([]admin.OrgUserResponse, error) { + return dsschema.AllPages(ctx, func(ctx context.Context, pageNum int) (dsschema.PaginateResponse[admin.OrgUserResponse], *http.Response, error) { + request := conn.MongoDBCloudUsersApi.ListTeamUsers(ctx, orgID, teamID) + request = request.PageNum(pageNum) + return request.Execute() + }) +} From b486498bba62df96b534ea600959a8eefc729578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 9 Jul 2025 12:57:32 +0200 Subject: [PATCH 03/30] Modified tests --- .../service/team/data_source_team_test.go | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/internal/service/team/data_source_team_test.go b/internal/service/team/data_source_team_test.go index 0eac53e4a5..ecb4884c7d 100644 --- a/internal/service/team/data_source_team_test.go +++ b/internal/service/team/data_source_team_test.go @@ -3,6 +3,7 @@ package team_test import ( "fmt" "os" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -29,6 +30,11 @@ func TestAccConfigDSTeam_basic(t *testing.T) { resource.TestCheckResourceAttrSet(dataSourceName, "team_id"), resource.TestCheckResourceAttr(dataSourceName, "name", name), resource.TestCheckResourceAttr(dataSourceName, "usernames.#", "1"), + resource.TestCheckResourceAttrSet(dataSourceName, "users.0.team_ids.0"), + resource.TestCheckResourceAttrSet(dataSourceName, "users.0.roles.0.project_roles_assignments.#"), + resource.TestMatchResourceAttr(dataSourceName, "users.0.username", regexp.MustCompile(`.*@mongodb\.com$`)), + resource.TestMatchResourceAttr(dataSourceName, "users.0.last_auth", regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$`)), // Follows RFC3339 timestamp + resource.TestMatchResourceAttr(dataSourceName, "users.0.created_at", regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$`)), // Follows RFC3339 timestamp ), }, }, @@ -61,6 +67,32 @@ func TestAccConfigDSTeamByName_basic(t *testing.T) { }) } +func TestAccConfigDSTeam_NoUsers(t *testing.T) { + var ( + dataSourceName = "data.mongodbatlas_team.test3" + orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") + name = acc.RandomName() + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.PreCheckAtlasUsername(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + CheckDestroy: acc.CheckDestroyTeam, + Steps: []resource.TestStep{ + { + Config: dataSourceConfigNoUsers(orgID, name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(dataSourceName, "org_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "team_id"), + resource.TestCheckResourceAttr(dataSourceName, "name", name), + resource.TestCheckResourceAttr(dataSourceName, "usernames.#", "0"), + resource.TestCheckResourceAttr(dataSourceName, "users.#", "0"), + ), + }, + }, + }) +} + func dataSourceConfigBasic(orgID, name, username string) string { return fmt.Sprintf(` resource "mongodbatlas_team" "test" { @@ -91,3 +123,19 @@ func dataSourceConfigBasicByName(orgID, name, username string) string { } `, orgID, name, username) } + +func dataSourceConfigNoUsers(orgID, name string) string { + return fmt.Sprintf(` + resource "mongodbatlas_team" "test" { + org_id = "%s" + name = "%s" + usernames = [] + } + + data "mongodbatlas_team" "test3" { + org_id = mongodbatlas_team.test.org_id + team_id = mongodbatlas_team.test.team_id + } + + `, orgID, name) +} From ac113eda2688a1f8e0846c37fe2ce4b174994a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 9 Jul 2025 12:59:20 +0200 Subject: [PATCH 04/30] Modified doc --- docs/data-sources/team.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/data-sources/team.md b/docs/data-sources/team.md index a15e880541..19b6c1b9d3 100644 --- a/docs/data-sources/team.md +++ b/docs/data-sources/team.md @@ -51,5 +51,26 @@ In addition to all arguments above, the following attributes are exported: * `team_id` - The unique identifier for the team. * `name` - The name of the team you want to create. * `usernames` - The users who are part of the organization. +* `users`- Returns a list of all pending and active MongoDB Cloud users associated with the specified organization. + +### Users +* `id` - Unique 24-hexadecimal digit string that identifies the MongoDB Cloud user. +* `org_membership_status` - String enum that indicates whether the MongoDB Cloud user has a pending invitation to join the organization or they are already active in the organization. +* `roles` - Organization- and project-level roles assigned to one MongoDB Cloud user within one organization. +* `team_ids` - List of unique 24-hexadecimal digit strings that identifies the teams to which this MongoDB Cloud user belongs. +* `username` - Email address that represents the username of the MongoDB Cloud user. +* `country` - Two-character alphabetical string that identifies the MongoDB Cloud user's geographic location. This parameter uses the ISO 3166-1a2 code format. +* `invitation_created_at` - Date and time when MongoDB Cloud sent the invitation. MongoDB Cloud represents this timestamp in ISO 8601 format in UTC. +* `invitation_expires_at` - Date and time when the invitation from MongoDB Cloud expires. MongoDB Cloud represents this timestamp in ISO 8601 format in UTC. +* `inviter_username` - Username of the MongoDB Cloud user who sent the invitation to join the organization. +* `created_at` - Date and time when MongoDB Cloud created the current account. This value is in the ISO 8601 timestamp format in UTC. +* `first_name` - First or given name that belongs to the MongoDB Cloud user. +* `last_auth` - Date and time when the current account last authenticated. This value is in the ISO 8601 timestamp format in UTC. +* `last_name` - Last name, family name, or surname that belongs to the MongoDB Cloud user. +* `mobile_number` - Mobile phone number that belongs to the MongoDB Cloud user. + + +~> **NOTE:** - Users with pending invitations created using [`mongodbatlas_project_invitation`](../resources/project_invitation.md) resource or via the deprecated [Invite One MongoDB Cloud User to Join One Project](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-createprojectinvitation) endpoint are excluded (or cannot be managed) with this resource. See [MongoDB Atlas API] for details. +To manage these users with this resource/data source, refer to our [migration guide]. See detailed information for arguments and attributes: [MongoDB API Teams](https://docs.atlas.mongodb.com/reference/api/teams-create-one/) From ae5fe56dd489c63e4c70ea5e03fceb829f35f43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 9 Jul 2025 13:59:26 +0200 Subject: [PATCH 05/30] Added changelog --- .changelog/3483.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/3483.txt diff --git a/.changelog/3483.txt b/.changelog/3483.txt new file mode 100644 index 0000000000..580e6a21f1 --- /dev/null +++ b/.changelog/3483.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/mongodbatlas_team: Adds `users` attribute +``` \ No newline at end of file From 25b4b6349928b1531bd51e338d7b7ebc6e271f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Thu, 10 Jul 2025 11:51:39 +0200 Subject: [PATCH 06/30] Created ressource, schema and models --- .../clouduserteamassignment/main_test.go | 15 ++ .../service/clouduserteamassignment/model.go | 28 +++ .../clouduserteamassignment/model_test.go | 58 ++++++ .../clouduserteamassignment/resource.go | 180 ++++++++++++++++++ .../resource_schema.go | 100 ++++++++++ .../clouduserteamassignment/resource_test.go | 36 ++++ 6 files changed, 417 insertions(+) create mode 100644 internal/service/clouduserteamassignment/main_test.go create mode 100644 internal/service/clouduserteamassignment/model.go create mode 100644 internal/service/clouduserteamassignment/model_test.go create mode 100644 internal/service/clouduserteamassignment/resource.go create mode 100644 internal/service/clouduserteamassignment/resource_schema.go create mode 100644 internal/service/clouduserteamassignment/resource_test.go diff --git a/internal/service/clouduserteamassignment/main_test.go b/internal/service/clouduserteamassignment/main_test.go new file mode 100644 index 0000000000..e63b13fa75 --- /dev/null +++ b/internal/service/clouduserteamassignment/main_test.go @@ -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) +} diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go new file mode 100644 index 0000000000..2461a9daaa --- /dev/null +++ b/internal/service/clouduserteamassignment/model.go @@ -0,0 +1,28 @@ +package clouduserteamassignment + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + // "go.mongodb.org/atlas-sdk/v20231115003/admin" use latest version +) + +// TODO: `ctx` parameter and `diags` return value can be removed if tf schema has no complex data types (e.g., schema.ListAttribute, schema.SetAttribute) +func NewTFModel(ctx context.Context, apiResp *admin.CloudUserTeamAssignment) (*TFModel, diag.Diagnostics) { + // complexAttr, diagnostics := types.ListValueFrom(ctx, InnerObjectType, newTFComplexAttrModel(apiResp.ComplexAttr)) + // if diagnostics.HasError() { + // return nil, diagnostics + // } + return &TFModel{}, nil +} + +// TODO: If SDK defined different models for create and update separate functions will need to be defined. +// TODO: `ctx` parameter and `diags` in return value can be removed if tf schema has no complex data types (e.g., schema.ListAttribute, schema.SetAttribute) +func NewAtlasReq(ctx context.Context, plan *TFModel) (*admin.CloudUserTeamAssignment, diag.Diagnostics) { + // var tfList []complexArgumentData + // resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) + // if resp.Diagnostics.HasError() { + // return nil, diagnostics + // } + return &admin.CloudUserTeamAssignment{}, nil +} diff --git a/internal/service/clouduserteamassignment/model_test.go b/internal/service/clouduserteamassignment/model_test.go new file mode 100644 index 0000000000..24ac669a56 --- /dev/null +++ b/internal/service/clouduserteamassignment/model_test.go @@ -0,0 +1,58 @@ +package clouduserteamassignment_test + +import ( + "context" + "testing" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/clouduserteamassignment" + "github.com/stretchr/testify/assert" + // "go.mongodb.org/atlas-sdk/v20231115003/admin" use latest version +) + +type sdkToTFModelTestCase struct { + SDKResp *admin.CloudUserTeamAssignment + expectedTFModel *clouduserteamassignment.TFModel +} + +func TestCloudUserTeamAssignmentSDKToTFModel(t *testing.T) { + testCases := map[string]sdkToTFModelTestCase{ // TODO: consider adding test cases to contemplate all possible API responses + "Complete SDK response": { + SDKResp: &admin.CloudUserTeamAssignment{}, + expectedTFModel: &clouduserteamassignment.TFModel{}, + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + resultModel, diags := clouduserteamassignment.NewTFModel(context.Background(), tc.SDKResp) + if diags.HasError() { + t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary()) + } + assert.Equal(t, tc.expectedTFModel, resultModel, "created terraform model did not match expected output") + }) + } +} + +type tfToSDKModelTestCase struct { + tfModel *clouduserteamassignment.TFModel + expectedSDKReq *admin.CloudUserTeamAssignment +} + +func TestCloudUserTeamAssignmentTFModelToSDK(t *testing.T) { + testCases := map[string]tfToSDKModelTestCase{ + "Complete TF state": { + tfModel: &clouduserteamassignment.TFModel{}, + expectedSDKReq: &admin.CloudUserTeamAssignment{}, + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + apiReqResult, diags := clouduserteamassignment.NewAtlasReq(context.Background(), tc.tfModel) + if diags.HasError() { + t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary()) + } + assert.Equal(t, tc.expectedSDKReq, apiReqResult, "created sdk model did not match expected output") + }) + } +} diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go new file mode 100644 index 0000000000..40a8102d86 --- /dev/null +++ b/internal/service/clouduserteamassignment/resource.go @@ -0,0 +1,180 @@ +package clouduserteamassignment + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" +) + +const resourceName = "cloud_user_team_assignment" + +var _ resource.ResourceWithConfigure = &rs{} +var _ resource.ResourceWithImportState = &rs{} + +func Resource() resource.Resource { + return &rs{ + RSCommon: config.RSCommon{ + ResourceName: resourceName, + }, + } +} + +type rs struct { + config.RSCommon +} + +type TFUserTeamAssignmentModel struct { + OrgID types.String `tfsdk:"org_id"` + TeamID types.String `tfsdk:"team_id"` + UserID types.String `tfsdk:"user_id"` + Username types.String `tfsdk:"username"` + OrgMembershipStatus types.String `tfsdk:"org_membership_status"` + Roles TFRolesModel `tfsdk:"roles"` + TeamIDs types.Set `tfsdk:"team_ids"` + InvitationCreatedAt types.String `tfsdk:"invitation_created_at"` + InvitationExpiresAt types.String `tfsdk:"invitation_expires_at"` + InviterUsername types.String `tfsdk:"inviter_username"` + Country types.String `tfsdk:"country"` + FirstName types.String `tfsdk:"first_name"` + LastName types.String `tfsdk:"last_name"` + CreatedAt types.String `tfsdk:"created_at"` + LastAuth types.String `tfsdk:"last_auth"` + MobileNumber types.String `tfsdk:"mobile_number"` +} + +type TFRolesModel struct { + ProjectRoleAssignments TFProjectRoleAssignmentsModel `tfsdk:"project_role_assignments"` + OrgRoles types.Set `tfsdk:"org_roles"` +} + +type TFProjectRoleAssignmentsModel struct { + ProjectID types.String `tfsdk:"project_id"` + ProjectRoles types.Set `tfsdk:"project_roles"` +} + +func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + // TODO: Schema and model must be defined in resource_schema.go. Details on scaffolding this file found in contributing/development-best-practices.md under "Scaffolding Schema and Model Definitions" + resp.Schema = ResourceSchema(ctx) + conversion.UpdateSchemaDescription(&resp.Schema) +} + +func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var userTeamAssignment TFUserTeamAssignmentModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &userTeamAssignment)...) + if resp.Diagnostics.HasError() { + return + } + + cloudUserTeamAssignmentReq, diags := NewAtlasReq(ctx, &userTeamAssignment) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + // TODO: make POST request to Atlas API and handle error in response + + // connV2 := r.Client.AtlasV2 + //if err != nil { + // resp.Diagnostics.AddError("error creating resource", err.Error()) + // return + //} + + // TODO: process response into new terraform state + newCloudUserTeamAssignmentModel, diags := NewTFModel(ctx, apiResp) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) +} + +func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var cloudUserTeamAssignmentState TFModel + resp.Diagnostics.Append(req.State.Get(ctx, &cloudUserTeamAssignmentState)...) + if resp.Diagnostics.HasError() { + return + } + + // TODO: make get request to resource + + // connV2 := r.Client.AtlasV2 + //if err != nil { + // if validate.StatusNotFound(apiResp) { + // resp.State.RemoveResource(ctx) + // return + // } + // resp.Diagnostics.AddError("error fetching resource", err.Error()) + // return + //} + + // TODO: process response into new terraform state + newCloudUserTeamAssignmentModel, diags := NewTFModel(ctx, apiResp) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) +} + +func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var tfModel TFModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &tfModel)...) + if resp.Diagnostics.HasError() { + return + } + + cloudUserTeamAssignmentReq, diags := NewAtlasReq(ctx, &tfModel) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + // TODO: make PATCH request to Atlas API and handle error in response + // connV2 := r.Client.AtlasV2 + //if err != nil { + // resp.Diagnostics.AddError("error updating resource", err.Error()) + // return + //} + + // TODO: process response into new terraform state + + newCloudUserTeamAssignmentModel, diags := NewTFModel(ctx, apiResp) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) +} + +func (r *rs) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var cloudUserTeamAssignmentState *TFModel + resp.Diagnostics.Append(req.State.Get(ctx, &cloudUserTeamAssignmentState)...) + if resp.Diagnostics.HasError() { + return + } + + // TODO: make Delete request to Atlas API + + // connV2 := r.Client.AtlasV2 + // if _, _, err := connV2.Api.Delete().Execute(); err != nil { + // resp.Diagnostics.AddError("error deleting resource", err.Error()) + // return + // } +} + +func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // TODO: parse req.ID string taking into account documented format. Example: + + // projectID, other, err := splitCloudUserTeamAssignmentImportID(req.ID) + // if err != nil { + // resp.Diagnostics.AddError("error splitting import ID", err.Error()) + // return + //} + + // TODO: define attributes that are required for read operation to work correctly. Example: + + // resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectID)...) +} diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go new file mode 100644 index 0000000000..1702c824e8 --- /dev/null +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -0,0 +1,100 @@ +package clouduserteamassignment + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func ResourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Description: "Resource for managing Cloud User Team Assignments in MongoDB Atlas.", + Attributes: map[string]schema.Attribute{ + "org_id": schema.StringAttribute{ + Required: true, + }, + "team_id": schema.StringAttribute{ + Required: true, + }, + "user_id": schema.StringAttribute{ + Required: true, + }, + "username": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "org_membership_status": schema.StringAttribute{ + Computed: true, + }, + "roles": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "project_role_assignmets": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Computed: true, + }, + "project_roles": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + }, + }, + "org_roles": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + }, + }, + "team_ids": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "invitation_created_at": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "invitation_expires_at": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "inviter_username": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "country": schema.StringAttribute{ + Computed: true, + }, + "first_name": schema.StringAttribute{ + Computed: true, + }, + "last_name": schema.StringAttribute{ + Computed: true, + }, + "created_at": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "last_auth": schema.StringAttribute{ + Computed: true, + }, + "mobile_number": schema.StringAttribute{ + Computed: true, + }, + }, + } +} diff --git a/internal/service/clouduserteamassignment/resource_test.go b/internal/service/clouduserteamassignment/resource_test.go new file mode 100644 index 0000000000..c163139a1c --- /dev/null +++ b/internal/service/clouduserteamassignment/resource_test.go @@ -0,0 +1,36 @@ +package clouduserteamassignment_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" +) + +// TODO: if acceptance test will be run in an existing CI group of resources, the name should include the group in the prefix followed by the name of the resource e.i. TestAccStreamRSStreamInstance_basic +// In addition, if acceptance test contains testing of both resource and data sources, the RS/DS can be omitted. +func TestAccCloudUserTeamAssignmentRS_basic(t *testing.T) { + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.PreCheckBasic(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + // CheckDestroy: checkDestroyCloudUserTeamAssignment, + Steps: []resource.TestStep{ // TODO: verify updates and import in case of resources + // { + // Config: cloudUserTeamAssignmentConfig(), + // Check: cloudUserTeamAssignmentAttributeChecks(), + // }, + // { + // Config: cloudUserTeamAssignmentConfig(), + // Check: cloudUserTeamAssignmentAttributeChecks(), + // }, + // { + // Config: cloudUserTeamAssignmentConfig(), + // ResourceName: resourceName, + // ImportStateIdFunc: checkCloudUserTeamAssignmentImportStateIDFunc, + // ImportState: true, + // ImportStateVerify: true, + }, + }, + ) +} From 138c010ddc408cfc48e497d8bd3bee6b0bacaf2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Thu, 10 Jul 2025 16:42:47 +0200 Subject: [PATCH 07/30] WIP - Implementing Create method --- .../service/clouduserteamassignment/model.go | 84 ++++++++++++++++--- .../clouduserteamassignment/resource.go | 27 +++--- .../resource_schema.go | 18 ++-- 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index 2461a9daaa..5ccbd93f44 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -2,27 +2,87 @@ package clouduserteamassignment import ( "context" + "errors" "github.com/hashicorp/terraform-plugin-framework/diag" - // "go.mongodb.org/atlas-sdk/v20231115003/admin" use latest version + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "go.mongodb.org/atlas-sdk/v20250312005/admin" ) // TODO: `ctx` parameter and `diags` return value can be removed if tf schema has no complex data types (e.g., schema.ListAttribute, schema.SetAttribute) -func NewTFModel(ctx context.Context, apiResp *admin.CloudUserTeamAssignment) (*TFModel, diag.Diagnostics) { +func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, apiResp *admin.OrgUserResponse) (*TFUserTeamAssignmentModel, diag.Diagnostics) { // complexAttr, diagnostics := types.ListValueFrom(ctx, InnerObjectType, newTFComplexAttrModel(apiResp.ComplexAttr)) // if diagnostics.HasError() { // return nil, diagnostics // } - return &TFModel{}, nil + var diags diag.Diagnostics + if apiResp == nil { + diags.AddError("Invalid data", "The API response for the user team assignment is nil and cannot be processed.") + return nil, diags + } + rolesModel, diags := NewTFRolesModel(ctx, &apiResp.Roles) + if diags.HasError() { + return nil, diags + } + userTeamAssignment := TFUserTeamAssignmentModel{ + OrgID: types.StringValue(orgID), + TeamID: types.StringValue(teamID), + UserID: types.StringValue(apiResp.Id), + Username: types.StringValue(apiResp.Username), + OrgMembershipStatus: types.StringValue(apiResp.OrgMembershipStatus), + Roles: rolesModel, + 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 } -// TODO: If SDK defined different models for create and update separate functions will need to be defined. -// TODO: `ctx` parameter and `diags` in return value can be removed if tf schema has no complex data types (e.g., schema.ListAttribute, schema.SetAttribute) -func NewAtlasReq(ctx context.Context, plan *TFModel) (*admin.CloudUserTeamAssignment, diag.Diagnostics) { - // var tfList []complexArgumentData - // resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) - // if resp.Diagnostics.HasError() { - // return nil, diagnostics - // } - return &admin.CloudUserTeamAssignment{}, nil +func NewTFRolesModel(ctx context.Context, apiResp *admin.OrgUserRolesResponse) (*TFRolesModel, diag.Diagnostics) { + var diags diag.Diagnostics + + projectRoleAssignments := make([]*TFProjectRoleAssignmentsModel, len(*apiResp.GroupRoleAssignments)) + for i, roleAssignment := range *apiResp.GroupRoleAssignments { + projectRoleAssignments[i] = &TFProjectRoleAssignmentsModel{ + ProjectID: types.StringValue(roleAssignment.GetGroupId()), + ProjectRoles: types.SetNull(types.StringType), + } + if roleAssignment.GetGroupRoles() != nil { + projectRoles, diags := types.SetValueFrom(ctx, types.StringType, roleAssignment.GetGroupRoles()) + if diags.HasError() { + return nil, diags + } + projectRoleAssignments[i].ProjectRoles = projectRoles + } + } + + orgRoles, _ := types.SetValueFrom(ctx, types.StringType, *apiResp.OrgRoles) + + return &TFRolesModel{ + ProjectRoleAssignments: projectRoleAssignments, + OrgRoles: orgRoles, + }, diags +} + +func NewCloudUserTeamAssignmentReq(ctx context.Context, plan *TFUserTeamAssignmentModel) (*admin.AddOrRemoveUserFromTeam, diag.Diagnostics) { + addOrRemoveUserFromTeam := admin.AddOrRemoveUserFromTeam{ + Id: plan.UserID.ValueString(), + } + return &addOrRemoveUserFromTeam, nil } diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index 40a8102d86..bb026b6474 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -46,8 +46,8 @@ type TFUserTeamAssignmentModel struct { } type TFRolesModel struct { - ProjectRoleAssignments TFProjectRoleAssignmentsModel `tfsdk:"project_role_assignments"` - OrgRoles types.Set `tfsdk:"org_roles"` + ProjectRoleAssignments []*TFProjectRoleAssignmentsModel `tfsdk:"project_role_assignments"` + OrgRoles types.Set `tfsdk:"org_roles"` } type TFProjectRoleAssignmentsModel struct { @@ -67,13 +67,6 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou if resp.Diagnostics.HasError() { return } - - cloudUserTeamAssignmentReq, diags := NewAtlasReq(ctx, &userTeamAssignment) - if diags.HasError() { - resp.Diagnostics.Append(diags...) - return - } - // TODO: make POST request to Atlas API and handle error in response // connV2 := r.Client.AtlasV2 @@ -81,9 +74,23 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou // resp.Diagnostics.AddError("error creating resource", err.Error()) // return //} + connV2 := r.Client.AtlasV2 + orgID := userTeamAssignment.OrgID.ValueString() + teamID := userTeamAssignment.TeamID.ValueString() + cloudUserTeamAssignmentReq, diags := NewCloudUserTeamAssignmentReq(ctx, &userTeamAssignment) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + apiResp, _, err := connV2.MongoDBCloudUsersApi.AddUserToTeam(ctx, orgID, teamID, cloudUserTeamAssignmentReq).Execute() + if err != nil { + resp.Diagnostics.AddError("error creating resource", err.Error()) + return + } // TODO: process response into new terraform state - newCloudUserTeamAssignmentModel, diags := NewTFModel(ctx, apiResp) + newCloudUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, apiResp) if diags.HasError() { resp.Diagnostics.Append(diags...) return diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index 1702c824e8..dbe075945c 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -34,15 +34,17 @@ func ResourceSchema(ctx context.Context) schema.Schema { "roles": schema.SingleNestedAttribute{ Computed: true, Attributes: map[string]schema.Attribute{ - "project_role_assignmets": schema.SingleNestedAttribute{ + "project_role_assignmets": schema.SetNestedAttribute{ Computed: true, - Attributes: map[string]schema.Attribute{ - "project_id": schema.StringAttribute{ - Computed: true, - }, - "project_roles": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Computed: true, + }, + "project_roles": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, }, }, }, From 776b1078ffbe5315f918957f8b7cf39302e45dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 14 Jul 2025 12:11:54 +0200 Subject: [PATCH 08/30] WIP - Added markdown description for attributes in schema and fixed model functions --- .../service/clouduserteamassignment/model.go | 89 ++++++++++++------- .../clouduserteamassignment/resource.go | 29 ------ .../resource_schema.go | 70 ++++++++++++++- 3 files changed, 128 insertions(+), 60 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index 5ccbd93f44..8f1de870a0 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -2,9 +2,9 @@ package clouduserteamassignment import ( "context" - "errors" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "go.mongodb.org/atlas-sdk/v20250312005/admin" @@ -16,22 +16,25 @@ func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, api // if diagnostics.HasError() { // return nil, diagnostics // } - var diags diag.Diagnostics + diags := diag.Diagnostics{} + var rolesObj types.Object + var rolesDiags diag.Diagnostics + if apiResp == nil { diags.AddError("Invalid data", "The API response for the user team assignment is nil and cannot be processed.") return nil, diags } - rolesModel, diags := NewTFRolesModel(ctx, &apiResp.Roles) - if diags.HasError() { - return nil, diags - } + + rolesObj, rolesDiags = NewTFRolesModel(ctx, &apiResp.Roles) + diags.Append(rolesDiags...) + userTeamAssignment := TFUserTeamAssignmentModel{ OrgID: types.StringValue(orgID), TeamID: types.StringValue(teamID), - UserID: types.StringValue(apiResp.Id), - Username: types.StringValue(apiResp.Username), - OrgMembershipStatus: types.StringValue(apiResp.OrgMembershipStatus), - Roles: rolesModel, + 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), @@ -54,30 +57,56 @@ func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, api return &userTeamAssignment, nil } -func NewTFRolesModel(ctx context.Context, apiResp *admin.OrgUserRolesResponse) (*TFRolesModel, diag.Diagnostics) { - var diags diag.Diagnostics +func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (types.Object, diag.Diagnostics) { + diags := diag.Diagnostics{} - projectRoleAssignments := make([]*TFProjectRoleAssignmentsModel, len(*apiResp.GroupRoleAssignments)) - for i, roleAssignment := range *apiResp.GroupRoleAssignments { - projectRoleAssignments[i] = &TFProjectRoleAssignmentsModel{ - ProjectID: types.StringValue(roleAssignment.GetGroupId()), - ProjectRoles: types.SetNull(types.StringType), - } - if roleAssignment.GetGroupRoles() != nil { - projectRoles, diags := types.SetValueFrom(ctx, types.StringType, roleAssignment.GetGroupRoles()) - if diags.HasError() { - return nil, diags - } - projectRoleAssignments[i].ProjectRoles = projectRoles - } + 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, + }, + ) - orgRoles, _ := types.SetValueFrom(ctx, types.StringType, *apiResp.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, + }) + } - return &TFRolesModel{ - ProjectRoleAssignments: projectRoleAssignments, - OrgRoles: orgRoles, - }, diags + praList, _ := types.ListValueFrom(ctx, ProjectRoleAssignmentsAttrType.ElemType.(types.ObjectType), projectRoleAssignments) + return praList } func NewCloudUserTeamAssignmentReq(ctx context.Context, plan *TFUserTeamAssignmentModel) (*admin.AddOrRemoveUserFromTeam, diag.Diagnostics) { diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index bb026b6474..0f18b6dbd8 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -26,35 +26,6 @@ type rs struct { config.RSCommon } -type TFUserTeamAssignmentModel struct { - OrgID types.String `tfsdk:"org_id"` - TeamID types.String `tfsdk:"team_id"` - UserID types.String `tfsdk:"user_id"` - Username types.String `tfsdk:"username"` - OrgMembershipStatus types.String `tfsdk:"org_membership_status"` - Roles TFRolesModel `tfsdk:"roles"` - TeamIDs types.Set `tfsdk:"team_ids"` - InvitationCreatedAt types.String `tfsdk:"invitation_created_at"` - InvitationExpiresAt types.String `tfsdk:"invitation_expires_at"` - InviterUsername types.String `tfsdk:"inviter_username"` - Country types.String `tfsdk:"country"` - FirstName types.String `tfsdk:"first_name"` - LastName types.String `tfsdk:"last_name"` - CreatedAt types.String `tfsdk:"created_at"` - LastAuth types.String `tfsdk:"last_auth"` - MobileNumber types.String `tfsdk:"mobile_number"` -} - -type TFRolesModel struct { - ProjectRoleAssignments []*TFProjectRoleAssignmentsModel `tfsdk:"project_role_assignments"` - OrgRoles types.Set `tfsdk:"org_roles"` -} - -type TFProjectRoleAssignmentsModel struct { - ProjectID types.String `tfsdk:"project_id"` - ProjectRoles types.Set `tfsdk:"project_roles"` -} - func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { // TODO: Schema and model must be defined in resource_schema.go. Details on scaffolding this file found in contributing/development-best-practices.md under "Scaffolding Schema and Model Definitions" resp.Schema = ResourceSchema(ctx) diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index dbe075945c..bcce89a346 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -7,43 +7,52 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/attr" ) func ResourceSchema(ctx context.Context) schema.Schema { return schema.Schema{ - Description: "Resource for managing Cloud User Team Assignments in MongoDB Atlas.", Attributes: map[string]schema.Attribute{ "org_id": schema.StringAttribute{ Required: true, + MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the organization that contains your projects. Use the [/orgs](#tag/Organizations/operation/listOrganizations) endpoint to retrieve all organizations to which the authenticated user has access.", }, "team_id": schema.StringAttribute{ Required: true, + MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the team to which you want to assign the MongoDB Cloud user. Use the [/teams](#tag/Teams/operation/listTeams) endpoint to retrieve all teams to which the authenticated user has access.", }, "user_id": schema.StringAttribute{ Required: true, + MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the MongoDB Cloud user.", }, "username": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Email address that represents the username of the MongoDB Cloud user.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "org_membership_status": schema.StringAttribute{ Computed: true, + MarkdownDescription: "String enum that indicates whether the MongoDB Cloud user has a pending invitation to join the organization or they are already active in the organization.", }, "roles": schema.SingleNestedAttribute{ Computed: true, + MarkdownDescription: "Organization and project level roles to assign the MongoDB Cloud user within one organization.", Attributes: map[string]schema.Attribute{ "project_role_assignmets": schema.SetNestedAttribute{ Computed: true, + MarkdownDescription: "List of project level role assignments to assign the MongoDB Cloud user.", NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "project_id": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the project to which these roles belong.", }, "project_roles": schema.SetAttribute{ ElementType: types.StringType, Computed: true, + MarkdownDescription: "One or more project-level roles assigned to the MongoDB Cloud user.", }, }, }, @@ -51,52 +60,111 @@ func ResourceSchema(ctx context.Context) schema.Schema { "org_roles": schema.SetAttribute{ ElementType: types.StringType, Computed: true, + MarkdownDescription: "One or more organization level roles to assign the MongoDB Cloud user.", }, }, }, "team_ids": schema.SetAttribute{ ElementType: types.StringType, Computed: true, + MarkdownDescription: "List of unique 24-hexadecimal digit strings that identifies the teams to which this MongoDB Cloud user belongs.", }, "invitation_created_at": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Date and time when MongoDB Cloud sent the invitation. MongoDB Cloud represents this timestamp in ISO 8601 format in UTC.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "invitation_expires_at": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Date and time when the invitation from MongoDB Cloud expires. MongoDB Cloud represents this timestamp in ISO 8601 format in UTC.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "inviter_username": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Username of the MongoDB Cloud user who sent the invitation to join the organization.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "country": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Two-character alphabetical string that identifies the MongoDB Cloud user's geographic location. This parameter uses the ISO 3166-1a2 code format.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "first_name": schema.StringAttribute{ Computed: true, + MarkdownDescription: "First or given name that belongs to the MongoDB Cloud user.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "last_name": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Last name, family name, or surname that belongs to the MongoDB Cloud user.", }, "created_at": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Date and time when MongoDB Cloud created the current account. This value is in the ISO 8601 timestamp format in UTC.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "last_auth": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Date and time when the current account last authenticated. This value is in the ISO 8601 timestamp format in UTC.", }, "mobile_number": schema.StringAttribute{ Computed: true, + MarkdownDescription: "Mobile phone number that belongs to the MongoDB Cloud user.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } } + +type TFUserTeamAssignmentModel struct { + OrgID types.String `tfsdk:"org_id"` + TeamID types.String `tfsdk:"team_id"` + UserID types.String `tfsdk:"user_id"` + Username types.String `tfsdk:"username"` + OrgMembershipStatus types.String `tfsdk:"org_membership_status"` + Roles types.Object `tfsdk:"roles"` + TeamIDs types.Set `tfsdk:"team_ids"` + InvitationCreatedAt types.String `tfsdk:"invitation_created_at"` + InvitationExpiresAt types.String `tfsdk:"invitation_expires_at"` + InviterUsername types.String `tfsdk:"inviter_username"` + Country types.String `tfsdk:"country"` + FirstName types.String `tfsdk:"first_name"` + LastName types.String `tfsdk:"last_name"` + CreatedAt types.String `tfsdk:"created_at"` + LastAuth types.String `tfsdk:"last_auth"` + MobileNumber types.String `tfsdk:"mobile_number"` +} + +type TFRolesModel struct { + ProjectRoleAssignments types.List `tfsdk:"project_role_assignments"` + OrgRoles types.Set `tfsdk:"org_roles"` +} + +type TFProjectRoleAssignmentsModel struct { + ProjectID types.String `tfsdk:"project_id"` + ProjectRoles types.Set `tfsdk:"project_roles"` +} + +var ProjectRoleAssignmentsAttrType = types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "project_id": types.StringType, + "project_roles": types.SetType{ElemType: types.StringType}, +}}} + +var RolesObjectAttrTypes = map[string]attr.Type{ + "org_roles": types.SetType{ElemType: types.StringType}, + "project_role_assignments": ProjectRoleAssignmentsAttrType, +} From 4c518eff61d56ce2d81612b6f3cfa85e1660edb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Tue, 15 Jul 2025 08:54:36 +0200 Subject: [PATCH 09/30] Fixed variables names --- .../service/clouduserteamassignment/model.go | 23 +++----- .../resource_schema.go | 56 +++++++++---------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index 8f1de870a0..e52be70d5a 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -3,19 +3,14 @@ package clouduserteamassignment import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "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" ) -// TODO: `ctx` parameter and `diags` return value can be removed if tf schema has no complex data types (e.g., schema.ListAttribute, schema.SetAttribute) func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, apiResp *admin.OrgUserResponse) (*TFUserTeamAssignmentModel, diag.Diagnostics) { - // complexAttr, diagnostics := types.ListValueFrom(ctx, InnerObjectType, newTFComplexAttrModel(apiResp.ComplexAttr)) - // if diagnostics.HasError() { - // return nil, diagnostics - // } diags := diag.Diagnostics{} var rolesObj types.Object var rolesDiags diag.Diagnostics @@ -24,14 +19,14 @@ func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, api diags.AddError("Invalid data", "The API response for the user team assignment is nil and cannot be processed.") return nil, diags } - + rolesObj, rolesDiags = NewTFRolesModel(ctx, &apiResp.Roles) diags.Append(rolesDiags...) - + userTeamAssignment := TFUserTeamAssignmentModel{ - OrgID: types.StringValue(orgID), - TeamID: types.StringValue(teamID), - UserID: types.StringValue(apiResp.GetId()), + OrgId: types.StringValue(orgID), + TeamId: types.StringValue(teamID), + UserId: types.StringValue(apiResp.GetId()), Username: types.StringValue(apiResp.GetUsername()), OrgMembershipStatus: types.StringValue(apiResp.GetOrgMembershipStatus()), Roles: rolesObj, @@ -70,7 +65,7 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty } else { orgRoles, _ = types.SetValueFrom(ctx, types.StringType, *roles.OrgRoles) } - + projectRoleAssignmentsList := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) rolesObj, _ := types.ObjectValue( @@ -109,9 +104,9 @@ func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]ad return praList } -func NewCloudUserTeamAssignmentReq(ctx context.Context, plan *TFUserTeamAssignmentModel) (*admin.AddOrRemoveUserFromTeam, diag.Diagnostics) { +func NewUserTeamAssignmentReq(ctx context.Context, plan *TFUserTeamAssignmentModel) (*admin.AddOrRemoveUserFromTeam, diag.Diagnostics) { addOrRemoveUserFromTeam := admin.AddOrRemoveUserFromTeam{ - Id: plan.UserID.ValueString(), + Id: plan.UserId.ValueString(), } return &addOrRemoveUserFromTeam, nil } diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index bcce89a346..5dfa6516d9 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -3,124 +3,124 @@ package clouduserteamassignment import ( "context" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/attr" ) func ResourceSchema(ctx context.Context) schema.Schema { return schema.Schema{ Attributes: map[string]schema.Attribute{ "org_id": schema.StringAttribute{ - Required: true, + Required: true, MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the organization that contains your projects. Use the [/orgs](#tag/Organizations/operation/listOrganizations) endpoint to retrieve all organizations to which the authenticated user has access.", }, "team_id": schema.StringAttribute{ - Required: true, + Required: true, MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the team to which you want to assign the MongoDB Cloud user. Use the [/teams](#tag/Teams/operation/listTeams) endpoint to retrieve all teams to which the authenticated user has access.", }, "user_id": schema.StringAttribute{ - Required: true, + Required: true, MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the MongoDB Cloud user.", }, "username": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Email address that represents the username of the MongoDB Cloud user.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "org_membership_status": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "String enum that indicates whether the MongoDB Cloud user has a pending invitation to join the organization or they are already active in the organization.", }, "roles": schema.SingleNestedAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Organization and project level roles to assign the MongoDB Cloud user within one organization.", Attributes: map[string]schema.Attribute{ "project_role_assignmets": schema.SetNestedAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "List of project level role assignments to assign the MongoDB Cloud user.", NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "project_id": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the project to which these roles belong.", }, "project_roles": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, + ElementType: types.StringType, + Computed: true, MarkdownDescription: "One or more project-level roles assigned to the MongoDB Cloud user.", }, }, }, }, "org_roles": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, + ElementType: types.StringType, + Computed: true, MarkdownDescription: "One or more organization level roles to assign the MongoDB Cloud user.", }, }, }, "team_ids": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, + ElementType: types.StringType, + Computed: true, MarkdownDescription: "List of unique 24-hexadecimal digit strings that identifies the teams to which this MongoDB Cloud user belongs.", }, "invitation_created_at": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Date and time when MongoDB Cloud sent the invitation. MongoDB Cloud represents this timestamp in ISO 8601 format in UTC.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "invitation_expires_at": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Date and time when the invitation from MongoDB Cloud expires. MongoDB Cloud represents this timestamp in ISO 8601 format in UTC.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "inviter_username": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Username of the MongoDB Cloud user who sent the invitation to join the organization.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "country": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Two-character alphabetical string that identifies the MongoDB Cloud user's geographic location. This parameter uses the ISO 3166-1a2 code format.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "first_name": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "First or given name that belongs to the MongoDB Cloud user.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "last_name": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Last name, family name, or surname that belongs to the MongoDB Cloud user.", }, "created_at": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Date and time when MongoDB Cloud created the current account. This value is in the ISO 8601 timestamp format in UTC.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "last_auth": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Date and time when the current account last authenticated. This value is in the ISO 8601 timestamp format in UTC.", }, "mobile_number": schema.StringAttribute{ - Computed: true, + Computed: true, MarkdownDescription: "Mobile phone number that belongs to the MongoDB Cloud user.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -131,9 +131,9 @@ func ResourceSchema(ctx context.Context) schema.Schema { } type TFUserTeamAssignmentModel struct { - OrgID types.String `tfsdk:"org_id"` - TeamID types.String `tfsdk:"team_id"` - UserID types.String `tfsdk:"user_id"` + OrgId types.String `tfsdk:"org_id"` + TeamId types.String `tfsdk:"team_id"` + UserId types.String `tfsdk:"user_id"` Username types.String `tfsdk:"username"` OrgMembershipStatus types.String `tfsdk:"org_membership_status"` Roles types.Object `tfsdk:"roles"` @@ -151,7 +151,7 @@ type TFUserTeamAssignmentModel struct { type TFRolesModel struct { ProjectRoleAssignments types.List `tfsdk:"project_role_assignments"` - OrgRoles types.Set `tfsdk:"org_roles"` + OrgRoles types.Set `tfsdk:"org_roles"` } type TFProjectRoleAssignmentsModel struct { From 08658180775b8b853478cee581aa050adcdb6187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Tue, 15 Jul 2025 08:56:44 +0200 Subject: [PATCH 10/30] WIP - Implement Read, Update, Delete, Import --- .../clouduserteamassignment/resource.go | 155 ++++++++++-------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index 0f18b6dbd8..f709e48da6 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -2,11 +2,16 @@ package clouduserteamassignment import ( "context" + "fmt" + "net/http" + "regexp" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" + "go.mongodb.org/atlas-sdk/v20250312005/admin" ) const resourceName = "cloud_user_team_assignment" @@ -27,28 +32,20 @@ type rs struct { } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - // TODO: Schema and model must be defined in resource_schema.go. Details on scaffolding this file found in contributing/development-best-practices.md under "Scaffolding Schema and Model Definitions" resp.Schema = ResourceSchema(ctx) conversion.UpdateSchemaDescription(&resp.Schema) } func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var userTeamAssignment TFUserTeamAssignmentModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &userTeamAssignment)...) + var plan TFUserTeamAssignmentModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } - // TODO: make POST request to Atlas API and handle error in response - - // connV2 := r.Client.AtlasV2 - //if err != nil { - // resp.Diagnostics.AddError("error creating resource", err.Error()) - // return - //} connV2 := r.Client.AtlasV2 - orgID := userTeamAssignment.OrgID.ValueString() - teamID := userTeamAssignment.TeamID.ValueString() - cloudUserTeamAssignmentReq, diags := NewCloudUserTeamAssignmentReq(ctx, &userTeamAssignment) + orgID := plan.OrgId.ValueString() + teamID := plan.TeamId.ValueString() + cloudUserTeamAssignmentReq, diags := NewUserTeamAssignmentReq(ctx, &plan) if diags.HasError() { resp.Diagnostics.Append(diags...) return @@ -56,70 +53,67 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou apiResp, _, err := connV2.MongoDBCloudUsersApi.AddUserToTeam(ctx, orgID, teamID, cloudUserTeamAssignmentReq).Execute() if err != nil { - resp.Diagnostics.AddError("error creating resource", err.Error()) + resp.Diagnostics.AddError(fmt.Sprintf("error assigning user to TeamID(%s):", teamID), err.Error()) return } - // TODO: process response into new terraform state - newCloudUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, apiResp) + newUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, orgID, teamID, apiResp) if diags.HasError() { resp.Diagnostics.Append(diags...) return } - resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) + resp.Diagnostics.Append(resp.State.Set(ctx, newUserTeamAssignmentModel)...) } func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var cloudUserTeamAssignmentState TFModel - resp.Diagnostics.Append(req.State.Get(ctx, &cloudUserTeamAssignmentState)...) + var state TFUserTeamAssignmentModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } + connV2 := r.Client.AtlasV2 + orgID := state.OrgId.ValueString() + teamID := state.TeamId.ValueString() - // TODO: make get request to resource - - // connV2 := r.Client.AtlasV2 - //if err != nil { - // if validate.StatusNotFound(apiResp) { - // resp.State.RemoveResource(ctx) - // return - // } - // resp.Diagnostics.AddError("error fetching resource", err.Error()) - // return - //} + var userListResp *admin.PaginatedOrgUser + var httpResp *http.Response + var err error - // TODO: process response into new terraform state - newCloudUserTeamAssignmentModel, diags := NewTFModel(ctx, apiResp) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + userListResp, httpResp, err = connV2.MongoDBCloudUsersApi.ListTeamUsers(ctx, orgID, teamID).Execute() + if validate.StatusNotFound(httpResp) { + resp.State.RemoveResource(ctx) return } - resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) -} -func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var tfModel TFModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &tfModel)...) - if resp.Diagnostics.HasError() { + var userResp *admin.OrgUserResponse + if !state.UserId.IsNull() && state.UserId.ValueString() != "" { + userID := state.UserId.ValueString() + for _, user := range userListResp.GetResults() { + if user.GetId() == userID { + userResp = &user + break + } + } + } else if !state.Username.IsNull() && state.Username.ValueString() != "" { // required for import + username := state.Username.ValueString() + for _, user := range userListResp.GetResults() { + if user.GetUsername() == username { + userResp = &user + break + } + } + } + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("error fetching user(%s) from TeamID(%s):", userResp.Username, teamID), err.Error()) return } - cloudUserTeamAssignmentReq, diags := NewAtlasReq(ctx, &tfModel) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + if userResp == nil { + resp.State.RemoveResource(ctx) return } - // TODO: make PATCH request to Atlas API and handle error in response - // connV2 := r.Client.AtlasV2 - //if err != nil { - // resp.Diagnostics.AddError("error updating resource", err.Error()) - // return - //} - - // TODO: process response into new terraform state - - newCloudUserTeamAssignmentModel, diags := NewTFModel(ctx, apiResp) + newCloudUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, orgID, teamID, userResp) if diags.HasError() { resp.Diagnostics.Append(diags...) return @@ -127,32 +121,53 @@ func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resou resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) } +func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + func (r *rs) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var cloudUserTeamAssignmentState *TFModel - resp.Diagnostics.Append(req.State.Get(ctx, &cloudUserTeamAssignmentState)...) + var state *TFUserTeamAssignmentModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - // TODO: make Delete request to Atlas API + connV2 := r.Client.AtlasV2 + orgID := state.OrgId.ValueString() + userID := state.UserId.ValueString() + teamID := state.TeamId.ValueString() + + userInfo := &admin.AddOrRemoveUserFromTeam{ + Id: userID, + } - // connV2 := r.Client.AtlasV2 - // if _, _, err := connV2.Api.Delete().Execute(); err != nil { - // resp.Diagnostics.AddError("error deleting resource", err.Error()) - // return - // } + _, httpResp, err := connV2.MongoDBCloudUsersApi.RemoveUserFromTeam(ctx, orgID, teamID, userInfo).Execute() + if err != nil { + if validate.StatusNotFound(httpResp) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError(fmt.Sprintf("error deleting user(%s) from TeamID(%s):", userID, teamID), err.Error()) + return + } } func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - // TODO: parse req.ID string taking into account documented format. Example: + importID := req.ID + ok, part1, part2, part3 := conversion.ImportSplit3(req.ID) + if !ok { + resp.Diagnostics.AddError("invalid import ID format", "expected 'org_id/team_id/user_id' or 'org_id/team_id/username', got: "+importID) + return + } + orgID, teamID, user := part1, part2, part3 - // projectID, other, err := splitCloudUserTeamAssignmentImportID(req.ID) - // if err != nil { - // resp.Diagnostics.AddError("error splitting import ID", err.Error()) - // return - //} + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("org_id"), orgID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("team_id"), teamID)...) - // TODO: define attributes that are required for read operation to work correctly. Example: + emailRegex := regexp.MustCompile(`^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$`) - // resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectID)...) + if emailRegex.MatchString(user) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("username"), user)...) + } else { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), user)...) + } } From 6329b76da950d7ec59b8f03c774ae14f9e6295ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:32:25 +0200 Subject: [PATCH 11/30] Fixed model attributes names. --- internal/service/clouduserteamassignment/model.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index e52be70d5a..3ca228adc1 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -10,13 +10,12 @@ import ( "go.mongodb.org/atlas-sdk/v20250312005/admin" ) -func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, apiResp *admin.OrgUserResponse) (*TFUserTeamAssignmentModel, diag.Diagnostics) { +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 { - diags.AddError("Invalid data", "The API response for the user team assignment is nil and cannot be processed.") return nil, diags } @@ -24,8 +23,6 @@ func NewTFUserTeamAssignmentModel(ctx context.Context, orgID, teamID string, api diags.Append(rolesDiags...) userTeamAssignment := TFUserTeamAssignmentModel{ - OrgId: types.StringValue(orgID), - TeamId: types.StringValue(teamID), UserId: types.StringValue(apiResp.GetId()), Username: types.StringValue(apiResp.GetUsername()), OrgMembershipStatus: types.StringValue(apiResp.GetOrgMembershipStatus()), @@ -95,7 +92,7 @@ func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]ad projectRoles, _ = types.SetValueFrom(ctx, types.StringType, pra.GroupRoles) } projectRoleAssignments = append(projectRoleAssignments, TFProjectRoleAssignmentsModel{ - ProjectID: projectID, + ProjectId: projectID, ProjectRoles: projectRoles, }) } From 13ad925554b7d291c851de18ff5de53e4c84484d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:33:21 +0200 Subject: [PATCH 12/30] Minor change --- internal/service/clouduserteamassignment/resource_schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index 5dfa6516d9..6fa22585e1 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -40,7 +40,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { Computed: true, MarkdownDescription: "Organization and project level roles to assign the MongoDB Cloud user within one organization.", Attributes: map[string]schema.Attribute{ - "project_role_assignmets": schema.SetNestedAttribute{ + "project_role_assignments": schema.ListNestedAttribute{ Computed: true, MarkdownDescription: "List of project level role assignments to assign the MongoDB Cloud user.", NestedObject: schema.NestedAttributeObject{ @@ -155,7 +155,7 @@ type TFRolesModel struct { } type TFProjectRoleAssignmentsModel struct { - ProjectID types.String `tfsdk:"project_id"` + ProjectId types.String `tfsdk:"project_id"` ProjectRoles types.Set `tfsdk:"project_roles"` } From 78464a5478a2aeb4d20699d569f8130c61c8e90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:39:14 +0200 Subject: [PATCH 13/30] Added tests --- .../clouduserteamassignment/model_test.go | 180 +++++++++++++++--- .../resource_migration_test.go | 12 ++ .../clouduserteamassignment/resource_test.go | 128 +++++++++++-- 3 files changed, 273 insertions(+), 47 deletions(-) create mode 100644 internal/service/clouduserteamassignment/resource_migration_test.go diff --git a/internal/service/clouduserteamassignment/model_test.go b/internal/service/clouduserteamassignment/model_test.go index 24ac669a56..c3ff0633c6 100644 --- a/internal/service/clouduserteamassignment/model_test.go +++ b/internal/service/clouduserteamassignment/model_test.go @@ -3,56 +3,184 @@ 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/v20231115003/admin" use latest version + "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.CloudUserTeamAssignment - expectedTFModel *clouduserteamassignment.TFModel + SDKResp *admin.OrgUserResponse + expectedTFModel *clouduserteamassignment.TFUserTeamAssignmentModel } -func TestCloudUserTeamAssignmentSDKToTFModel(t *testing.T) { - testCases := map[string]sdkToTFModelTestCase{ // TODO: consider adding test cases to contemplate all possible API responses +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: &admin.CloudUserTeamAssignment{}, - expectedTFModel: &clouduserteamassignment.TFModel{}, + SDKResp: fullResp, + expectedTFModel: expectedFullModel, }, } for testName, tc := range testCases { t.Run(testName, func(t *testing.T) { - resultModel, diags := clouduserteamassignment.NewTFModel(context.Background(), tc.SDKResp) - if diags.HasError() { - t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary()) - } + 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") }) } } -type tfToSDKModelTestCase struct { - tfModel *clouduserteamassignment.TFModel - expectedSDKReq *admin.CloudUserTeamAssignment +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 TestCloudUserTeamAssignmentTFModelToSDK(t *testing.T) { - testCases := map[string]tfToSDKModelTestCase{ - "Complete TF state": { - tfModel: &clouduserteamassignment.TFModel{}, - expectedSDKReq: &admin.CloudUserTeamAssignment{}, +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 testName, tc := range testCases { - t.Run(testName, func(t *testing.T) { - apiReqResult, diags := clouduserteamassignment.NewAtlasReq(context.Background(), tc.tfModel) - if diags.HasError() { - t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary()) - } - assert.Equal(t, tc.expectedSDKReq, apiReqResult, "created sdk model did not match expected output") + 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) }) } } diff --git a/internal/service/clouduserteamassignment/resource_migration_test.go b/internal/service/clouduserteamassignment/resource_migration_test.go new file mode 100644 index 0000000000..9911597312 --- /dev/null +++ b/internal/service/clouduserteamassignment/resource_migration_test.go @@ -0,0 +1,12 @@ +package clouduserteamassignment_test + +import ( + "testing" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/mig" +) + +func TestMigCloudUserTeamAssignmentRS_basic(t *testing.T) { + mig.SkipIfVersionBelow(t, "2.0.0") // when resource 1st released + mig.CreateAndRunTest(t, basicTestCase(t)) +} diff --git a/internal/service/clouduserteamassignment/resource_test.go b/internal/service/clouduserteamassignment/resource_test.go index c163139a1c..e62cc4f0ee 100644 --- a/internal/service/clouduserteamassignment/resource_test.go +++ b/internal/service/clouduserteamassignment/resource_test.go @@ -1,36 +1,122 @@ package clouduserteamassignment_test import ( + "context" + "fmt" + "os" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc" ) -// TODO: if acceptance test will be run in an existing CI group of resources, the name should include the group in the prefix followed by the name of the resource e.i. TestAccStreamRSStreamInstance_basic -// In addition, if acceptance test contains testing of both resource and data sources, the RS/DS can be omitted. +var resourceName = "mongodbatlas_cloud_user_team_assignment.test" + func TestAccCloudUserTeamAssignmentRS_basic(t *testing.T) { + resource.ParallelTest(t, *basicTestCase(t)) +} + +func basicTestCase(t *testing.T) *resource.TestCase { + t.Helper() + + orgID := os.Getenv("MONGODB_ATLAS_ORG_ID") + userID := os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID") + teamName := acc.RandomName() - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acc.PreCheckBasic(t) }, + return &resource.TestCase{ + PreCheck: func() { acc.PreCheckBasic(t); acc.PreCheckBasicOwnerID(t) }, ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, - // CheckDestroy: checkDestroyCloudUserTeamAssignment, - Steps: []resource.TestStep{ // TODO: verify updates and import in case of resources - // { - // Config: cloudUserTeamAssignmentConfig(), - // Check: cloudUserTeamAssignmentAttributeChecks(), - // }, - // { - // Config: cloudUserTeamAssignmentConfig(), - // Check: cloudUserTeamAssignmentAttributeChecks(), - // }, - // { - // Config: cloudUserTeamAssignmentConfig(), - // ResourceName: resourceName, - // ImportStateIdFunc: checkCloudUserTeamAssignmentImportStateIDFunc, - // ImportState: true, - // ImportStateVerify: true, + CheckDestroy: checkDestroy, + Steps: []resource.TestStep{ + { + Config: cloudUserTeamAssignmentConfig(orgID, userID, teamName), + Check: cloudUserTeamAssignmentAttributeChecks(orgID, userID), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "user_id", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + attrs := s.RootModule().Resources[resourceName].Primary.Attributes + orgID := attrs["org_id"] + teamID := attrs["team_id"] + userID := attrs["user_id"] + return orgID + "/" + teamID + "/" + userID, nil + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "user_id", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + attrs := s.RootModule().Resources[resourceName].Primary.Attributes + orgID := attrs["org_id"] + teamID := attrs["team_id"] + username := attrs["username"] + return orgID + "/" + teamID + "/" + username, nil + }, + }, }, - }, + } +} + +func cloudUserTeamAssignmentConfig(orgID, userID, teamName string) string { + return fmt.Sprintf(` + resource "mongodbatlas_team" "test" { + org_id = %[1]q + name = %[3]q + } + resource "mongodbatlas_cloud_user_team_assignment" "test" { + org_id = %[1]q + team_id = mongodbatlas_team.test.team_id + user_id = %[2]q + } + `, + orgID, userID, teamName) +} + +func cloudUserTeamAssignmentAttributeChecks(orgID, userID string) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "org_id", orgID), + resource.TestCheckResourceAttr(resourceName, "user_id", userID), + + resource.TestCheckResourceAttrSet(resourceName, "username"), + resource.TestCheckResourceAttrWith(resourceName, "username", acc.IsUsername()), + resource.TestCheckResourceAttrWith(resourceName, "created_at", acc.IsTimestamp()), + + resource.TestCheckResourceAttrWith(resourceName, "team_ids.#", acc.IntGreatThan(0)), ) } + +func checkDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "mongodbatlas_cloud_user_team_assignment" { + continue + } + orgID := rs.Primary.Attributes["org_id"] + teamID := rs.Primary.Attributes["team_id"] + userID := rs.Primary.Attributes["user_id"] + username := rs.Primary.Attributes["username"] + conn := acc.ConnV2() + + userListResp, _, err := conn.MongoDBCloudUsersApi.ListTeamUsers(context.Background(), orgID, teamID).Execute() + if err != nil { + continue + } + + if userListResp != nil && userListResp.Results != nil { + for _, user := range *userListResp.Results { + if userID != "" && user.GetId() == userID { + return fmt.Errorf("cloud user team assignment for user (%s) in team (%s) still exists", userID, teamID) + } + if username != "" && user.GetUsername() == username { + return fmt.Errorf("cloud user team assignment for user (%s) in team (%s) still exists", username, teamID) + } + } + } + } + return nil +} From bb0aaaef20915c285eb81b82973d687377fdcbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:45:37 +0200 Subject: [PATCH 14/30] Fix --- internal/service/clouduserteamassignment/resource_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource_test.go b/internal/service/clouduserteamassignment/resource_test.go index e62cc4f0ee..7715ae5338 100644 --- a/internal/service/clouduserteamassignment/resource_test.go +++ b/internal/service/clouduserteamassignment/resource_test.go @@ -108,11 +108,12 @@ func checkDestroy(s *terraform.State) error { } if userListResp != nil && userListResp.Results != nil { - for _, user := range *userListResp.Results { - if userID != "" && user.GetId() == userID { + results := *userListResp.Results + for i := range results { + if userID != "" && results[i].GetId() == userID { return fmt.Errorf("cloud user team assignment for user (%s) in team (%s) still exists", userID, teamID) } - if username != "" && user.GetUsername() == username { + if username != "" && results[i].GetUsername() == username { return fmt.Errorf("cloud user team assignment for user (%s) in team (%s) still exists", username, teamID) } } From 11b6f5699d17902f8cb51b413a4e0752d67077b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:47:02 +0200 Subject: [PATCH 15/30] Add github --- .github/workflows/acceptance-tests-runner.yml | 29 ++++++++++++++++++- internal/provider/provider.go | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/acceptance-tests-runner.yml b/.github/workflows/acceptance-tests-runner.yml index 86d123febf..288e0a2c57 100644 --- a/.github/workflows/acceptance-tests-runner.yml +++ b/.github/workflows/acceptance-tests-runner.yml @@ -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' }} @@ -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: @@ -588,7 +591,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/clouduserorgassignment + run: make testacc + cluster: needs: [ change-detection, get-provider-version ] if: ${{ needs.change-detection.outputs.cluster == 'true' || inputs.test_group == 'cluster' }} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c05a5cec1e..4ee9c3f3d6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -28,6 +28,7 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/alertconfiguration" "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" @@ -480,6 +481,7 @@ func (p *MongodbtlasProvider) Resources(context.Context) []func() resource.Resou streamprivatelinkendpoint.Resource, flexcluster.Resource, resourcepolicy.Resource, + clouduserteamassignment.Resource, } if config.PreviewProviderV2AdvancedCluster() { resources = append(resources, advancedclustertpf.Resource) From 005b0c814b9205e67ec6bb7256e2afc855b8b84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:47:48 +0200 Subject: [PATCH 16/30] Fix --- .../clouduserteamassignment/resource.go | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index f709e48da6..6f1e37bfd9 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -57,11 +57,13 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou return } - newUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, orgID, teamID, apiResp) + newUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, apiResp) if diags.HasError() { resp.Diagnostics.Append(diags...) return } + newUserTeamAssignmentModel.OrgId = plan.OrgId + newUserTeamAssignmentModel.TeamId = plan.TeamId resp.Diagnostics.Append(resp.State.Set(ctx, newUserTeamAssignmentModel)...) } @@ -79,45 +81,68 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. var httpResp *http.Response var err error - userListResp, httpResp, err = connV2.MongoDBCloudUsersApi.ListTeamUsers(ctx, orgID, teamID).Execute() - if validate.StatusNotFound(httpResp) { - resp.State.RemoveResource(ctx) - return - } - var userResp *admin.OrgUserResponse if !state.UserId.IsNull() && state.UserId.ValueString() != "" { userID := state.UserId.ValueString() - for _, user := range userListResp.GetResults() { - if user.GetId() == userID { - userResp = &user + userListResp, httpResp, err = connV2.MongoDBCloudUsersApi.ListTeamUsers(ctx, orgID, teamID).Execute() + + if err != nil { + if validate.StatusNotFound(httpResp) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error getting team users by username", err.Error()) + return + } + if len(userListResp.GetResults()) == 0 { + resp.State.RemoveResource(ctx) + return + } + results := userListResp.GetResults() + for i := range results { + if results[i].GetId() == userID { + userResp = &results[i] break } } } else if !state.Username.IsNull() && state.Username.ValueString() != "" { // required for import username := state.Username.ValueString() - for _, user := range userListResp.GetResults() { - if user.GetUsername() == username { - userResp = &user - break + params := &admin.ListTeamUsersApiParams{ + Username: &username, + OrgId: orgID, + TeamId: teamID, + } + userListResp, httpResp, err = connV2.MongoDBCloudUsersApi.ListTeamUsersWithParams(ctx, params).Execute() + + if err != nil { + if validate.StatusNotFound(httpResp) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Error getting team users by username", err.Error()) + return + } + if userListResp != nil && userListResp.Results != nil { + if len(*userListResp.Results) == 0 { + resp.State.RemoveResource(ctx) + return } + userResp = &(*userListResp.Results)[0] } } - if err != nil { - resp.Diagnostics.AddError(fmt.Sprintf("error fetching user(%s) from TeamID(%s):", userResp.Username, teamID), err.Error()) - return - } if userResp == nil { resp.State.RemoveResource(ctx) return } - newCloudUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, orgID, teamID, userResp) + newCloudUserTeamAssignmentModel, diags := NewTFUserTeamAssignmentModel(ctx, userResp) if diags.HasError() { resp.Diagnostics.Append(diags...) return } + newCloudUserTeamAssignmentModel.OrgId = state.OrgId + newCloudUserTeamAssignmentModel.TeamId = state.TeamId resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserTeamAssignmentModel)...) } From a810a32435a9472ee49fef13d18e27cc8ba226cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:54:35 +0200 Subject: [PATCH 17/30] Fix --- internal/service/clouduserteamassignment/resource.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index 6f1e37bfd9..97b62c0e9d 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -178,12 +178,12 @@ func (r *rs) Delete(ctx context.Context, req resource.DeleteRequest, resp *resou func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { importID := req.ID - ok, part1, part2, part3 := conversion.ImportSplit3(req.ID) + ok, parts := conversion.ImportSplit(req.ID, 3) if !ok { resp.Diagnostics.AddError("invalid import ID format", "expected 'org_id/team_id/user_id' or 'org_id/team_id/username', got: "+importID) return } - orgID, teamID, user := part1, part2, part3 + orgID, teamID, user := parts[0], parts[1], parts[2] resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("org_id"), orgID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("team_id"), teamID)...) From 10a3c1fc75e09abd0a6e79d482807578755194b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 15:58:16 +0200 Subject: [PATCH 18/30] Changelog --- .changelog/3502.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/3502.txt diff --git a/.changelog/3502.txt b/.changelog/3502.txt new file mode 100644 index 0000000000..7fecf382e9 --- /dev/null +++ b/.changelog/3502.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +resource/mongodbatlas_cloud_user_team_assignment +``` \ No newline at end of file From a54e77ca0ceeee267e5a3b54049a59f88e84674d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 16:09:56 +0200 Subject: [PATCH 19/30] Fix --- .github/workflows/acceptance-tests-runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/acceptance-tests-runner.yml b/.github/workflows/acceptance-tests-runner.yml index 1f46519e90..b6619cff59 100644 --- a/.github/workflows/acceptance-tests-runner.yml +++ b/.github/workflows/acceptance-tests-runner.yml @@ -614,7 +614,7 @@ jobs: 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/clouduserorgassignment + ACCTEST_PACKAGES: ./internal/service/clouduserteamassignment run: make testacc cluster: From 389c42a5f50d3e60b4deb40363c99b9530aea19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 16 Jul 2025 17:30:49 +0200 Subject: [PATCH 20/30] Fixed function naming --- internal/service/clouduserteamassignment/resource_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource_test.go b/internal/service/clouduserteamassignment/resource_test.go index 7715ae5338..f046e45bae 100644 --- a/internal/service/clouduserteamassignment/resource_test.go +++ b/internal/service/clouduserteamassignment/resource_test.go @@ -30,8 +30,8 @@ func basicTestCase(t *testing.T) *resource.TestCase { CheckDestroy: checkDestroy, Steps: []resource.TestStep{ { - Config: cloudUserTeamAssignmentConfig(orgID, userID, teamName), - Check: cloudUserTeamAssignmentAttributeChecks(orgID, userID), + Config: configBasic(orgID, userID, teamName), + Check: checks(orgID, userID), }, { ResourceName: resourceName, @@ -63,7 +63,7 @@ func basicTestCase(t *testing.T) *resource.TestCase { } } -func cloudUserTeamAssignmentConfig(orgID, userID, teamName string) string { +func configBasic(orgID, userID, teamName string) string { return fmt.Sprintf(` resource "mongodbatlas_team" "test" { org_id = %[1]q @@ -78,7 +78,7 @@ func cloudUserTeamAssignmentConfig(orgID, userID, teamName string) string { orgID, userID, teamName) } -func cloudUserTeamAssignmentAttributeChecks(orgID, userID string) resource.TestCheckFunc { +func checks(orgID, userID string) resource.TestCheckFunc { return resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "org_id", orgID), resource.TestCheckResourceAttr(resourceName, "user_id", userID), From 7c3c11fe0d3a7b0a07b9bbfb84c88bd1865d47dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Thu, 17 Jul 2025 14:03:46 +0200 Subject: [PATCH 21/30] Changed test --- internal/service/clouduserteamassignment/resource_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource_test.go b/internal/service/clouduserteamassignment/resource_test.go index f046e45bae..062b38db27 100644 --- a/internal/service/clouduserteamassignment/resource_test.go +++ b/internal/service/clouduserteamassignment/resource_test.go @@ -42,8 +42,8 @@ func basicTestCase(t *testing.T) *resource.TestCase { attrs := s.RootModule().Resources[resourceName].Primary.Attributes orgID := attrs["org_id"] teamID := attrs["team_id"] - userID := attrs["user_id"] - return orgID + "/" + teamID + "/" + userID, nil + username := attrs["username"] + return orgID + "/" + teamID + "/" + username, nil }, }, { @@ -55,8 +55,8 @@ func basicTestCase(t *testing.T) *resource.TestCase { attrs := s.RootModule().Resources[resourceName].Primary.Attributes orgID := attrs["org_id"] teamID := attrs["team_id"] - username := attrs["username"] - return orgID + "/" + teamID + "/" + username, nil + userID := attrs["user_id"] + return orgID + "/" + teamID + "/" + userID, nil }, }, }, From 7ebd8d237b2b2b94102e21b949aa11ec5079ec20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Fri, 18 Jul 2025 10:24:30 +0200 Subject: [PATCH 22/30] Changed `project_role_assignments` from List to Set --- internal/service/clouduserteamassignment/model.go | 12 ++++++------ .../clouduserteamassignment/resource_schema.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index 3ca228adc1..980e5ee850 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -63,12 +63,12 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty orgRoles, _ = types.SetValueFrom(ctx, types.StringType, *roles.OrgRoles) } - projectRoleAssignmentsList := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) + projectRoleAssignmentsSet := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) rolesObj, _ := types.ObjectValue( RolesObjectAttrTypes, map[string]attr.Value{ - "project_role_assignments": projectRoleAssignmentsList, + "project_role_assignments": projectRoleAssignmentsSet, "org_roles": orgRoles, }, ) @@ -76,9 +76,9 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty return rolesObj, diags } -func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]admin.GroupRoleAssignment) types.List { +func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]admin.GroupRoleAssignment) types.Set { if groupRoleAssignments == nil { - return types.ListNull(ProjectRoleAssignmentsAttrType) + return types.SetNull(ProjectRoleAssignmentsAttrType) } var projectRoleAssignments []TFProjectRoleAssignmentsModel @@ -97,8 +97,8 @@ func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]ad }) } - praList, _ := types.ListValueFrom(ctx, ProjectRoleAssignmentsAttrType.ElemType.(types.ObjectType), projectRoleAssignments) - return praList + praSet, _ := types.SetValueFrom(ctx, ProjectRoleAssignmentsAttrType.ElemType.(types.ObjectType), projectRoleAssignments) + return praSet } func NewUserTeamAssignmentReq(ctx context.Context, plan *TFUserTeamAssignmentModel) (*admin.AddOrRemoveUserFromTeam, diag.Diagnostics) { diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index 6fa22585e1..46156a147b 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -40,7 +40,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { Computed: true, MarkdownDescription: "Organization and project level roles to assign the MongoDB Cloud user within one organization.", Attributes: map[string]schema.Attribute{ - "project_role_assignments": schema.ListNestedAttribute{ + "project_role_assignments": schema.SetNestedAttribute{ Computed: true, MarkdownDescription: "List of project level role assignments to assign the MongoDB Cloud user.", NestedObject: schema.NestedAttributeObject{ @@ -159,7 +159,7 @@ type TFProjectRoleAssignmentsModel struct { ProjectRoles types.Set `tfsdk:"project_roles"` } -var ProjectRoleAssignmentsAttrType = types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ +var ProjectRoleAssignmentsAttrType = types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ "project_id": types.StringType, "project_roles": types.SetType{ElemType: types.StringType}, }}} From 3d467d0b369fae12cb0fa64c7d970e34433c980d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 21 Jul 2025 11:11:05 +0200 Subject: [PATCH 23/30] Changed the attribute name `project_roles_assignments` to project_role_assignments in `organization` and `team` DS for consistency in naming. --- internal/common/conversion/flatten_expand.go | 6 +++--- internal/common/dsschema/users_schema.go | 2 +- internal/service/organization/resource_organization_test.go | 4 ++-- internal/service/team/data_source_team_test.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/common/conversion/flatten_expand.go b/internal/common/conversion/flatten_expand.go index c54ebe7f19..955d58e40e 100644 --- a/internal/common/conversion/flatten_expand.go +++ b/internal/common/conversion/flatten_expand.go @@ -57,14 +57,14 @@ func FlattenUsers(users []admin.OrgUserResponse) []map[string]any { func flattenUserRoles(roles admin.OrgUserRolesResponse) []map[string]any { ret := make([]map[string]any, 0) roleMap := map[string]any{ - "org_roles": []string{}, - "project_roles_assignments": []map[string]any{}, + "org_roles": []string{}, + "project_role_assignments": []map[string]any{}, } if roles.HasOrgRoles() { roleMap["org_roles"] = roles.GetOrgRoles() } if roles.HasGroupRoleAssignments() { - roleMap["project_roles_assignments"] = flattenProjectRolesAssignments(roles.GetGroupRoleAssignments()) + roleMap["project_role_assignments"] = flattenProjectRolesAssignments(roles.GetGroupRoleAssignments()) } ret = append(ret, roleMap) return ret diff --git a/internal/common/dsschema/users_schema.go b/internal/common/dsschema/users_schema.go index 4d1825f6df..a2f7f6e506 100644 --- a/internal/common/dsschema/users_schema.go +++ b/internal/common/dsschema/users_schema.go @@ -28,7 +28,7 @@ func DSOrgUsersSchema() *schema.Schema { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "project_roles_assignments": { + "project_role_assignments": { Type: schema.TypeSet, Computed: true, Elem: &schema.Resource{ diff --git a/internal/service/organization/resource_organization_test.go b/internal/service/organization/resource_organization_test.go index 04b2c27b30..840495a41b 100644 --- a/internal/service/organization/resource_organization_test.go +++ b/internal/service/organization/resource_organization_test.go @@ -207,7 +207,7 @@ func TestAccConfigDSOrganization_users(t *testing.T) { resource.TestCheckResourceAttrWith(datasourceName, "users.#", acc.IntGreatThan(0)), resource.TestCheckResourceAttrSet(datasourceName, "users.0.id"), resource.TestCheckResourceAttrSet(datasourceName, "users.0.roles.0.org_roles.#"), - resource.TestCheckResourceAttrSet(datasourceName, "users.0.roles.0.project_roles_assignments.#"), + resource.TestCheckResourceAttrSet(datasourceName, "users.0.roles.0.project_role_assignments.#"), resource.TestCheckResourceAttrWith(datasourceName, "users.0.username", acc.IsUsername()), resource.TestCheckResourceAttrWith(datasourceName, "users.0.last_auth", acc.IsTimestamp()), resource.TestCheckResourceAttrWith(datasourceName, "users.0.created_at", acc.IsTimestamp()), @@ -215,7 +215,7 @@ func TestAccConfigDSOrganization_users(t *testing.T) { resource.TestCheckResourceAttrWith(pluralDSName, "results.0.users.#", acc.IntGreatThan(0)), resource.TestCheckResourceAttrSet(pluralDSName, "results.0.users.0.id"), resource.TestCheckResourceAttrSet(pluralDSName, "results.0.users.0.roles.0.org_roles.#"), - resource.TestCheckResourceAttrSet(pluralDSName, "results.0.users.0.roles.0.project_roles_assignments.#"), + resource.TestCheckResourceAttrSet(pluralDSName, "results.0.users.0.roles.0.project_role_assignments.#"), resource.TestCheckResourceAttrWith(pluralDSName, "results.0.users.0.username", acc.IsUsername()), resource.TestCheckResourceAttrWith(pluralDSName, "results.0.users.0.last_auth", acc.IsTimestamp()), ), diff --git a/internal/service/team/data_source_team_test.go b/internal/service/team/data_source_team_test.go index e2a6d3a7d7..c95da30d7e 100644 --- a/internal/service/team/data_source_team_test.go +++ b/internal/service/team/data_source_team_test.go @@ -30,7 +30,7 @@ func TestAccConfigDSTeam_basic(t *testing.T) { resource.TestCheckResourceAttr(dataSourceName, "name", name), resource.TestCheckResourceAttr(dataSourceName, "usernames.#", "1"), resource.TestCheckResourceAttrSet(dataSourceName, "users.0.team_ids.0"), - resource.TestCheckResourceAttrSet(dataSourceName, "users.0.roles.0.project_roles_assignments.#"), + resource.TestCheckResourceAttrSet(dataSourceName, "users.0.roles.0.project_role_assignments.#"), resource.TestCheckResourceAttrWith(dataSourceName, "users.0.username", acc.IsUsername()), resource.TestCheckResourceAttrWith(dataSourceName, "users.0.last_auth", acc.IsTimestamp()), resource.TestCheckResourceAttrWith(dataSourceName, "users.0.created_at", acc.IsTimestamp()), From eba734dc9d8873ea988af63ab8a0f8ebd48f7930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 21 Jul 2025 11:41:10 +0200 Subject: [PATCH 24/30] minor changes --- .../service/clouduserteamassignment/model.go | 24 ++++++------------- .../clouduserteamassignment/model_test.go | 4 ++-- .../clouduserteamassignment/resource.go | 2 +- .../resource_schema.go | 6 ++--- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index 980e5ee850..e3c4366a62 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -22,11 +22,14 @@ func NewTFUserTeamAssignmentModel(ctx context.Context, apiResp *admin.OrgUserRes rolesObj, rolesDiags = NewTFRolesModel(ctx, &apiResp.Roles) diags.Append(rolesDiags...) + teamIDs := conversion.TFSetValueOrNull(ctx, apiResp.TeamIds, types.StringType) + userTeamAssignment := TFUserTeamAssignmentModel{ UserId: types.StringValue(apiResp.GetId()), Username: types.StringValue(apiResp.GetUsername()), OrgMembershipStatus: types.StringValue(apiResp.GetOrgMembershipStatus()), Roles: rolesObj, + TeamIds: teamIDs, InvitationCreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.InvitationCreatedAt)), InvitationExpiresAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.InvitationExpiresAt)), InviterUsername: types.StringPointerValue(apiResp.InviterUsername), @@ -38,14 +41,6 @@ func NewTFUserTeamAssignmentModel(ctx context.Context, apiResp *admin.OrgUserRes 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 } @@ -63,16 +58,15 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty orgRoles, _ = types.SetValueFrom(ctx, types.StringType, *roles.OrgRoles) } - projectRoleAssignmentsSet := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) + projectRoleAssignmentsList := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) rolesObj, _ := types.ObjectValue( RolesObjectAttrTypes, map[string]attr.Value{ - "project_role_assignments": projectRoleAssignmentsSet, + "project_role_assignments": projectRoleAssignmentsList, "org_roles": orgRoles, }, ) - return rolesObj, diags } @@ -85,12 +79,8 @@ func NewTFProjectRoleAssignments(ctx context.Context, groupRoleAssignments *[]ad 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) - } + projectRoles := conversion.TFSetValueOrNull(ctx, pra.GroupRoles, types.StringType) + projectRoleAssignments = append(projectRoleAssignments, TFProjectRoleAssignmentsModel{ ProjectId: projectID, ProjectRoles: projectRoles, diff --git a/internal/service/clouduserteamassignment/model_test.go b/internal/service/clouduserteamassignment/model_test.go index c3ff0633c6..9164939dd2 100644 --- a/internal/service/clouduserteamassignment/model_test.go +++ b/internal/service/clouduserteamassignment/model_test.go @@ -95,7 +95,7 @@ func TestUserTeamAssignmentSDKToTFModel(t *testing.T) { OrgId: types.StringNull(), TeamId: types.StringNull(), Roles: expectedRoles, - TeamIDs: expectedTeams, + TeamIds: expectedTeams, } testCases := map[string]sdkToTFModelTestCase{ @@ -159,7 +159,7 @@ func TestNewUserTeamAssignmentReq(t *testing.T) { Roles: createRolesObject(ctx, testOrgRoles, []clouduserteamassignment.TFProjectRoleAssignmentsModel{ projectAssignment, }), - TeamIDs: teams, + TeamIds: teams, InvitationCreatedAt: types.StringValue(testInvitationCreatedAt), InvitationExpiresAt: types.StringValue(testInvitationExpiresAt), InviterUsername: types.StringValue(testInviterUsername), diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index 97b62c0e9d..5d9f6a8518 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -32,7 +32,7 @@ type rs struct { } func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = ResourceSchema(ctx) + resp.Schema = resourceSchema() conversion.UpdateSchemaDescription(&resp.Schema) } diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index 46156a147b..60852598e2 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -1,8 +1,6 @@ package clouduserteamassignment import ( - "context" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -10,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -func ResourceSchema(ctx context.Context) schema.Schema { +func resourceSchema() schema.Schema { return schema.Schema{ Attributes: map[string]schema.Attribute{ "org_id": schema.StringAttribute{ @@ -137,7 +135,7 @@ type TFUserTeamAssignmentModel struct { Username types.String `tfsdk:"username"` OrgMembershipStatus types.String `tfsdk:"org_membership_status"` Roles types.Object `tfsdk:"roles"` - TeamIDs types.Set `tfsdk:"team_ids"` + TeamIds types.Set `tfsdk:"team_ids"` InvitationCreatedAt types.String `tfsdk:"invitation_created_at"` InvitationExpiresAt types.String `tfsdk:"invitation_expires_at"` InviterUsername types.String `tfsdk:"inviter_username"` From 951931576f0d3231bb347813ac98f5d3fbd73429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 21 Jul 2025 11:51:08 +0200 Subject: [PATCH 25/30] Fix --- internal/service/clouduserteamassignment/model.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index e3c4366a62..03764d8bf7 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -51,20 +51,15 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty 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) - } + orgRoles := conversion.TFSetValueOrNull(ctx, roles.OrgRoles, types.StringType) projectRoleAssignmentsList := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) rolesObj, _ := types.ObjectValue( RolesObjectAttrTypes, map[string]attr.Value{ - "project_role_assignments": projectRoleAssignmentsList, "org_roles": orgRoles, + "project_role_assignments": projectRoleAssignmentsList, }, ) return rolesObj, diags From adf5af54b0027d5d2dc2aea1e928cf8a4fc54fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 21 Jul 2025 14:18:56 +0200 Subject: [PATCH 26/30] Fix --- .../clouduserteamassignment/resource.go | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index 5d9f6a8518..cbc17e87d9 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -94,15 +94,17 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. resp.Diagnostics.AddError("Error getting team users by username", err.Error()) return } - if len(userListResp.GetResults()) == 0 { - resp.State.RemoveResource(ctx) - return - } - results := userListResp.GetResults() - for i := range results { - if results[i].GetId() == userID { - userResp = &results[i] - break + if userListResp != nil && userListResp.Results != nil { + if len(userListResp.GetResults()) == 0 { + resp.State.RemoveResource(ctx) + return + } + results := userListResp.GetResults() + for i := range results { + if results[i].GetId() == userID { + userResp = &results[i] + break + } } } } else if !state.Username.IsNull() && state.Username.ValueString() != "" { // required for import From bfd3817c5ca47f3eb50f76f683b21276cb3749cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 21 Jul 2025 14:24:23 +0200 Subject: [PATCH 27/30] Fix --- internal/service/clouduserteamassignment/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index cbc17e87d9..d504af4cc5 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -91,7 +91,7 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. resp.State.RemoveResource(ctx) return } - resp.Diagnostics.AddError("Error getting team users by username", err.Error()) + resp.Diagnostics.AddError("Error getting team users by user_id", err.Error()) return } if userListResp != nil && userListResp.Results != nil { From 057f72cbd4deb10be8e5794c45f040448b89aa2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Mon, 21 Jul 2025 17:53:37 +0200 Subject: [PATCH 28/30] Changed `project_role_assignments` from List to Set --- internal/service/clouduserteamassignment/model.go | 4 ++-- internal/service/clouduserteamassignment/resource_schema.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index 03764d8bf7..edbc29c639 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -53,13 +53,13 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty orgRoles := conversion.TFSetValueOrNull(ctx, roles.OrgRoles, types.StringType) - projectRoleAssignmentsList := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) + projectRoleAssignmentsSet := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) rolesObj, _ := types.ObjectValue( RolesObjectAttrTypes, map[string]attr.Value{ "org_roles": orgRoles, - "project_role_assignments": projectRoleAssignmentsList, + "project_role_assignments": projectRoleAssignmentsSet, }, ) return rolesObj, diags diff --git a/internal/service/clouduserteamassignment/resource_schema.go b/internal/service/clouduserteamassignment/resource_schema.go index 60852598e2..c126c569eb 100644 --- a/internal/service/clouduserteamassignment/resource_schema.go +++ b/internal/service/clouduserteamassignment/resource_schema.go @@ -148,8 +148,8 @@ type TFUserTeamAssignmentModel struct { } type TFRolesModel struct { - ProjectRoleAssignments types.List `tfsdk:"project_role_assignments"` - OrgRoles types.Set `tfsdk:"org_roles"` + ProjectRoleAssignments types.Set `tfsdk:"project_role_assignments"` + OrgRoles types.Set `tfsdk:"org_roles"` } type TFProjectRoleAssignmentsModel struct { From 57c73b34420ffab6a3de9ac2782480f0ff8f87b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Tue, 22 Jul 2025 12:45:08 +0200 Subject: [PATCH 29/30] Removed redundant check --- internal/service/clouduserteamassignment/resource.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index d504af4cc5..0eb5dae687 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -94,7 +94,7 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. resp.Diagnostics.AddError("Error getting team users by user_id", err.Error()) return } - if userListResp != nil && userListResp.Results != nil { + if userListResp != nil { if len(userListResp.GetResults()) == 0 { resp.State.RemoveResource(ctx) return @@ -124,12 +124,12 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. resp.Diagnostics.AddError("Error getting team users by username", err.Error()) return } - if userListResp != nil && userListResp.Results != nil { - if len(*userListResp.Results) == 0 { + if userListResp != nil { + if len(userListResp.GetResults()) == 0 { resp.State.RemoveResource(ctx) return } - userResp = &(*userListResp.Results)[0] + userResp = &(userListResp.GetResults())[0] } } From 503bbfc53ad9486c5b872359f0e1d1e800d72dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Sa=CC=81nchez=20Sa=CC=81nchez?= Date: Wed, 23 Jul 2025 10:58:06 +0200 Subject: [PATCH 30/30] Code improvements --- .../service/clouduserteamassignment/model.go | 18 +++++++----------- .../clouduserteamassignment/resource.go | 15 +++++++++++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/internal/service/clouduserteamassignment/model.go b/internal/service/clouduserteamassignment/model.go index edbc29c639..0bb83f93dc 100644 --- a/internal/service/clouduserteamassignment/model.go +++ b/internal/service/clouduserteamassignment/model.go @@ -3,7 +3,6 @@ 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" @@ -12,14 +11,12 @@ import ( 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) + rolesObj, rolesDiags := NewTFRolesModel(ctx, &apiResp.Roles) diags.Append(rolesDiags...) teamIDs := conversion.TFSetValueOrNull(ctx, apiResp.TeamIds, types.StringType) @@ -55,13 +52,12 @@ func NewTFRolesModel(ctx context.Context, roles *admin.OrgUserRolesResponse) (ty projectRoleAssignmentsSet := NewTFProjectRoleAssignments(ctx, roles.GroupRoleAssignments) - rolesObj, _ := types.ObjectValue( - RolesObjectAttrTypes, - map[string]attr.Value{ - "org_roles": orgRoles, - "project_role_assignments": projectRoleAssignmentsSet, - }, - ) + rolesModel := TFRolesModel{ + OrgRoles: orgRoles, + ProjectRoleAssignments: projectRoleAssignmentsSet, + } + + rolesObj, _ := types.ObjectValueFrom(ctx, RolesObjectAttrTypes, rolesModel) return rolesObj, diags } diff --git a/internal/service/clouduserteamassignment/resource.go b/internal/service/clouduserteamassignment/resource.go index 0eb5dae687..cac8ae5998 100644 --- a/internal/service/clouduserteamassignment/resource.go +++ b/internal/service/clouduserteamassignment/resource.go @@ -14,7 +14,13 @@ import ( "go.mongodb.org/atlas-sdk/v20250312005/admin" ) -const resourceName = "cloud_user_team_assignment" +const ( + resourceName = "cloud_user_team_assignment" + warnUnsupportedOperation = "Operation not supported" + errorReadingByUserID = "Error getting team users by user_id" + errorReadingByUsername = "Error getting team users by username" + invalidImportID = "Invalid import ID format" +) var _ resource.ResourceWithConfigure = &rs{} var _ resource.ResourceWithImportState = &rs{} @@ -91,7 +97,7 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. resp.State.RemoveResource(ctx) return } - resp.Diagnostics.AddError("Error getting team users by user_id", err.Error()) + resp.Diagnostics.AddError(errorReadingByUserID, err.Error()) return } if userListResp != nil { @@ -121,7 +127,7 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. resp.State.RemoveResource(ctx) return } - resp.Diagnostics.AddError("Error getting team users by username", err.Error()) + resp.Diagnostics.AddError(errorReadingByUsername, err.Error()) return } if userListResp != nil { @@ -149,6 +155,7 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource. } func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError(warnUnsupportedOperation, "Updating the cloud user team assignment is not supported. To modify your infrastructure, please delete the existing mongodbatlas_cloud_user_team_assignment resource and create a new one with the necessary updates") } func (r *rs) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { @@ -182,7 +189,7 @@ func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, r importID := req.ID ok, parts := conversion.ImportSplit(req.ID, 3) if !ok { - resp.Diagnostics.AddError("invalid import ID format", "expected 'org_id/team_id/user_id' or 'org_id/team_id/username', got: "+importID) + resp.Diagnostics.AddError(invalidImportID, "expected 'org_id/team_id/user_id' or 'org_id/team_id/username', got: "+importID) return } orgID, teamID, user := parts[0], parts[1], parts[2]