Skip to content

Commit 77b1965

Browse files
csanxCristina Sánchez Sánchezmaasthalantoli
authored
feat: Adds users attribute to mongodbatlas_project singular data source (#3439)
* WIP * Fixed acceptance test * Added a comment * Added changelog * Update docs/data-sources/project.md * Update .changelog/3439.txt Co-authored-by: Leo Antoli <430982+lantoli@users.noreply.github.com> * PR comments * Fix * Arguments fixed * Fix constant --------- Co-authored-by: Cristina Sánchez Sánchez <cristina.sanchez@mongodb.com> Co-authored-by: maastha <122359335+maastha@users.noreply.github.com> Co-authored-by: Leo Antoli <430982+lantoli@users.noreply.github.com>
1 parent 14e40d7 commit 77b1965

File tree

8 files changed

+377
-52
lines changed

8 files changed

+377
-52
lines changed

.changelog/3439.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
data-source/mongodbatlas_project: Adds `users` attribute
3+
```

docs/data-sources/project.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ In addition to all arguments above, the following attributes are exported:
8282
* `teams` - Returns all teams to which the authenticated user has access in the project. See [Teams](#teams).
8383
* `limits` - The limits for the specified project. See [Limits](#limits).
8484
* `ip_addresses` - IP addresses in a project categorized by services. See [IP Addresses](#ip-addresses). **WARNING:** This attribute is deprecated, use the `mongodbatlas_project_ip_addresses` data source instead.
85-
85+
* `users` - Returns list of all pending and active MongoDB Cloud users associated with the specified project.
8686
* `is_collect_database_specifics_statistics_enabled` - Flag that indicates whether to enable statistics in [cluster metrics](https://www.mongodb.com/docs/atlas/monitor-cluster-metrics/) collection for the project.
8787
* `is_data_explorer_enabled` - Flag that indicates whether to enable Data Explorer for the project. If enabled, you can query your database with an easy to use interface.
8888
* `is_extended_storage_sizes_enabled` - Flag that indicates whether to enable extended storage sizes for the specified project.
@@ -112,6 +112,17 @@ In addition to all arguments above, the following attributes are exported:
112112
* `services.clusters.#.inbound` - List of inbound IP addresses associated with the cluster. If your network allows outbound HTTP requests only to specific IP addresses, you must allow access to the following IP addresses so that your application can connect to your Atlas cluster.
113113
* `services.clusters.#.outbound` - List of outbound IP addresses associated with the cluster. If your network allows inbound HTTP requests only from specific IP addresses, you must allow access from the following IP addresses so that your Atlas cluster can communicate with your webhooks and KMS.
114114

115+
### Users
116+
* `id`- Unique 24-hexadecimal digit string that identifies the MongoDB Cloud user.
117+
* `orgMembershipStatus`- 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.
118+
* `roles`- One or more project-level roles assigned to the MongoDB Cloud user.
119+
* `username`- Email address that represents the username of the MongoDB Cloud user.
120+
* `country`- Two-character alphabetical string that identifies the MongoDB Cloud user's geographic location. This parameter uses the ISO 3166-1a2 code format.
121+
* `createdAt`- Date and time when MongoDB Cloud created the current account. This value is in the ISO 8601 timestamp format in UTC.
122+
* `firstName`- First or given name that belongs to the MongoDB Cloud user.
123+
* `lastAuth` - Date and time when the current account last authenticated. This value is in the ISO 8601 timestamp format in UTC.
124+
* `lastName`- Last name, family name, or surname that belongs to the MongoDB Cloud user.
125+
* `mobileNumber` - Mobile phone number that belongs to the MongoDB Cloud user.
115126

116127

117128
See [MongoDB Atlas API - Project](https://www.mongodb.com/docs/atlas/reference/api-resources-spec/#tag/Projects) - [and MongoDB Atlas API - Teams](https://docs.atlas.mongodb.com/reference/api/project-get-teams/) Documentation for more information.

internal/service/project/data_source_project.go

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package project
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67

78
"go.mongodb.org/atlas-sdk/v20250312004/admin"
89

@@ -14,6 +15,7 @@ import (
1415
"github.com/hashicorp/terraform-plugin-framework/types"
1516
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant"
1617
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
18+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/dsschema"
1719
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
1820
)
1921

@@ -33,31 +35,48 @@ type projectDS struct {
3335
}
3436

3537
type TFProjectDSModel struct {
36-
IPAddresses types.Object `tfsdk:"ip_addresses"`
37-
Created types.String `tfsdk:"created"`
38-
OrgID types.String `tfsdk:"org_id"`
39-
RegionUsageRestrictions types.String `tfsdk:"region_usage_restrictions"`
40-
ID types.String `tfsdk:"id"`
41-
Name types.String `tfsdk:"name"`
42-
ProjectID types.String `tfsdk:"project_id"`
43-
Tags types.Map `tfsdk:"tags"`
44-
Teams []*TFTeamDSModel `tfsdk:"teams"`
45-
Limits []*TFLimitModel `tfsdk:"limits"`
46-
ClusterCount types.Int64 `tfsdk:"cluster_count"`
47-
IsCollectDatabaseSpecificsStatisticsEnabled types.Bool `tfsdk:"is_collect_database_specifics_statistics_enabled"`
48-
IsRealtimePerformancePanelEnabled types.Bool `tfsdk:"is_realtime_performance_panel_enabled"`
49-
IsSchemaAdvisorEnabled types.Bool `tfsdk:"is_schema_advisor_enabled"`
50-
IsPerformanceAdvisorEnabled types.Bool `tfsdk:"is_performance_advisor_enabled"`
51-
IsExtendedStorageSizesEnabled types.Bool `tfsdk:"is_extended_storage_sizes_enabled"`
52-
IsDataExplorerEnabled types.Bool `tfsdk:"is_data_explorer_enabled"`
53-
IsSlowOperationThresholdingEnabled types.Bool `tfsdk:"is_slow_operation_thresholding_enabled"`
38+
Tags types.Map `tfsdk:"tags"`
39+
IPAddresses types.Object `tfsdk:"ip_addresses"`
40+
Created types.String `tfsdk:"created"`
41+
OrgID types.String `tfsdk:"org_id"`
42+
RegionUsageRestrictions types.String `tfsdk:"region_usage_restrictions"`
43+
ID types.String `tfsdk:"id"`
44+
Name types.String `tfsdk:"name"`
45+
ProjectID types.String `tfsdk:"project_id"`
46+
Teams []*TFTeamDSModel `tfsdk:"teams"`
47+
Limits []*TFLimitModel `tfsdk:"limits"`
48+
Users []*TFCloudUsersDSModel `tfsdk:"users"`
49+
ClusterCount types.Int64 `tfsdk:"cluster_count"`
50+
IsCollectDatabaseSpecificsStatisticsEnabled types.Bool `tfsdk:"is_collect_database_specifics_statistics_enabled"`
51+
IsRealtimePerformancePanelEnabled types.Bool `tfsdk:"is_realtime_performance_panel_enabled"`
52+
IsSchemaAdvisorEnabled types.Bool `tfsdk:"is_schema_advisor_enabled"`
53+
IsPerformanceAdvisorEnabled types.Bool `tfsdk:"is_performance_advisor_enabled"`
54+
IsExtendedStorageSizesEnabled types.Bool `tfsdk:"is_extended_storage_sizes_enabled"`
55+
IsDataExplorerEnabled types.Bool `tfsdk:"is_data_explorer_enabled"`
56+
IsSlowOperationThresholdingEnabled types.Bool `tfsdk:"is_slow_operation_thresholding_enabled"`
5457
}
5558

5659
type TFTeamDSModel struct {
5760
TeamID types.String `tfsdk:"team_id"`
5861
RoleNames types.List `tfsdk:"role_names"`
5962
}
6063

64+
type TFCloudUsersDSModel struct {
65+
ID types.String `tfsdk:"id"`
66+
OrgMembershipStatus types.String `tfsdk:"org_membership_status"`
67+
Roles types.List `tfsdk:"roles"`
68+
Username types.String `tfsdk:"username"`
69+
InvitationCreatedAt types.String `tfsdk:"invitation_created_at"`
70+
InvitationExpiresAt types.String `tfsdk:"invitation_expires_at"`
71+
InviterUsername types.String `tfsdk:"inviter_username"`
72+
Country types.String `tfsdk:"country"`
73+
CreatedAt types.String `tfsdk:"created_at"`
74+
FirstName types.String `tfsdk:"first_name"`
75+
LastAuth types.String `tfsdk:"last_auth"`
76+
LastName types.String `tfsdk:"last_name"`
77+
MobileNumber types.String `tfsdk:"mobile_number"`
78+
}
79+
6180
func (d *projectDS) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
6281
resp.Schema = schema.Schema{
6382
Attributes: map[string]schema.Attribute{
@@ -179,6 +198,53 @@ func (d *projectDS) Schema(ctx context.Context, req datasource.SchemaRequest, re
179198
ElementType: types.StringType,
180199
Computed: true,
181200
},
201+
"users": schema.SetNestedAttribute{
202+
Computed: true,
203+
NestedObject: schema.NestedAttributeObject{
204+
Attributes: map[string]schema.Attribute{
205+
"id": schema.StringAttribute{
206+
Computed: true,
207+
},
208+
"org_membership_status": schema.StringAttribute{
209+
Computed: true,
210+
},
211+
"roles": schema.ListAttribute{
212+
Computed: true,
213+
ElementType: types.StringType,
214+
},
215+
"username": schema.StringAttribute{
216+
Computed: true,
217+
},
218+
"invitation_created_at": schema.StringAttribute{
219+
Computed: true,
220+
},
221+
"invitation_expires_at": schema.StringAttribute{
222+
Computed: true,
223+
},
224+
"inviter_username": schema.StringAttribute{
225+
Computed: true,
226+
},
227+
"country": schema.StringAttribute{
228+
Computed: true,
229+
},
230+
"created_at": schema.StringAttribute{
231+
Computed: true,
232+
},
233+
"first_name": schema.StringAttribute{
234+
Computed: true,
235+
},
236+
"last_auth": schema.StringAttribute{
237+
Computed: true,
238+
},
239+
"last_name": schema.StringAttribute{
240+
Computed: true,
241+
},
242+
"mobile_number": schema.StringAttribute{
243+
Computed: true,
244+
},
245+
},
246+
},
247+
},
182248
},
183249
}
184250
conversion.UpdateSchemaDescription(&resp.Schema)
@@ -215,14 +281,22 @@ func (d *projectDS) Read(ctx context.Context, req datasource.ReadRequest, resp *
215281
return
216282
}
217283
}
284+
projectPropsParams := &PropsParams{
285+
ProjectID: project.GetId(),
286+
IsDataSource: true,
287+
ProjectsAPI: connV2.ProjectsApi,
288+
TeamsAPI: connV2.TeamsApi,
289+
PerformanceAdvisorAPI: connV2.PerformanceAdvisorApi,
290+
MongoDBCloudUsersAPI: connV2.MongoDBCloudUsersApi,
291+
}
218292

219-
projectProps, err := GetProjectPropsFromAPI(ctx, connV2.ProjectsApi, connV2.TeamsApi, connV2.PerformanceAdvisorApi, project.GetId(), &resp.Diagnostics)
293+
projectProps, err := GetProjectPropsFromAPI(ctx, projectPropsParams, &resp.Diagnostics)
220294
if err != nil {
221295
resp.Diagnostics.AddError("error when getting project properties", fmt.Sprintf(ErrorProjectRead, project.GetId(), err.Error()))
222296
return
223297
}
224298

225-
newProjectState, diags := NewTFProjectDataSourceModel(ctx, project, *projectProps)
299+
newProjectState, diags := NewTFProjectDataSourceModel(ctx, project, projectProps)
226300
resp.Diagnostics.Append(diags...)
227301
if resp.Diagnostics.HasError() {
228302
return
@@ -233,3 +307,11 @@ func (d *projectDS) Read(ctx context.Context, req datasource.ReadRequest, resp *
233307
return
234308
}
235309
}
310+
311+
func ListAllProjectUsers(ctx context.Context, projectID string, mongoDBCloudUsersAPI admin.MongoDBCloudUsersApi) ([]admin.GroupUserResponse, error) {
312+
return dsschema.AllPages(ctx, func(ctx context.Context, pageNum int) (dsschema.PaginateResponse[admin.GroupUserResponse], *http.Response, error) {
313+
request := mongoDBCloudUsersAPI.ListProjectUsers(ctx, projectID)
314+
request = request.PageNum(pageNum)
315+
return request.Execute()
316+
})
317+
}

internal/service/project/data_source_projects.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,19 @@ func populateProjectsDataSourceModel(ctx context.Context, connV2 *admin.APIClien
215215
results := make([]*TFProjectDSModel, 0, len(input))
216216
for i := range input {
217217
project := input[i]
218-
projectProps, err := GetProjectPropsFromAPI(ctx, connV2.ProjectsApi, connV2.TeamsApi, connV2.PerformanceAdvisorApi, project.GetId(), &diagnostics)
218+
219+
projectPropsParams := &PropsParams{
220+
ProjectID: project.GetId(),
221+
IsDataSource: true,
222+
ProjectsAPI: connV2.ProjectsApi,
223+
TeamsAPI: connV2.TeamsApi,
224+
PerformanceAdvisorAPI: connV2.PerformanceAdvisorApi,
225+
MongoDBCloudUsersAPI: connV2.MongoDBCloudUsersApi,
226+
}
227+
228+
projectProps, err := GetProjectPropsFromAPI(ctx, projectPropsParams, &diagnostics)
219229
if err == nil { // if the project is still valid, e.g. could have just been deleted
220-
projectModel, diags := NewTFProjectDataSourceModel(ctx, &project, *projectProps)
230+
projectModel, diags := NewTFProjectDataSourceModel(ctx, &project, projectProps)
221231
diagnostics = append(diagnostics, diags...)
222232
if projectModel != nil {
223233
results = append(results, projectModel)

internal/service/project/model_project.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ import (
1111
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
1212
)
1313

14-
func NewTFProjectDataSourceModel(ctx context.Context, project *admin.Group, projectProps AdditionalProperties) (*TFProjectDSModel, diag.Diagnostics) {
15-
ipAddressesModel, diags := NewTFIPAddressesModel(ctx, projectProps.IPAddresses)
14+
func NewTFProjectDataSourceModel(ctx context.Context, project *admin.Group, projectProps *AdditionalProperties) (*TFProjectDSModel, diag.Diagnostics) {
15+
var diags diag.Diagnostics
16+
if project == nil {
17+
diags.AddError("Invalid Project Data", "Project data is nil and cannot be processed")
18+
return nil, diags
19+
}
20+
if projectProps == nil {
21+
diags.AddError("Invalid Project Properties", "Project properties data is nil and cannot be processed")
22+
return nil, diags
23+
}
24+
ipAddressesModel, ipDiags := NewTFIPAddressesModel(ctx, projectProps.IPAddresses)
25+
diags.Append(ipDiags...)
1626
if diags.HasError() {
1727
return nil, diags
1828
}
@@ -36,11 +46,12 @@ func NewTFProjectDataSourceModel(ctx context.Context, project *admin.Group, proj
3646
IPAddresses: ipAddressesModel,
3747
Tags: conversion.NewTFTags(project.GetTags()),
3848
IsSlowOperationThresholdingEnabled: types.BoolValue(projectProps.IsSlowOperationThresholdingEnabled),
49+
Users: NewTFCloudUsersDataSourceModel(ctx, projectProps.Users),
3950
}, nil
4051
}
4152

4253
func NewTFTeamsDataSourceModel(ctx context.Context, atlasTeams *admin.PaginatedTeamRole) []*TFTeamDSModel {
43-
if atlasTeams.GetTotalCount() == 0 {
54+
if atlasTeams == nil || atlasTeams.GetTotalCount() == 0 {
4455
return nil
4556
}
4657
results := atlasTeams.GetResults()
@@ -71,6 +82,33 @@ func NewTFLimitsDataSourceModel(ctx context.Context, dataFederationLimits []admi
7182
return limits
7283
}
7384

85+
func NewTFCloudUsersDataSourceModel(ctx context.Context, cloudUsers []admin.GroupUserResponse) []*TFCloudUsersDSModel {
86+
if len(cloudUsers) == 0 {
87+
return []*TFCloudUsersDSModel{}
88+
}
89+
users := make([]*TFCloudUsersDSModel, len(cloudUsers))
90+
for i := range cloudUsers {
91+
cloudUser := &cloudUsers[i]
92+
roles, _ := types.ListValueFrom(ctx, types.StringType, cloudUser.Roles)
93+
users[i] = &TFCloudUsersDSModel{
94+
ID: types.StringValue(cloudUser.Id),
95+
OrgMembershipStatus: types.StringValue(cloudUser.OrgMembershipStatus),
96+
Roles: roles,
97+
Username: types.StringValue(cloudUser.Username),
98+
InvitationCreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(cloudUser.InvitationCreatedAt)),
99+
InvitationExpiresAt: types.StringPointerValue(conversion.TimePtrToStringPtr(cloudUser.InvitationExpiresAt)),
100+
InviterUsername: types.StringPointerValue(cloudUser.InviterUsername),
101+
Country: types.StringPointerValue(cloudUser.Country),
102+
CreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(cloudUser.CreatedAt)),
103+
FirstName: types.StringPointerValue(cloudUser.FirstName),
104+
LastAuth: types.StringPointerValue(conversion.TimePtrToStringPtr(cloudUser.LastAuth)),
105+
LastName: types.StringPointerValue(cloudUser.LastName),
106+
MobileNumber: types.StringPointerValue(cloudUser.MobileNumber),
107+
}
108+
}
109+
return users
110+
}
111+
74112
func NewTFIPAddressesModel(ctx context.Context, ipAddresses *admin.GroupIPAddresses) (types.Object, diag.Diagnostics) {
75113
clusterIPs := []TFClusterIPsModel{}
76114
if ipAddresses != nil && ipAddresses.Services != nil {
@@ -94,8 +132,18 @@ func NewTFIPAddressesModel(ctx context.Context, ipAddresses *admin.GroupIPAddres
94132
return obj, diags
95133
}
96134

97-
func NewTFProjectResourceModel(ctx context.Context, projectRes *admin.Group, projectProps AdditionalProperties) (*TFProjectRSModel, diag.Diagnostics) {
98-
ipAddressesModel, diags := NewTFIPAddressesModel(ctx, projectProps.IPAddresses)
135+
func NewTFProjectResourceModel(ctx context.Context, projectRes *admin.Group, projectProps *AdditionalProperties) (*TFProjectRSModel, diag.Diagnostics) {
136+
var diags diag.Diagnostics
137+
if projectRes == nil {
138+
diags.AddError("Invalid Project Data", "Project data is nil and cannot be processed")
139+
return nil, diags
140+
}
141+
if projectProps == nil {
142+
diags.AddError("Invalid Project Properties", "Project properties data is nil and cannot be processed")
143+
return nil, diags
144+
}
145+
ipAddressesModel, ipDiags := NewTFIPAddressesModel(ctx, projectProps.IPAddresses)
146+
diags.Append(ipDiags...)
99147
if diags.HasError() {
100148
return nil, diags
101149
}
@@ -145,6 +193,9 @@ func newTFLimitsResourceModel(ctx context.Context, dataFederationLimits []admin.
145193
}
146194

147195
func newTFTeamsResourceModel(ctx context.Context, atlasTeams *admin.PaginatedTeamRole) types.Set {
196+
if atlasTeams == nil || atlasTeams.GetTotalCount() == 0 {
197+
return types.SetNull(TfTeamObjectType)
198+
}
148199
results := atlasTeams.GetResults()
149200
teams := make([]TFTeamModel, len(results))
150201
for i, atlasTeam := range results {

0 commit comments

Comments
 (0)