Skip to content

Commit 6df6ad6

Browse files
marcosumalantoli
andauthored
feat: set format for AWS region value in the provider definition (#1549)
* fixed region value when creating configuration for aws role authentication. * restore go.mod and go.sum. * separates logic into util func. * feat: adds more environment variables to be used when authenticating with an AWS Role (#1551) * set all possible default values taken from environment varialbes (if exist). * addresses comments. * feat: adds test coverage for AWS Role Authentication (#1552) * adds test coverage. * pre-check that regular credentials env variables are not set. * feat: adds test infrastructure to run acceptance tests on PRs (#1553) * adds test infra. * fix. * Update mongodbatlas/provider.go Co-authored-by: Leo Antoli <[email protected]> * Update mongodbatlas/provider.go Co-authored-by: Leo Antoli <[email protected]> * addresses comments. --------- Co-authored-by: Leo Antoli <[email protected]> --------- Co-authored-by: Leo Antoli <[email protected]>
1 parent f3fbacf commit 6df6ad6

File tree

8 files changed

+270
-13
lines changed

8 files changed

+270
-13
lines changed

.github/workflows/acceptance-tests.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ jobs:
3131
serverless: ${{ steps.filter.outputs.serverless }}
3232
network: ${{ steps.filter.outputs.network }}
3333
config: ${{ steps.filter.outputs.config }}
34+
assume_role: ${{ steps.filter.outputs.assume_role }}
3435
steps:
3536
- uses: actions/checkout@v4
3637
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || inputs.parent-event-name == 'release' }}
3738
- uses: dorny/paths-filter@v2
3839
id: filter
3940
with:
4041
filters: |
42+
assume_role:
43+
- 'mongodbatlas/**provider**.go'
4144
cluster_outage_simulation:
4245
- 'mongodbatlas/**cluster_outage_simulation**.go'
4346
advanced_cluster:
@@ -103,6 +106,28 @@ jobs:
103106
- 'mongodbatlas/resource_mongodbatlas_team*.go'
104107
- 'mongodbatlas/resource_mongodbatlas_third_party_integration*.go'
105108
109+
fetch-sts-assume-role-creds:
110+
runs-on: ubuntu-latest
111+
outputs:
112+
aws_access_key_id: ${{ steps.sts-assume-role-step.outputs.aws_access_key_id }}
113+
aws_secret_access_key: ${{ steps.sts-assume-role-step.outputs.aws_secret_access_key }}
114+
aws_session_token: ${{ steps.sts-assume-role-step.outputs.aws_session_token }}
115+
steps:
116+
- name: Checkout
117+
uses: actions/checkout@v4
118+
with:
119+
sparse-checkout: |
120+
scripts
121+
- id: sts-assume-role-step
122+
name: Generate STS Temporary credential for acceptance testing
123+
shell: bash
124+
env:
125+
AWS_REGION: ${{ vars.AWS_REGION }}
126+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
127+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
128+
ASSUME_ROLE_ARN: ${{ vars.ASSUME_ROLE_ARN }}
129+
run: bash ./scripts/generate-credentials-with-sts-assume-role.sh
130+
106131
cluster_outage_simulation:
107132
needs: [ change-detection ]
108133
if: ${{ needs.change-detection.outputs.cluster_outage_simulation == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event.label.name == 'run-testacc' || github.event.label.name == 'run-testacc-cluster-outage-simulation' || inputs.parent-event-name == 'release' }}
@@ -424,3 +449,33 @@ jobs:
424449
CI: true
425450
TEST_REGEX: "^TestAccConfig"
426451
run: make testacc
452+
453+
assume_role:
454+
needs: [ change-detection, fetch-sts-assume-role-creds]
455+
if: ${{ needs.change-detection.outputs.config == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event.label.name == 'run-testacc' || github.event.label.name == 'run-testacc-config' || inputs.parent-event-name == 'release' }}
456+
runs-on: ubuntu-latest
457+
steps:
458+
- name: Checkout
459+
uses: actions/checkout@v4
460+
- name: Set up Go
461+
uses: actions/setup-go@v4
462+
with:
463+
go-version-file: 'go.mod'
464+
- name: Acceptance Tests
465+
env:
466+
ASSUME_ROLE_ARN: ${{ vars.ASSUME_ROLE_ARN }}
467+
AWS_REGION: ${{ vars.AWS_REGION }}
468+
STS_ENDPOINT: ${{ vars.STS_ENDPOINT }}
469+
SECRET_NAME: ${{ vars.AWS_SECRET_NAME }}
470+
AWS_ACCESS_KEY_ID: ${{ needs.fetch-sts-assume-role-creds.outputs.AWS_ACCESS_KEY_ID }}
471+
AWS_SECRET_ACCESS_KEY: ${{ needs.fetch-sts-assume-role-creds.outputs.AWS_SECRET_ACCESS_KEY }}
472+
AWS_SESSION_TOKEN: ${{ needs.fetch-sts-assume-role-creds.outputs.AWS_SESSION_TOKEN }}
473+
MONGODB_ATLAS_ORG_ID: ${{ vars.MONGODB_ATLAS_ORG_ID_CLOUD_DEV }}
474+
MONGODB_ATLAS_BASE_URL: ${{ vars.MONGODB_ATLAS_BASE_URL }}
475+
ACCTEST_TIMEOUT: ${{ vars.ACCTEST_TIMEOUT }}
476+
TF_LOG: ${{ vars.LOG_LEVEL }}
477+
TF_ACC: 1
478+
PARALLEL_GO_TEST: 20
479+
CI: true
480+
TEST_REGEX: "^TestAccSTSAssumeRole"
481+
run: make testacc

