Skip to content

Commit bfcb81c

Browse files
authored
feat: Adds new mongodbatlas_cloud_user_org_assignment resource (#3486)
1 parent 5d46115 commit bfcb81c

File tree

19 files changed

+1347
-17
lines changed

19 files changed

+1347
-17
lines changed

.changelog/3486.txt

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

.changelog/3491.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_org_assignment
3+
```

.github/workflows/acceptance-tests-runner.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ jobs:
244244
autogen: ${{ steps.filter.outputs.autogen == 'true' || env.mustTrigger == 'true' }}
245245
backup: ${{ steps.filter.outputs.backup == 'true' || env.mustTrigger == 'true' }}
246246
control_plane_ip_addresses: ${{ steps.filter.outputs.control_plane_ip_addresses == 'true' || env.mustTrigger == 'true' }}
247+
cloud_user: ${{ steps.filter.outputs.cloud_user == 'true' || env.mustTrigger == 'true' }}
247248
cluster: ${{ steps.filter.outputs.cluster == 'true' || env.mustTrigger == 'true' }}
248249
cluster_outage_simulation: ${{ steps.filter.outputs.cluster_outage_simulation == 'true' || env.mustTrigger == 'true' }}
249250
config: ${{ steps.filter.outputs.config == 'true' || env.mustTrigger == 'true' }}
@@ -299,6 +300,8 @@ jobs:
299300
- 'internal/service/onlinearchive/*.go'
300301
control_plane_ip_addresses:
301302
- 'internal/service/controlplaneipaddresses/*.go'
303+
cloud_user:
304+
- 'internal/service/clouduserorgassignment/*.go'
302305
cluster:
303306
- 'internal/service/cluster/*.go'
304307
cluster_outage_simulation:
@@ -589,6 +592,29 @@ jobs:
589592
MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }}
590593
ACCTEST_PACKAGES: ./internal/service/controlplaneipaddresses
591594
run: make testacc
595+
596+
cloud_user:
597+
needs: [ change-detection, get-provider-version ]
598+
if: ${{ needs.change-detection.outputs.cloud_user == 'true' || inputs.test_group == 'cloud_user' }}
599+
runs-on: ubuntu-latest
600+
permissions: {}
601+
steps:
602+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
603+
with:
604+
ref: ${{ inputs.ref || github.ref }}
605+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
606+
with:
607+
go-version-file: 'go.mod'
608+
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
609+
with:
610+
terraform_version: ${{ inputs.terraform_version }}
611+
terraform_wrapper: false
612+
- name: Acceptance Tests
613+
env:
614+
MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }}
615+
MONGODB_ATLAS_TEAMS_IDS: ${{ inputs.mongodb_atlas_teams_ids }}
616+
ACCTEST_PACKAGES: ./internal/service/clouduserorgassignment
617+
run: make testacc
592618

593619
cluster:
594620
needs: [ change-detection, get-provider-version ]

internal/common/conversion/collections.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package conversion
22

3-
import "reflect"
3+
import (
4+
"context"
5+
"reflect"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
410

511
// HasElementsSliceOrMap checks if param is a non-empty slice or map
612
func HasElementsSliceOrMap(value any) bool {
@@ -22,3 +28,11 @@ func ToAnySlicePointer(value *[]map[string]any) *[]any {
2228
}
2329
return &ret
2430
}
31+
32+
func TFSetValueOrNull[T any](ctx context.Context, ptr *[]T, elemType attr.Type) types.Set {
33+
if ptr == nil || len(*ptr) == 0 {
34+
return types.SetNull(elemType)
35+
}
36+
set, _ := types.SetValueFrom(ctx, elemType, *ptr)
37+
return set
38+
}

internal/common/conversion/collections_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package conversion_test
33
import (
44
"testing"
55

6-
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
6+
"github.com/hashicorp/terraform-plugin-framework/types"
77
"github.com/stretchr/testify/assert"
8+
9+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
810
)
911

1012
func TestHasElementsSliceOrMap(t *testing.T) {
@@ -61,3 +63,25 @@ func TestToAnySlicePointer(t *testing.T) {
6163
})
6264
}
6365
}
66+
67+
func TestTFSetValueOrNull(t *testing.T) {
68+
ctx := t.Context()
69+
70+
testCases := map[string]*[]string{
71+
"nil": nil,
72+
"empty": {},
73+
"populated": {"a", "b", "c"},
74+
}
75+
76+
for name, value := range testCases {
77+
t.Run(name, func(t *testing.T) {
78+
result := conversion.TFSetValueOrNull(ctx, value, types.StringType)
79+
if value == nil || len(*value) == 0 {
80+
assert.True(t, result.IsNull())
81+
} else {
82+
assert.False(t, result.IsNull())
83+
assert.Len(t, result.Elements(), len(*value))
84+
}
85+
})
86+
}
87+
}

internal/common/schemafunc/attr.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package schemafunc
2+
3+
import "github.com/hashicorp/terraform-plugin-go/tftypes"
4+
5+
func GetAttrFromStateObj[T any](rawState map[string]tftypes.Value, attrName string) *T {
6+
var ret *T
7+
if err := rawState[attrName].As(&ret); err != nil {
8+
return nil
9+
}
10+
return ret
11+
}

internal/provider/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/alertconfiguration"
3030
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/apikeyprojectassignment"
3131
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/atlasuser"
32+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/clouduserorgassignment"
3233
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/controlplaneipaddresses"
3334
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/databaseuser"
3435
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/encryptionatrest"
@@ -457,6 +458,7 @@ func (p *MongodbtlasProvider) DataSources(context.Context) []func() datasource.D
457458
flexrestorejob.PluralDataSource,
458459
resourcepolicy.DataSource,
459460
resourcepolicy.PluralDataSource,
461+
clouduserorgassignment.DataSource,
460462
apikeyprojectassignment.DataSource,
461463
apikeyprojectassignment.PluralDataSource,
462464
}
@@ -483,6 +485,7 @@ func (p *MongodbtlasProvider) Resources(context.Context) []func() resource.Resou
483485
streamprivatelinkendpoint.Resource,
484486
flexcluster.Resource,
485487
resourcepolicy.Resource,
488+
clouduserorgassignment.Resource,
486489
apikeyprojectassignment.Resource,
487490
}
488491
if config.PreviewProviderV2AdvancedCluster() {

internal/service/advancedclustertpf/move_upgrade_state.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/hashicorp/terraform-plugin-go/tftypes"
1919

2020
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
21+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/schemafunc"
2122
)
2223

2324
// MoveState is used with moved block to upgrade from cluster to adv_cluster
@@ -112,17 +113,9 @@ func setStateResponse(ctx context.Context, diags *diag.Diagnostics, stateIn *tfp
112113
diags.Append(stateOut.Set(ctx, model)...)
113114
}
114115

115-
func getAttrFromStateObj[T any](rawState map[string]tftypes.Value, attrName string) *T {
116-
var ret *T
117-
if err := rawState[attrName].As(&ret); err != nil {
118-
return nil
119-
}
120-
return ret
121-
}
122-
123116
func getProjectIDNameFromStateObj(diags *diag.Diagnostics, stateObj map[string]tftypes.Value) (projectID, name *string) {
124-
projectID = getAttrFromStateObj[string](stateObj, "project_id")
125-
name = getAttrFromStateObj[string](stateObj, "name")
117+
projectID = schemafunc.GetAttrFromStateObj[string](stateObj, "project_id")
118+
name = schemafunc.GetAttrFromStateObj[string](stateObj, "name")
126119
if !conversion.IsStringPresent(projectID) || !conversion.IsStringPresent(name) {
127120
diags.AddError("Unable to read project_id or name from state", fmt.Sprintf("project_id: %s, name: %s",
128121
conversion.SafeString(projectID), conversion.SafeString(name)))
@@ -138,13 +131,13 @@ func getTimeoutFromStateObj(stateObj map[string]tftypes.Value) timeouts.Value {
138131
"delete": types.StringType,
139132
}
140133
nullObj := timeouts.Value{Object: types.ObjectNull(attrTypes)}
141-
timeoutState := getAttrFromStateObj[map[string]tftypes.Value](stateObj, "timeouts")
134+
timeoutState := schemafunc.GetAttrFromStateObj[map[string]tftypes.Value](stateObj, "timeouts")
142135
if timeoutState == nil {
143136
return nullObj
144137
}
145138
timeoutMap := make(map[string]attr.Value)
146139
for action := range attrTypes {
147-
actionTimeout := getAttrFromStateObj[string](*timeoutState, action)
140+
actionTimeout := schemafunc.GetAttrFromStateObj[string](*timeoutState, action)
148141
if actionTimeout == nil {
149142
timeoutMap[action] = types.StringNull()
150143
} else {
@@ -159,16 +152,16 @@ func getTimeoutFromStateObj(stateObj map[string]tftypes.Value) timeouts.Value {
159152
}
160153

161154
func setOptionalModelAttrs(stateObj map[string]tftypes.Value, model *TFModel) {
162-
if retainBackupsEnabled := getAttrFromStateObj[bool](stateObj, "retain_backups_enabled"); retainBackupsEnabled != nil {
155+
if retainBackupsEnabled := schemafunc.GetAttrFromStateObj[bool](stateObj, "retain_backups_enabled"); retainBackupsEnabled != nil {
163156
model.RetainBackupsEnabled = types.BoolPointerValue(retainBackupsEnabled)
164157
}
165-
if mongoDBMajorVersion := getAttrFromStateObj[string](stateObj, "mongo_db_major_version"); mongoDBMajorVersion != nil {
158+
if mongoDBMajorVersion := schemafunc.GetAttrFromStateObj[string](stateObj, "mongo_db_major_version"); mongoDBMajorVersion != nil {
166159
model.MongoDBMajorVersion = types.StringPointerValue(mongoDBMajorVersion)
167160
}
168161
}
169162

170163
func setReplicationSpecNumShardsAttr(ctx context.Context, stateObj map[string]tftypes.Value, model *TFModel) {
171-
specsVal := getAttrFromStateObj[[]tftypes.Value](stateObj, "replication_specs")
164+
specsVal := schemafunc.GetAttrFromStateObj[[]tftypes.Value](stateObj, "replication_specs")
172165
if specsVal == nil {
173166
return
174167
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package clouduserorgassignment
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"go.mongodb.org/atlas-sdk/v20250312005/admin"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/datasource"
10+
11+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
12+
)
13+
14+
var _ datasource.DataSource = &cloudUserOrgAssignmentDS{}
15+
var _ datasource.DataSourceWithConfigure = &cloudUserOrgAssignmentDS{}
16+
17+
func DataSource() datasource.DataSource {
18+
return &cloudUserOrgAssignmentDS{
19+
DSCommon: config.DSCommon{
20+
DataSourceName: resourceName,
21+
},
22+
}
23+
}
24+
25+
type cloudUserOrgAssignmentDS struct {
26+
config.DSCommon
27+
}
28+
29+
func (d *cloudUserOrgAssignmentDS) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
30+
resp.Schema = dataSourceSchema()
31+
}
32+
33+
func (d *cloudUserOrgAssignmentDS) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
34+
var cloudUserOrgAssignmentConfig TFModel
35+
resp.Diagnostics.Append(req.Config.Get(ctx, &cloudUserOrgAssignmentConfig)...)
36+
if resp.Diagnostics.HasError() {
37+
return
38+
}
39+
40+
connV2 := d.Client.AtlasV2
41+
orgID := cloudUserOrgAssignmentConfig.OrgId.ValueString()
42+
username := cloudUserOrgAssignmentConfig.Username.ValueString()
43+
userID := cloudUserOrgAssignmentConfig.UserId.ValueString()
44+
45+
if username == "" && userID == "" {
46+
resp.Diagnostics.AddError("invalid configuration", "either username or user_id must be provided")
47+
return
48+
}
49+
50+
var orgUser *admin.OrgUserResponse
51+
var err error
52+
53+
if userID != "" {
54+
orgUser, _, err = connV2.MongoDBCloudUsersApi.GetOrganizationUser(ctx, orgID, userID).Execute()
55+
if err != nil {
56+
resp.Diagnostics.AddError(fmt.Sprintf("error retrieving resource by user_id: %s", userID), err.Error())
57+
return
58+
}
59+
} else {
60+
params := &admin.ListOrganizationUsersApiParams{
61+
OrgId: orgID,
62+
Username: &username,
63+
}
64+
usersResp, _, err := connV2.MongoDBCloudUsersApi.ListOrganizationUsersWithParams(ctx, params).Execute()
65+
if err != nil {
66+
resp.Diagnostics.AddError(fmt.Sprintf("error retrieving resource by username: %s", username), err.Error())
67+
return
68+
}
69+
70+
if usersResp == nil || usersResp.Results == nil || len(*usersResp.Results) == 0 {
71+
resp.Diagnostics.AddError("resource not found", "no user found with the specified username")
72+
return
73+
}
74+
75+
orgUser = &(*usersResp.Results)[0]
76+
}
77+
78+
tfModel, diags := NewTFModel(ctx, orgUser, cloudUserOrgAssignmentConfig.OrgId.ValueString())
79+
resp.Diagnostics.Append(diags...)
80+
if resp.Diagnostics.HasError() {
81+
return
82+
}
83+
84+
resp.Diagnostics.Append(resp.State.Set(ctx, tfModel)...)
85+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package clouduserorgassignment_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc"
8+
)
9+
10+
func TestMain(m *testing.M) {
11+
cleanup := acc.SetupSharedResources()
12+
exitCode := m.Run()
13+
cleanup()
14+
os.Exit(exitCode)
15+
}

0 commit comments

Comments
 (0)