Skip to content

Commit 308f682

Browse files
authored
test: Ensures project withDefaultAlertsSettings works with import and introduce create_only plan modifier (#3105)
* fix: Sets default value for WithDefaultAlertsSettings during state import * chore: Adds release note * fix: Removes 'with_default_alerts_settings' from ImportStateVerifyIgnore in project tests when it is not set to `false` in earlier steps * doc: update changelog message * doc: Add back reference based on comments * revert changes of old implementation * refactor: Introduce Modifier interface and enhance non-updatable attribute handling to support multiple planmodifiers * refactor: Mark with_default_alerts_settings as NonUpdateable * refactor:Rename NonUpdatableAttributePlanModifier with CreateOnlyAttributePlanModifier in resource schemas * feat: Implement CreateOnlyAttributePlanModifier with default boolean support and refactor IsKnown utility function * chore: small fix to planModifier using state value when Unknown in the plan * test: Support testing plan error after importing * doc: Update error message in addDiags to clarify import restrictions * chore: revert old changes for advancedclustertpf * test: remove migration test util in favor of acc test step for empty plan check * revert old changes * doc: Updates docs with latest API docs * refactor: remove the withDefaultAlertSettings override for plan * refactor: Add create only attribute plan modifier for project_owner_id * chore: update related docs * refactor: Update error message to match original message * docs: Update description for 'with_default_alerts_settings' to clarify default behavior * refactor: Improve test function names and enhance default alert settings migration test cases * test: Update alert settings handling in TestAccProject_withFalseDefaultSettings * refactor: Rename Modifier interface to CreateOnlyModifier for clarity * docs: Add comments to clarify CreateOnlyAttributePlanModifierWithBoolDefault behavior
1 parent a16827e commit 308f682

File tree

10 files changed

+164
-63
lines changed

10 files changed

+164
-63
lines changed

docs/resources/project.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ resource "mongodbatlas_project" "test" {
5151
* `org_id` - (Required) The ID of the organization you want to create the project within.
5252
* `project_owner_id` - (Optional) Unique 24-hexadecimal digit string that identifies the Atlas user account to be granted the [Project Owner](https://docs.atlas.mongodb.com/reference/user-roles/#mongodb-authrole-Project-Owner) role on the specified project. If you set this parameter, it overrides the default value of the oldest [Organization Owner](https://docs.atlas.mongodb.com/reference/user-roles/#mongodb-authrole-Organization-Owner).
5353
* `tags` - (Optional) Map that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the project. See [below](#tags).
54-
* `with_default_alerts_settings` - (Optional) It allows users to disable the creation of the default alert settings. By default, this flag is set to true.
54+
* `with_default_alerts_settings` - (Optional) Flag that indicates whether to create the project with default alert settings. This setting cannot be updated after project creation. By default, this flag is set to true.
5555
* `is_collect_database_specifics_statistics_enabled` - (Optional) Flag that indicates whether to enable statistics in [cluster metrics](https://www.mongodb.com/docs/atlas/monitor-cluster-metrics/) collection for the project. By default, this flag is set to true.
5656
* `is_data_explorer_enabled` - (Optional) Flag that indicates whether to enable Data Explorer for the project. If enabled, you can query your database with an easy to use interface. When Data Explorer is disabled, you cannot terminate slow operations from the [Real-Time Performance Panel](https://www.mongodb.com/docs/atlas/real-time-performance-panel/#std-label-real-time-metrics-status-tab) or create indexes from the [Performance Advisor](https://www.mongodb.com/docs/atlas/performance-advisor/#std-label-performance-advisor). You can still view Performance Advisor recommendations, but you must create those indexes from [mongosh](https://www.mongodb.com/docs/mongodb-shell/#mongodb-binary-bin.mongosh). By default, this flag is set to true.
5757
* `is_extended_storage_sizes_enabled` - (Optional) Flag that indicates whether to enable extended storage sizes for the specified project. Clusters with extended storage sizes must be on AWS or GCP, and cannot span multiple regions. When extending storage size, initial syncs and cross-project snapshot restores will be slow. This setting should only be used as a measure of temporary relief; consider sharding if more storage is required.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package customplanmodifier
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/path"
10+
planmodifier "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
)
14+
15+
type CreateOnlyModifier interface {
16+
planmodifier.String
17+
planmodifier.Bool
18+
}
19+
20+
// CreateOnlyAttributePlanModifier returns a plan modifier that ensures that update operations fails when the attribute is changed.
21+
// This is useful for attributes only supported in create and not in update.
22+
// It shows a helpful error message helping the user to update their config to match the state.
23+
// Never use a schema.Default for create only attributes, instead use WithXXXDefault, the default will lead to plan changes that are not expected after import.
24+
// Implement CopyFromPlan if the attribute is not in the API Response.
25+
func CreateOnlyAttributePlanModifier() CreateOnlyModifier {
26+
return &createOnlyAttributePlanModifier{}
27+
}
28+
29+
// CreateOnlyAttributePlanModifierWithBoolDefault sets a default value on create operation that will show in the plan.
30+
// This avoids any custom logic in the resource "Create" handler.
31+
// On update the default has no impact and the UseStateForUnknown behavior is observed instead.
32+
// Always use Optional+Computed when using a default value.
33+
func CreateOnlyAttributePlanModifierWithBoolDefault(b bool) CreateOnlyModifier {
34+
return &createOnlyAttributePlanModifier{defaultBool: &b}
35+
}
36+
37+
type createOnlyAttributePlanModifier struct {
38+
defaultBool *bool
39+
}
40+
41+
func (d *createOnlyAttributePlanModifier) Description(ctx context.Context) string {
42+
return d.MarkdownDescription(ctx)
43+
}
44+
45+
func (d *createOnlyAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
46+
return "Ensures the update operation fails when updating an attribute. If the read after import don't equal the configuration value it will also raise an error."
47+
}
48+
49+
func isCreate(t *tfsdk.State) bool {
50+
return t.Raw.IsNull()
51+
}
52+
53+
func (d *createOnlyAttributePlanModifier) UseDefault() bool {
54+
return d.defaultBool != nil
55+
}
56+
57+
func (d *createOnlyAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
58+
if isCreate(&req.State) {
59+
if !IsKnown(req.PlanValue) && d.UseDefault() {
60+
resp.PlanValue = types.BoolPointerValue(d.defaultBool)
61+
}
62+
return
63+
}
64+
if isUpdated(req.StateValue, req.PlanValue) {
65+
d.addDiags(&resp.Diagnostics, req.Path, req.StateValue)
66+
}
67+
if !IsKnown(req.PlanValue) {
68+
resp.PlanValue = req.StateValue
69+
}
70+
}
71+
72+
func (d *createOnlyAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
73+
if isCreate(&req.State) {
74+
return
75+
}
76+
if isUpdated(req.StateValue, req.PlanValue) {
77+
d.addDiags(&resp.Diagnostics, req.Path, req.StateValue)
78+
}
79+
if !IsKnown(req.PlanValue) {
80+
resp.PlanValue = req.StateValue
81+
}
82+
}
83+
84+
func isUpdated(state, plan attr.Value) bool {
85+
if !IsKnown(plan) {
86+
return false
87+
}
88+
return !state.Equal(plan)
89+
}
90+
91+
func (d *createOnlyAttributePlanModifier) addDiags(diags *diag.Diagnostics, attrPath path.Path, stateValue attr.Value) {
92+
message := fmt.Sprintf("%s cannot be updated or set after import, remove it from the configuration or use the state value (see below).", attrPath)
93+
detail := fmt.Sprintf("The current state value is %s", stateValue)
94+
diags.AddError(message, detail)
95+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package customplanmodifier
2+
3+
import "github.com/hashicorp/terraform-plugin-framework/attr"
4+
5+
// IsKnown returns true if the attribute is known (not null or unknown). Note that !IsKnown is not the same as IsUnknown because null is !IsKnown but not IsUnknown.
6+
func IsKnown(attribute attr.Value) bool {
7+
return !attribute.IsNull() && !attribute.IsUnknown()
8+
}

internal/common/customplanmodifier/non_updatable.go

Lines changed: 0 additions & 36 deletions
This file was deleted.

internal/service/flexcluster/resource_schema.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ func ResourceSchema(ctx context.Context) schema.Schema {
2121
"project_id": schema.StringAttribute{
2222
Required: true,
2323
PlanModifiers: []planmodifier.String{
24-
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
24+
customplanmodifier.CreateOnlyAttributePlanModifier(),
2525
},
2626
MarkdownDescription: "Unique 24-hexadecimal character string that identifies the project.",
2727
},
2828
"name": schema.StringAttribute{
2929
Required: true,
3030
PlanModifiers: []planmodifier.String{
31-
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
31+
customplanmodifier.CreateOnlyAttributePlanModifier(),
3232
},
3333
MarkdownDescription: "Human-readable label that identifies the instance.",
3434
},
@@ -37,7 +37,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
3737
"backing_provider_name": schema.StringAttribute{
3838
Required: true,
3939
PlanModifiers: []planmodifier.String{
40-
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
40+
customplanmodifier.CreateOnlyAttributePlanModifier(),
4141
},
4242
MarkdownDescription: "Cloud service provider on which MongoDB Cloud provisioned the flex cluster.",
4343
},
@@ -58,7 +58,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
5858
"region_name": schema.StringAttribute{
5959
Required: true,
6060
PlanModifiers: []planmodifier.String{
61-
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
61+
customplanmodifier.CreateOnlyAttributePlanModifier(),
6262
},
6363
MarkdownDescription: "Human-readable label that identifies the geographic location of your MongoDB flex cluster. The region you choose can affect network latency for clients accessing your databases. For a complete list of region names, see [AWS](https://docs.atlas.mongodb.com/reference/amazon-aws/#std-label-amazon-aws), [GCP](https://docs.atlas.mongodb.com/reference/google-gcp/), and [Azure](https://docs.atlas.mongodb.com/reference/microsoft-azure/).",
6464
},

internal/service/project/resource_project.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,6 @@ func (r *projectRS) ImportState(ctx context.Context, req resource.ImportStateReq
335335
func updatePlanFromConfig(projectPlanNewPtr, projectPlan *TFProjectRSModel) {
336336
// we need to reset defaults from what was previously in the state:
337337
// https://discuss.hashicorp.com/t/boolean-optional-default-value-migration-to-framework/55932
338-
projectPlanNewPtr.WithDefaultAlertsSettings = projectPlan.WithDefaultAlertsSettings
339338
projectPlanNewPtr.ProjectOwnerID = projectPlan.ProjectOwnerID
340339
if projectPlan.Tags.IsNull() && len(projectPlanNewPtr.Tags.Elements()) == 0 {
341340
projectPlanNewPtr.Tags = types.MapNull(types.StringType)

internal/service/project/resource_project_migration_test.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,24 @@ func TestMigProject_withTeams(t *testing.T) {
7979
})
8080
}
8181

82-
func TestMigProject_withFalseDefaultSettings(t *testing.T) {
82+
// empty is tested by the TestMigProject_basic
83+
func TestMigProject_withFalseDefaultAlertSettings(t *testing.T) {
84+
resource.ParallelTest(t, *defaultAlertSettingsTestCase(t, false))
85+
}
86+
87+
func TestMigProject_withTrueDefaultAlertSettings(t *testing.T) {
88+
resource.ParallelTest(t, *defaultAlertSettingsTestCase(t, true))
89+
}
90+
91+
func defaultAlertSettingsTestCase(t *testing.T, withDefaultAlertSettings bool) *resource.TestCase {
92+
t.Helper()
8393
var (
8494
orgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
8595
projectOwnerID = os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID")
8696
projectName = acc.RandomProjectName()
87-
config = configWithFalseDefaultSettings(orgID, projectName, projectOwnerID)
97+
config = configWithDefaultAlertSettings(orgID, projectName, projectOwnerID, withDefaultAlertSettings)
8898
)
89-
90-
resource.Test(t, resource.TestCase{
99+
return &resource.TestCase{
91100
PreCheck: func() { mig.PreCheckBasicOwnerID(t) },
92101
CheckDestroy: acc.CheckDestroyProject,
93102
Steps: []resource.TestStep{
@@ -102,7 +111,7 @@ func TestMigProject_withFalseDefaultSettings(t *testing.T) {
102111
},
103112
mig.TestStepCheckEmptyPlan(config),
104113
},
105-
})
114+
}
106115
}
107116

108117
func TestMigProject_withLimits(t *testing.T) {

internal/service/project/resource_project_schema.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/hashicorp/terraform-plugin-framework/attr"
88
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
9-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
109
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
1110
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
1211
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
@@ -15,6 +14,7 @@ import (
1514
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1615
"github.com/hashicorp/terraform-plugin-framework/types"
1716
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant"
17+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier"
1818
"go.mongodb.org/atlas-sdk/v20250312006/admin"
1919
)
2020

@@ -50,13 +50,16 @@ func ResourceSchema(ctx context.Context) schema.Schema {
5050
},
5151
"project_owner_id": schema.StringAttribute{
5252
Optional: true,
53+
PlanModifiers: []planmodifier.String{
54+
customplanmodifier.CreateOnlyAttributePlanModifier(),
55+
},
5356
},
5457
"with_default_alerts_settings": schema.BoolAttribute{
5558
// Default values also must be Computed otherwise Terraform throws error:
56-
// Schema Using Attribute Default For Non-Computed Attribute
57-
Optional: true,
58-
Computed: true,
59-
Default: booldefault.StaticBool(true),
59+
// Provider produced invalid plan: planned an invalid value for a non-computed attribute.
60+
Optional: true,
61+
Computed: true,
62+
PlanModifiers: []planmodifier.Bool{customplanmodifier.CreateOnlyAttributePlanModifierWithBoolDefault(true)},
6063
},
6164
"is_collect_database_specifics_statistics_enabled": schema.BoolAttribute{
6265
Computed: true,

internal/service/project/resource_project_test.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -647,9 +647,21 @@ func TestAccGovProject_withProjectOwner(t *testing.T) {
647647

648648
func TestAccProject_withFalseDefaultSettings(t *testing.T) {
649649
var (
650-
orgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
651-
projectOwnerID = os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID")
652-
projectName = acc.RandomProjectName()
650+
orgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
651+
projectOwnerID = os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID")
652+
projectName = acc.RandomProjectName()
653+
importResourceName = resourceName + "2"
654+
alertSettingsFalse = configWithDefaultAlertSettings(orgID, projectName, projectOwnerID, false)
655+
alertSettingsTrue = configWithDefaultAlertSettings(orgID, projectName, projectOwnerID, true)
656+
alertSettingsAbsent = configBasic(orgID, projectName, "", false, nil, nil)
657+
// To test plan behavior after import it is necessary to use a different resource name, otherwise we get:
658+
// Terraform is already managing a remote object for mongodbatlas_project.test. To import to this address you must first remove the existing object from the state.
659+
// This happens because `ImportStatePersist` uses the previous WorkingDirectory where the state from previous steps are saved
660+
// resource "mongodbatlas_project" "test" --> resource "mongodbatlas_project" "test2"
661+
alertSettingsFalseImport = strings.Replace(alertSettingsFalse, "test", "test2", 1)
662+
// Need BOTH mongodbatlas_project.test and mongodbatlas_project.test2, otherwise we get:
663+
// expected empty plan, but mongodbatlas_project.test has planned action(s): [delete]
664+
alertSettingsAbsentImport = alertSettingsFalse + strings.Replace(alertSettingsAbsent, "test", "test2", 1)
653665
)
654666

655667
resource.ParallelTest(t, resource.TestCase{
@@ -658,13 +670,25 @@ func TestAccProject_withFalseDefaultSettings(t *testing.T) {
658670
CheckDestroy: acc.CheckDestroyProject,
659671
Steps: []resource.TestStep{
660672
{
661-
Config: configWithFalseDefaultSettings(orgID, projectName, projectOwnerID),
673+
Config: alertSettingsFalse,
662674
Check: resource.ComposeAggregateTestCheckFunc(
663675
checkExists(resourceName),
664676
resource.TestCheckResourceAttr(resourceName, "name", projectName),
665677
resource.TestCheckResourceAttr(resourceName, "org_id", orgID),
666678
),
667679
},
680+
{
681+
Config: alertSettingsTrue,
682+
ExpectError: regexp.MustCompile("with_default_alerts_settings cannot be updated or set after import, remove it from the configuration or use the state value"),
683+
},
684+
{
685+
Config: alertSettingsFalseImport,
686+
ResourceName: importResourceName,
687+
ImportStateIdFunc: acc.ImportStateProjectIDFunc(resourceName),
688+
ImportState: true,
689+
ImportStatePersist: true, // save the state to use it in the next plan
690+
},
691+
acc.TestStepCheckEmptyPlan(alertSettingsAbsentImport),
668692
},
669693
})
670694
}
@@ -688,7 +712,7 @@ func TestAccProject_withUpdatedSettings(t *testing.T) {
688712
resource.TestCheckResourceAttr(resourceName, "name", projectName),
689713
resource.TestCheckResourceAttr(resourceName, "org_id", orgID),
690714
resource.TestCheckResourceAttr(resourceName, "project_owner_id", projectOwnerID),
691-
resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "false"),
715+
resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), // uses default value
692716
resource.TestCheckResourceAttr(resourceName, "is_collect_database_specifics_statistics_enabled", "false"),
693717
resource.TestCheckResourceAttr(resourceName, "is_data_explorer_enabled", "false"),
694718
resource.TestCheckResourceAttr(resourceName, "is_extended_storage_sizes_enabled", "false"),
@@ -701,7 +725,7 @@ func TestAccProject_withUpdatedSettings(t *testing.T) {
701725
Config: acc.ConfigProjectWithSettings(projectName, orgID, projectOwnerID, true),
702726
Check: resource.ComposeAggregateTestCheckFunc(
703727
checkExists(resourceName),
704-
resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"),
728+
resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), // uses default value
705729
resource.TestCheckResourceAttr(resourceName, "is_collect_database_specifics_statistics_enabled", "true"),
706730
resource.TestCheckResourceAttr(resourceName, "is_data_explorer_enabled", "true"),
707731
resource.TestCheckResourceAttr(resourceName, "is_extended_storage_sizes_enabled", "true"),
@@ -714,7 +738,7 @@ func TestAccProject_withUpdatedSettings(t *testing.T) {
714738
Config: acc.ConfigProjectWithSettings(projectName, orgID, projectOwnerID, false),
715739
Check: resource.ComposeAggregateTestCheckFunc(
716740
checkExists(resourceName),
717-
resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "false"),
741+
resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), // uses default value
718742
resource.TestCheckResourceAttr(resourceName, "is_collect_database_specifics_statistics_enabled", "false"),
719743
resource.TestCheckResourceAttr(resourceName, "is_data_explorer_enabled", "false"),
720744
resource.TestCheckResourceAttr(resourceName, "is_extended_storage_sizes_enabled", "false"),
@@ -1232,15 +1256,15 @@ func configGovWithOwner(orgID, projectName, projectOwnerID string) string {
12321256
`, orgID, projectName, projectOwnerID)
12331257
}
12341258

1235-
func configWithFalseDefaultSettings(orgID, projectName, projectOwnerID string) string {
1259+
func configWithDefaultAlertSettings(orgID, projectName, projectOwnerID string, withDefaultAlertsSettings bool) string {
12361260
return fmt.Sprintf(`
12371261
resource "mongodbatlas_project" "test" {
12381262
org_id = %[1]q
12391263
name = %[2]q
12401264
project_owner_id = %[3]q
1241-
with_default_alerts_settings = false
1265+
with_default_alerts_settings = %[4]t
12421266
}
1243-
`, orgID, projectName, projectOwnerID)
1267+
`, orgID, projectName, projectOwnerID, withDefaultAlertsSettings)
12441268
}
12451269

12461270
func configWithLimits(orgID, projectName string, limits []*admin.DataFederationLimit) string {

internal/testutil/acc/project.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ func ConfigProjectWithSettings(projectName, orgID, projectOwnerID string, value
3636
name = %[1]q
3737
org_id = %[2]q
3838
project_owner_id = %[3]q
39-
with_default_alerts_settings = %[4]t
4039
is_collect_database_specifics_statistics_enabled = %[4]t
4140
is_data_explorer_enabled = %[4]t
4241
is_extended_storage_sizes_enabled = %[4]t

0 commit comments

Comments
 (0)