mongodbatlas/fw_provider.go

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88

99
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
1010
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
11+
"github.com/hashicorp/terraform-plugin-framework/attr"
1112
"github.com/hashicorp/terraform-plugin-framework/datasource"
13+
"github.com/hashicorp/terraform-plugin-framework/diag"
1214
"github.com/hashicorp/terraform-plugin-framework/provider"
1315
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
1416
"github.com/hashicorp/terraform-plugin-framework/providerserver"
@@ -21,6 +23,7 @@ import (
2123
sdkv2schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2224

2325
cstmvalidator "github.com/mongodb/terraform-provider-mongodbatlas/mongodbatlas/framework/validator"
26+
"github.com/mongodb/terraform-provider-mongodbatlas/mongodbatlas/util"
2427
"github.com/mongodb/terraform-provider-mongodbatlas/version"
2528
)
2629

@@ -69,6 +72,18 @@ type tfAssumeRoleModel struct {
6972
SourceIdentity types.String `tfsdk:"source_identity"`
7073
}
7174

75+
var AssumeRoleType = types.ObjectType{AttrTypes: map[string]attr.Type{
76+
"policy_arns": types.SetType{ElemType: types.StringType},
77+
"transitive_tag_keys": types.SetType{ElemType: types.StringType},
78+
"tags": types.MapType{ElemType: types.StringType},
79+
"duration": types.StringType,
80+
"external_id": types.StringType,
81+
"policy": types.StringType,
82+
"role_arn": types.StringType,
83+
"session_name": types.StringType,
84+
"source_identity": types.StringType,
85+
}}
86+
7287
func (p *MongodbtlasProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
7388
resp.TypeName = "mongodbatlas"
7489
resp.Version = version.ProviderVersion
@@ -202,11 +217,7 @@ func (p *MongodbtlasProvider) Configure(ctx context.Context, req provider.Config
202217
return
203218
}
204219

205-
var assumeRoles []tfAssumeRoleModel
206-
data.AssumeRole.ElementsAs(ctx, &assumeRoles, true)
207-
awsRoleDefined := len(assumeRoles) > 0
208-
209-
data = setDefaultValuesWithValidations(&data, awsRoleDefined, resp)
220+
data = setDefaultValuesWithValidations(ctx, &data, resp)
210221
if resp.Diagnostics.HasError() {
211222
return
212223
}
@@ -218,10 +229,13 @@ func (p *MongodbtlasProvider) Configure(ctx context.Context, req provider.Config
218229
RealmBaseURL: data.RealmBaseURL.ValueString(),
219230
}
220231

232+
var assumeRoles []tfAssumeRoleModel
233+
data.AssumeRole.ElementsAs(ctx, &assumeRoles, true)
234+
awsRoleDefined := len(assumeRoles) > 0
221235
if awsRoleDefined {
222236
config.AssumeRole = parseTfModel(ctx, &assumeRoles[0])
223237
secret := data.SecretName.ValueString()
224-
region := data.Region.ValueString()
238+
region := util.MongoDBRegionToAWSRegion(data.Region.ValueString())
225239
awsAccessKeyID := data.AwsAccessKeyID.ValueString()
226240
awsSecretAccessKey := data.AwsSecretAccessKeyID.ValueString()
227241
awsSessionToken := data.AwsSessionToken.ValueString()
@@ -281,7 +295,7 @@ func parseTfModel(ctx context.Context, tfAssumeRoleModel *tfAssumeRoleModel) *As
281295

282296
const MongodbGovCloudURL = "https://cloud.mongodbgov.com"
283297

284-
func setDefaultValuesWithValidations(data *tfMongodbAtlasProviderModel, awsRoleDefined bool, resp *provider.ConfigureResponse) tfMongodbAtlasProviderModel {
298+
func setDefaultValuesWithValidations(ctx context.Context, data *tfMongodbAtlasProviderModel, resp *provider.ConfigureResponse) tfMongodbAtlasProviderModel {
285299
if mongodbgovCloud := data.IsMongodbGovCloud.ValueBool(); mongodbgovCloud {
286300
data.BaseURL = types.StringValue(MongodbGovCloudURL)
287301
}
@@ -292,6 +306,31 @@ func setDefaultValuesWithValidations(data *tfMongodbAtlasProviderModel, awsRoleD
292306
}, "").(string))
293307
}
294308

309+
awsRoleDefined := false
310+
if len(data.AssumeRole.Elements()) == 0 {
311+
assumeRoleArn := MultiEnvDefaultFunc([]string{
312+
"ASSUME_ROLE_ARN",
313+
"TF_VAR_ASSUME_ROLE_ARN",
314+
}, "").(string)
315+
if assumeRoleArn != "" {
316+
awsRoleDefined = true
317+
var diags diag.Diagnostics
318+
data.AssumeRole, diags = types.ListValueFrom(ctx, AssumeRoleType, []tfAssumeRoleModel{
319+
{
320+
Tags: types.MapNull(types.StringType),
321+
PolicyARNs: types.SetNull(types.StringType),
322+
TransitiveTagKeys: types.SetNull(types.StringType),
323+
RoleARN: types.StringValue(assumeRoleArn),
324+
},
325+
})
326+
if diags.HasError() {
327+
resp.Diagnostics.Append(diags...)
328+
}
329+
}
330+
} else {
331+
awsRoleDefined = true
332+
}
333+
295334
if data.PublicKey.ValueString() == "" {
296335
data.PublicKey = types.StringValue(MultiEnvDefaultFunc([]string{
297336
"MONGODB_ATLAS_PUBLIC_KEY",
@@ -353,6 +392,13 @@ func setDefaultValuesWithValidations(data *tfMongodbAtlasProviderModel, awsRoleD
353392
}, "").(string))
354393
}
355394

395+
if data.SecretName.ValueString() == "" {
396+
data.SecretName = types.StringValue(MultiEnvDefaultFunc([]string{
397+
"SECRET_NAME",
398+
"TF_VAR_SECRET_NAME",
399+
}, "").(string))
400+
}
401+
356402
return *data
357403
}
358404

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package mongodbatlas
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
8+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
9+
matlas "go.mongodb.org/atlas/mongodbatlas"
10+
)
11+
12+
func TestAccSTSAssumeRole_basic(t *testing.T) {
13+
var (
14+
resourceName = "mongodbatlas_project.test"
15+
projectName = acctest.RandomWithPrefix("test-acc")
16+
orgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
17+
clusterCount = "0"
18+
)
19+
resource.ParallelTest(t, resource.TestCase{
20+
PreCheck: func() { testCheckSTSAssumeRole(t); testCheckRegularCredsAreEmpty(t) },
21+
ProtoV6ProviderFactories: testAccProviderV6Factories,
22+
CheckDestroy: testAccCheckMongoDBAtlasProjectDestroy,
23+
Steps: []resource.TestStep{
24+
{
25+
Config: testAccMongoDBAtlasProjectConfig(projectName, orgID,
26+
[]*matlas.ProjectTeam{},
27+
),
28+
Check: resource.ComposeTestCheckFunc(
29+
resource.TestCheckResourceAttr(resourceName, "name", projectName),
30+
resource.TestCheckResourceAttr(resourceName, "org_id", orgID),
31+
resource.TestCheckResourceAttr(resourceName, "cluster_count", clusterCount),
32+
resource.TestCheckResourceAttr(resourceName, "teams.#", "0"),
33+
),
34+
},
35+
{
36+
ResourceName: resourceName,
37+
ImportStateIdFunc: testAccCheckMongoDBAtlasProjectImportStateIDFunc(resourceName),
38+
ImportState: true,
39+
ImportStateVerify: true,
40+
ImportStateVerifyIgnore: []string{"with_default_alerts_settings"},
41+
},
42+
},
43+
})
44+
}

