Skip to content

Commit bb955c5

Browse files
csanxCristina Sánchez Sánchez
andauthored
feat: Add new singular data source mongodbatlas_cloud_user_project_assignment (#3569)
* Add schema and Read() method * Add data source to provider * WIP * Tests * Changed tests * Changelog * Refactor * Typo * Fix * Updated tests * Fixed test * Added error --------- Co-authored-by: Cristina Sánchez Sánchez <[email protected]>
1 parent 781231d commit bb955c5

File tree

8 files changed

+161
-21
lines changed

8 files changed

+161
-21
lines changed

.changelog/3569.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-datasource
2+
data-source/mongodbatlas_cloud_user_project_assignment
3+
```

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ func (p *MongodbtlasProvider) DataSources(context.Context) []func() datasource.D
463463
resourcepolicy.DataSource,
464464
resourcepolicy.PluralDataSource,
465465
clouduserorgassignment.DataSource,
466+
clouduserprojectassignment.DataSource,
466467
clouduserteamassignment.DataSource,
467468
teamprojectassignment.DataSource,
468469
apikeyprojectassignment.DataSource,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package clouduserprojectassignment
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/datasource"
7+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
8+
)
9+
10+
var _ datasource.DataSource = &cloudUserProjectAssignmentDS{}
11+
var _ datasource.DataSourceWithConfigure = &cloudUserProjectAssignmentDS{}
12+
13+
func DataSource() datasource.DataSource {
14+
return &cloudUserProjectAssignmentDS{
15+
DSCommon: config.DSCommon{
16+
DataSourceName: resourceName,
17+
},
18+
}
19+
}
20+
21+
type cloudUserProjectAssignmentDS struct {
22+
config.DSCommon
23+
}
24+
25+
func (d *cloudUserProjectAssignmentDS) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
26+
resp.Schema = dataSourceSchema()
27+
}
28+
29+
func (d *cloudUserProjectAssignmentDS) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
30+
var state TFModel
31+
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
32+
if resp.Diagnostics.HasError() {
33+
return
34+
}
35+
36+
connV2 := d.Client.AtlasV2
37+
projectID := state.ProjectId.ValueString()
38+
userID := state.UserId.ValueString()
39+
username := state.Username.ValueString()
40+
41+
if username == "" && userID == "" {
42+
resp.Diagnostics.AddError("invalid configuration", "either username or user_id must be provided")
43+
return
44+
}
45+
userResp, err := fetchProjectUser(ctx, connV2, projectID, userID, username)
46+
if err != nil {
47+
resp.Diagnostics.AddError(errorReadingUser, err.Error())
48+
return
49+
}
50+
if userResp == nil {
51+
resp.Diagnostics.AddError("resource not found", "no user found with the specified identifier")
52+
return
53+
}
54+
55+
newCloudUserProjectAssignmentModel, diags := NewTFModel(ctx, projectID, userResp)
56+
if diags.HasError() {
57+
resp.Diagnostics.Append(diags...)
58+
return
59+
}
60+
resp.Diagnostics.Append(resp.State.Set(ctx, newCloudUserProjectAssignmentModel)...)
61+
}

internal/service/clouduserprojectassignment/model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func NewTFModel(ctx context.Context, projectID string, apiResp *admin.GroupUserR
2424
CreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.CreatedAt)),
2525
FirstName: types.StringPointerValue(apiResp.FirstName),
2626
ProjectId: types.StringValue(projectID),
27-
UserId: types.StringValue(apiResp.GetId()),
27+
UserId: types.StringValue(apiResp.Id),
2828
InvitationCreatedAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.InvitationCreatedAt)),
2929
InvitationExpiresAt: types.StringPointerValue(conversion.TimePtrToStringPtr(apiResp.InvitationExpiresAt)),
3030
InviterUsername: types.StringPointerValue(apiResp.InviterUsername),

internal/service/clouduserprojectassignment/resource.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ import (
1717
)
1818

1919
const (
20-
resourceName = "cloud_user_project_assignment"
21-
errorReadingUser = "Error retrieving project users"
20+
resourceName = "cloud_user_project_assignment"
21+
errorReadingByUserID = "Error getting project users by user_id"
22+
errorReadingByUsername = "Error getting project users by username"
23+
invalidImportID = "Invalid import ID format"
24+
errorReadingUser = "Error retrieving project users"
2225
)
2326

2427
var _ resource.ResourceWithConfigure = &rs{}
@@ -37,7 +40,7 @@ type rs struct {
3740
}
3841

3942
func (r *rs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
40-
resp.Schema = resourceSchema(ctx)
43+
resp.Schema = resourceSchema()
4144
conversion.UpdateSchemaDescription(&resp.Schema)
4245
}
4346

@@ -232,7 +235,7 @@ func (r *rs) ImportState(ctx context.Context, req resource.ImportStateRequest, r
232235
importID := req.ID
233236
ok, parts := conversion.ImportSplit(req.ID, 2)
234237
if !ok {
235-
resp.Diagnostics.AddError("invalid import ID format", "expected 'project_id/user_id' or 'project_id/username', got: "+importID)
238+
resp.Diagnostics.AddError(invalidImportID, "expected 'project_id/user_id' or 'project_id/username', got: "+importID)
236239
return
237240
}
238241
projectID, user := parts[0], parts[1]

internal/service/clouduserprojectassignment/resource_test.go

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"regexp"
78
"strings"
89
"testing"
910

@@ -14,19 +15,25 @@ import (
1415

1516
var resourceNamePending = "mongodbatlas_cloud_user_project_assignment.test_pending"
1617
var resourceNameActive = "mongodbatlas_cloud_user_project_assignment.test_active"
18+
var DSNameUsername = "data.mongodbatlas_cloud_user_project_assignment.test_username"
19+
var DSNameUserID = "data.mongodbatlas_cloud_user_project_assignment.test_user_id"
1720

18-
func TestAccCloudUserProjectAssignmentRS_basic(t *testing.T) {
21+
func TestAccCloudUserProjectAssignment_basic(t *testing.T) {
1922
resource.ParallelTest(t, *basicTestCase(t))
2023
}
2124

25+
func TestAccCloudUserProjectAssignmentDS_error(t *testing.T) {
26+
resource.ParallelTest(t, *errorTestCase(t))
27+
}
28+
2229
func basicTestCase(t *testing.T) *resource.TestCase {
2330
t.Helper()
2431

2532
// Use MONGODB_ATLAS_USERNAME_2 to avoid USER_ALREADY_IN_GROUP.
2633
// The default MONGODB_ATLAS_USERNAME (Org Owner) is auto-assigned if no ProjectOwner is set.
2734
activeUsername := os.Getenv("MONGODB_ATLAS_USERNAME_2")
2835
pendingUsername := acc.RandomEmail()
29-
projectName := acc.RandomName()
36+
projectID := acc.ProjectIDExecution(t)
3037
orgID := os.Getenv("MONGODB_ATLAS_ORG_ID")
3138
roles := []string{"GROUP_OWNER", "GROUP_CLUSTER_MANAGER"}
3239
updatedRoles := []string{"GROUP_OWNER", "GROUP_SEARCH_INDEX_EDITOR", "GROUP_READ_ONLY"}
@@ -37,12 +44,12 @@ func basicTestCase(t *testing.T) *resource.TestCase {
3744
CheckDestroy: checkDestroy,
3845
Steps: []resource.TestStep{
3946
{
40-
Config: configBasic(orgID, pendingUsername, activeUsername, projectName, roles),
41-
Check: checks(pendingUsername, activeUsername, projectName, roles),
47+
Config: configBasic(orgID, pendingUsername, activeUsername, projectID, roles),
48+
Check: checks(pendingUsername, activeUsername, projectID, roles),
4249
},
4350
{
44-
Config: configBasic(orgID, pendingUsername, activeUsername, projectName, updatedRoles),
45-
Check: checks(pendingUsername, activeUsername, projectName, updatedRoles),
51+
Config: configBasic(orgID, pendingUsername, activeUsername, projectID, updatedRoles),
52+
Check: checks(pendingUsername, activeUsername, projectID, updatedRoles),
4653
},
4754
{
4855
ResourceName: resourceNamePending,
@@ -96,7 +103,31 @@ func basicTestCase(t *testing.T) *resource.TestCase {
96103
}
97104
}
98105

99-
func configBasic(orgID, pendingUsername, activeUsername, projectName string, roles []string) string {
106+
func errorTestCase(t *testing.T) *resource.TestCase {
107+
t.Helper()
108+
orgID := os.Getenv("MONGODB_ATLAS_ORG_ID")
109+
projectID := acc.ProjectIDExecution(t)
110+
return &resource.TestCase{
111+
PreCheck: func() { acc.PreCheckBasic(t) },
112+
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
113+
Steps: []resource.TestStep{
114+
{
115+
Config: configError(orgID, projectID),
116+
ExpectError: regexp.MustCompile("either username or user_id must be provided"),
117+
},
118+
},
119+
}
120+
}
121+
122+
func configError(orgID, projectID string) string {
123+
return fmt.Sprintf(`
124+
data "mongodbatlas_cloud_user_project_assignment" "test" {
125+
project_id = %[1]q
126+
}
127+
`, projectID, orgID)
128+
}
129+
130+
func configBasic(orgID, pendingUsername, activeUsername, projectID string, roles []string) string {
100131
rolesStr := `"` + strings.Join(roles, `", "`) + `"`
101132
return fmt.Sprintf(`
102133
resource "mongodbatlas_project" "test" {
@@ -106,26 +137,41 @@ func configBasic(orgID, pendingUsername, activeUsername, projectName string, rol
106137
107138
resource "mongodbatlas_cloud_user_project_assignment" "test_pending" {
108139
username = %[3]q
109-
project_id = mongodbatlas_project.test.id
140+
project_id = %[1]q
110141
roles = [%[5]s]
111142
}
112143
113144
resource "mongodbatlas_cloud_user_project_assignment" "test_active" {
114145
username = %[4]q
115-
project_id = mongodbatlas_project.test.id
146+
project_id = %[1]q
116147
roles = [%[5]s]
148+
}
149+
150+
data "mongodbatlas_cloud_user_project_assignment" "test_username" {
151+
project_id = %[1]q
152+
username = mongodbatlas_cloud_user_project_assignment.test_pending.username
153+
}
154+
155+
data "mongodbatlas_cloud_user_project_assignment" "test_user_id" {
156+
project_id = %[1]q
157+
user_id = mongodbatlas_cloud_user_project_assignment.test_pending.user_id
117158
}`,
118-
projectName, orgID, pendingUsername, activeUsername, rolesStr)
159+
projectID, orgID, pendingUsername, activeUsername, rolesStr)
119160
}
120161

121-
func checks(pendingUsername, activeUsername, projectName string, roles []string) resource.TestCheckFunc {
162+
func checks(pendingUsername, activeUsername, projectID string, roles []string) resource.TestCheckFunc {
122163
checkFuncs := []resource.TestCheckFunc{
123164
resource.TestCheckResourceAttr(resourceNamePending, "username", pendingUsername),
124-
resource.TestCheckResourceAttrSet(resourceNamePending, "project_id"),
165+
resource.TestCheckResourceAttr(resourceNamePending, "project_id", projectID),
125166
resource.TestCheckResourceAttr(resourceNamePending, "roles.#", fmt.Sprintf("%d", len(roles))),
126167
resource.TestCheckResourceAttr(resourceNameActive, "username", activeUsername),
127-
resource.TestCheckResourceAttrSet(resourceNameActive, "project_id"),
168+
resource.TestCheckResourceAttr(resourceNameActive, "project_id", projectID),
128169
resource.TestCheckResourceAttr(resourceNameActive, "roles.#", fmt.Sprintf("%d", len(roles))),
170+
resource.TestCheckResourceAttr(DSNameUserID, "username", pendingUsername),
171+
resource.TestCheckResourceAttrPair(DSNameUserID, "username", resourceNamePending, "username"),
172+
resource.TestCheckResourceAttrPair(DSNameUsername, "user_id", DSNameUserID, "user_id"),
173+
resource.TestCheckResourceAttrPair(DSNameUsername, "project_id", DSNameUserID, "project_id"),
174+
resource.TestCheckResourceAttrPair(DSNameUsername, "roles.#", DSNameUserID, "roles.#"),
129175
}
130176

131177
for _, role := range roles {
@@ -134,6 +180,7 @@ func checks(pendingUsername, activeUsername, projectName string, roles []string)
134180
resource.TestCheckTypeSetElemAttr(resourceNameActive, "roles.*", role),
135181
)
136182
}
183+
137184
return resource.ComposeAggregateTestCheckFunc(checkFuncs...)
138185
}
139186

internal/service/clouduserprojectassignment/resource_schema.go renamed to internal/service/clouduserprojectassignment/schema.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package clouduserprojectassignment
22

33
import (
4-
"context"
5-
64
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
5+
dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
76
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
87
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
98
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
109
"github.com/hashicorp/terraform-plugin-framework/types"
10+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
1111

1212
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1313
)
1414

15-
func resourceSchema(ctx context.Context) schema.Schema {
15+
func resourceSchema() schema.Schema {
1616
return schema.Schema{
1717
Attributes: map[string]schema.Attribute{
1818
"country": schema.StringAttribute{
@@ -106,6 +106,26 @@ func resourceSchema(ctx context.Context) schema.Schema {
106106
}
107107
}
108108

109+
func dataSourceSchema() dsschema.Schema {
110+
return conversion.DataSourceSchemaFromResource(resourceSchema(), &conversion.DataSourceSchemaRequest{
111+
RequiredFields: []string{"project_id"},
112+
OverridenFields: dataSourceOverridenFields(),
113+
})
114+
}
115+
116+
func dataSourceOverridenFields() map[string]dsschema.Attribute {
117+
return map[string]dsschema.Attribute{
118+
"user_id": dsschema.StringAttribute{
119+
Optional: true,
120+
MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the MongoDB Cloud user.",
121+
},
122+
"username": dsschema.StringAttribute{
123+
Optional: true,
124+
MarkdownDescription: "Email address that represents the username of the MongoDB Cloud user.",
125+
},
126+
}
127+
}
128+
109129
type TFModel struct {
110130
Country types.String `tfsdk:"country"`
111131
CreatedAt types.String `tfsdk:"created_at"`

internal/service/clouduserprojectassignment/tfplugingen/generator_config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ resources:
1515
path: /api/atlas/v2/groups/{groupId}/users/{userId}
1616
method: DELETE
1717

18+
data_sources:
19+
cloud_user_project_assignment:
20+
read:
21+
path: /api/atlas/v2/groups/{groupId}/users/{userId}
22+
method: GET

0 commit comments

Comments
 (0)