mongodbatlas/provider.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
2626
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2727
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
28+
"github.com/mongodb/terraform-provider-mongodbatlas/mongodbatlas/util"
2829
"github.com/mwielbut/pointy"
2930
"github.com/spf13/cast"
3031
"github.com/zclconf/go-cty/cty"
@@ -254,9 +255,7 @@ func addBetaFeatures(provider *schema.Provider) {
254255
}
255256

256257
func providerConfigure(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) {
257-
assumeRoleValue, ok := d.GetOk("assume_role")
258-
awsRoleDefined := ok && len(assumeRoleValue.([]any)) > 0 && assumeRoleValue.([]any)[0] != nil
259-
diagnostics := setDefaultsAndValidations(d, awsRoleDefined)
258+
diagnostics := setDefaultsAndValidations(d)
260259
if diagnostics.HasError() {
261260
return nil, diagnostics
262261
}
@@ -268,10 +267,12 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (any, diag.D
268267
RealmBaseURL: d.Get("realm_base_url").(string),
269268
}
270269

270+
assumeRoleValue, ok := d.GetOk("assume_role")
271+
awsRoleDefined := ok && len(assumeRoleValue.([]any)) > 0 && assumeRoleValue.([]any)[0] != nil
271272
if awsRoleDefined {
272273
config.AssumeRole = expandAssumeRole(assumeRoleValue.([]any)[0].(map[string]any))
273274
secret := d.Get("secret_name").(string)
274-
region := d.Get("region").(string)
275+
region := util.MongoDBRegionToAWSRegion(d.Get("region").(string))
275276
awsAccessKeyID := d.Get("aws_access_key_id").(string)
276277
awsSecretAccessKey := d.Get("aws_secret_access_key").(string)
277278
awsSessionToken := d.Get("aws_session_token").(string)
@@ -290,7 +291,7 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (any, diag.D
290291
return client, diagnostics
291292
}
292293

293-
func setDefaultsAndValidations(d *schema.ResourceData, awsRoleDefined bool) diag.Diagnostics {
294+
func setDefaultsAndValidations(d *schema.ResourceData) diag.Diagnostics {
294295
diagnostics := []diag.Diagnostic{}
295296

296297
mongodbgovCloud := pointy.Bool(d.Get("is_mongodbgov_cloud").(bool))
@@ -307,6 +308,23 @@ func setDefaultsAndValidations(d *schema.ResourceData, awsRoleDefined bool) diag
307308
return append(diagnostics, diag.FromErr(err)...)
308309
}
309310

311+
awsRoleDefined := false
312+
assumeRoles := d.Get("assume_role").([]any)
313+
if len(assumeRoles) == 0 {
314+
roleArn := MultiEnvDefaultFunc([]string{
315+
"ASSUME_ROLE_ARN",
316+
"TF_VAR_ASSUME_ROLE_ARN",
317+
}, "").(string)
318+
if roleArn != "" {
319+
awsRoleDefined = true
320+
if err := d.Set("assume_role", []map[string]any{{"role_arn": roleArn}}); err != nil {
321+
return append(diagnostics, diag.FromErr(err)...)
322+
}
323+
}
324+
} else {
325+
awsRoleDefined = true
326+
}
327+
310328
if err := setValueFromConfigOrEnv(d, "public_key", []string{
311329
"MONGODB_ATLAS_PUBLIC_KEY",
312330
"MCLI_PUBLIC_API_KEY",
@@ -362,6 +380,13 @@ func setDefaultsAndValidations(d *schema.ResourceData, awsRoleDefined bool) diag
362380
return append(diagnostics, diag.FromErr(err)...)
363381
}
364382

383+
if err := setValueFromConfigOrEnv(d, "secret_name", []string{
384+
"SECRET_NAME",
385+
"TF_VAR_SECRET_NAME",
386+
}); err != nil {
387+
return append(diagnostics, diag.FromErr(err)...)
388+
}
389+
365390
if err := setValueFromConfigOrEnv(d, "aws_session_token", []string{
366391
"AWS_SESSION_TOKEN",
367392
"TF_VAR_AWS_SESSION_TOKEN",

mongodbatlas/provider_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,36 @@ func testCheckAwsEnv(tb testing.TB) {
159159
}
160160
}
161161

162+
func testCheckRegularCredsAreEmpty(tb testing.TB) {
163+
if os.Getenv("MONGODB_ATLAS_PUBLIC_KEY") != "" || os.Getenv("MONGODB_ATLAS_PRIVATE_KEY") != "" {
164+
tb.Fatal(`"MONGODB_ATLAS_PUBLIC_KEY" and "MONGODB_ATLAS_PRIVATE_KEY" are defined in this test and they should not.`)
165+
}
166+
}
167+
168+
func testCheckSTSAssumeRole(tb testing.TB) {
169+
if os.Getenv("AWS_REGION") == "" {
170+
tb.Fatal(`'AWS_REGION' must be set for acceptance testing with STS Assume Role.`)
171+
}
172+
if os.Getenv("STS_ENDPOINT") == "" {
173+
tb.Fatal(`'STS_ENDPOINT' must be set for acceptance testing with STS Assume Role.`)
174+
}
175+
if os.Getenv("ASSUME_ROLE_ARN") == "" {
176+
tb.Fatal(`'ASSUME_ROLE_ARN' must be set for acceptance testing with STS Assume Role.`)
177+
}
178+
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
179+
tb.Fatal(`'AWS_ACCESS_KEY_ID' must be set for acceptance testing with STS Assume Role.`)
180+
}
181+
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
182+
tb.Fatal(`'AWS_SECRET_ACCESS_KEY' must be set for acceptance testing with STS Assume Role.`)
183+
}
184+
if os.Getenv("AWS_SESSION_TOKEN") == "" {
185+
tb.Fatal(`'AWS_SESSION_TOKEN' must be set for acceptance testing with STS Assume Role.`)
186+
}
187+
if os.Getenv("SECRET_NAME") == "" {
188+
tb.Fatal(`'SECRET_NAME' must be set for acceptance testing with STS Assume Role.`)
189+
}
190+
}
191+
162192
func TestEncodeDecodeID(t *testing.T) {
163193
expected := map[string]string{
164194
"project_id": "5cf5a45a9ccf6400e60981b6",

mongodbatlas/util/type_conversion.go

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

3-
import "time"
3+
import (
4+
"strings"
5+
"time"
6+
)
47

58
func SafeString(s *string) string {
69
if s != nil {
@@ -48,3 +51,8 @@ func IntPtrToInt64Ptr(i *int) *int64 {
4851
func IsStringPresent(strPtr *string) bool {
4952
return strPtr != nil && len(*strPtr) > 0
5053
}
54+
55+
// MongoDBRegionToAWSRegion converts region in US_EAST_1-like format to us-east-1-like
56+
func MongoDBRegionToAWSRegion(region string) string {
57+
return strings.ReplaceAll(strings.ToLower(region), "_", "-")
58+
}

0 commit comments

Comments
 (0)