-
Notifications
You must be signed in to change notification settings - Fork 189
/
Copy pathplan_modifier.go
213 lines (200 loc) · 9.38 KB
/
plan_modifier.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package advancedclustertpf
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier"
)
var (
attributePlanModifiers = map[string]customplanmodifier.UnknownReplacementCall[PlanModifyResourceInfo]{
"read_only_specs": readOnlyReplaceUnknown,
"analytics_specs": analyticsAndElectableSpecsReplaceUnknown,
"electable_specs": analyticsAndElectableSpecsReplaceUnknown,
"auto_scaling": autoScalingReplaceUnknown,
"analytics_auto_scaling": autoScalingReplaceUnknown,
// TODO: Add the other computed attributes
}
// Change mappings uses `attribute_name`, it doesn't care about the nested level.
// However, it doesn't stop calling `replication_specs.**.attribute_name`.
attributeRootChangeMapping = map[string][]string{
"disk_size_gb": {}, // disk_size_gb can be change at any level/spec
"replication_specs": {},
"tls_cipher_config_mode": {"custom_openssl_cipher_config_tls12"},
"cluster_type": {"config_server_management_mode", "config_server_type"}, // computed values of config server change when REPLICA_SET changes to SHARDED
"expiration_date": {"version"}, // pinned_fcv
}
attributeReplicationSpecChangeMapping = map[string][]string{ //nolint:unused // Add logic to use this in CLOUDP-308783
// All these fields can exist in specs that are computed, therefore, it is not safe to use them when they have changed.
"disk_iops": {},
"ebs_volume_type": {},
"disk_size_gb": {}, // disk_size_gb can be change at any level/spec
"instance_size": {"disk_iops"}, // disk_iops can change based on instance_size changes
"provider_name": {"ebs_volume_type"}, // AWS --> AZURE will change ebs_volume_type
"region_name": {"container_id"}, // container_id changes based on region_name changes
"zone_name": {"zone_id"}, // zone_id copy from state is not safe when
}
)
func unknownReplacements(ctx context.Context, tfsdkState *tfsdk.State, tfsdkPlan *tfsdk.Plan, diags *diag.Diagnostics) {
var plan, state TFModel
diags.Append(tfsdkState.Get(ctx, &state)...)
diags.Append(tfsdkPlan.Get(ctx, &plan)...)
if diags.HasError() {
return
}
diff := findClusterDiff(ctx, &state, &plan, diags)
if diags.HasError() || diff.isAnyUpgrade() { // Don't do anything in upgrades
return
}
computedUsed, diskUsed := autoScalingUsed(ctx, diags, &state, &plan)
shardingConfigUpgrade := isShardingConfigUpgrade(ctx, &state, &plan, diags)
isUsingNewShardingConfig := usingNewShardingConfig(ctx, plan.ReplicationSpecs, diags)
if diags.HasError() {
return
}
info := PlanModifyResourceInfo{
AutoScalingComputedUsed: computedUsed,
AutoScalingDiskUsed: diskUsed,
IsShardingConfigUpgrade: shardingConfigUpgrade,
UsingNewShardingConfig: isUsingNewShardingConfig,
}
unknownReplacements := customplanmodifier.NewUnknownReplacements(ctx, tfsdkState, tfsdkPlan, diags, ResourceSchema(ctx), info)
for attrName, replacer := range attributePlanModifiers {
unknownReplacements.AddReplacement(attrName, replacer)
}
unknownReplacements.AddKeepUnknownAlways("connection_strings", "state_name", "mongo_db_version") // Volatile attributes, should not be copied from state)
unknownReplacements.AddKeepUnknownOnChanges(attributeRootChangeMapping)
if computedUsed {
unknownReplacements.AddKeepUnknownAlways("instance_size", "disk_iops")
}
if diskUsed {
unknownReplacements.AddKeepUnknownAlways("disk_size_gb")
}
unknownReplacements.AddKeepUnknownsExtraCall(replicationSpecsKeepUnknownWhenChanged)
unknownReplacements.ApplyReplacements(ctx, diags)
}
func autoScalingReplaceUnknown(ctx context.Context, state attr.Value, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value {
// don't use auto_scaling or analytics_auto_scaling from state if it's not enabled as it doesn't need to be present in Update request payload
autoScalingModel := conversion.TFModelObject[TFAutoScalingModel](ctx, state.(types.Object))
if autoScalingModel != nil && (autoScalingModel.ComputeEnabled.ValueBool() || autoScalingModel.DiskGBEnabled.ValueBool()) {
return state
}
return req.Unknown
}
type PlanModifyResourceInfo struct {
AutoScalingComputedUsed bool
AutoScalingDiskUsed bool
IsShardingConfigUpgrade bool
UsingNewShardingConfig bool
}
func parentRegionConfigs(ctx context.Context, path path.Path, differ *customplanmodifier.PlanModifyDiffer) []TFRegionConfigsModel {
regionConfigsPath, diags := conversion.AncestorPathNoIndex(path, "region_configs")
if diags.HasError() {
tflog.Error(ctx, conversion.FormatDiags(&diags))
return nil
}
return customplanmodifier.ReadPlanStructValues[TFRegionConfigsModel](ctx, differ, regionConfigsPath)
}
func readOnlyReplaceUnknown(ctx context.Context, state attr.Value, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value {
if req.Info.IsShardingConfigUpgrade {
return req.Unknown
}
stateParsed := conversion.TFModelObject[TFSpecsModel](ctx, state.(types.Object))
if stateParsed == nil {
return req.Unknown
}
electablePath := req.Path.ParentPath().AtName("electable_specs")
electable := customplanmodifier.ReadPlanStructValue[TFSpecsModel](ctx, req.Differ, electablePath)
if electable == nil {
electableState := customplanmodifier.ReadStateStructValue[TFSpecsModel](ctx, req.Differ, electablePath)
if electableState != nil && electableState.NodeCount.ValueInt64() > 0 {
electable = electableState
}
}
if electable == nil {
regionConfigs := parentRegionConfigs(ctx, req.Path, req.Differ)
// ensures values are taken from a defined electable spec if not present in current region config
electable = findDefinedElectableSpecInReplicationSpec(ctx, regionConfigs)
}
var newReadOnly *TFSpecsModel
if electable == nil {
// using values directly from state if no electable specs are present in plan
newReadOnly = stateParsed
} else {
// node_count is from state, all others are from electable_specs plan
newReadOnly = &TFSpecsModel{
NodeCount: stateParsed.NodeCount,
InstanceSize: electable.InstanceSize,
DiskSizeGb: electable.DiskSizeGb,
EbsVolumeType: electable.EbsVolumeType,
DiskIops: electable.DiskIops,
}
}
return ensureSpecRespectChanges(ctx, newReadOnly, req)
}
func analyticsAndElectableSpecsReplaceUnknown(ctx context.Context, state attr.Value, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) attr.Value {
if req.Info.IsShardingConfigUpgrade {
return req.Unknown
}
stateParsed := conversion.TFModelObject[TFSpecsModel](ctx, state.(types.Object))
// don't get analytics_specs from state if node_count is 0 to avoid possible ANALYTICS_INSTANCE_SIZE_MUST_MATCH and INSTANCE_SIZE_MUST_MATCH errors
if stateParsed == nil || stateParsed.NodeCount.ValueInt64() == 0 {
return req.Unknown
}
return ensureSpecRespectChanges(ctx, stateParsed, req)
}
func ensureSpecRespectChanges(ctx context.Context, spec *TFSpecsModel, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) types.Object {
// if disk_size_gb is defined at root level we cannot use (analytics|electable|read_only)_specs.disk_size_gb from state as it can be outdated
if req.Changes.AttributeChanged("disk_size_gb") || req.Info.AutoScalingDiskUsed {
spec.DiskSizeGb = types.Float64Unknown()
}
if req.Info.AutoScalingComputedUsed {
spec.DiskIops = types.Int64Unknown()
}
return conversion.AsObjectValue(ctx, spec, SpecsObjType.AttrTypes)
}
func replicationSpecsKeepUnknownWhenChanged(ctx context.Context, state attr.Value, req *customplanmodifier.UnknownReplacementRequest[PlanModifyResourceInfo]) []string {
if !conversion.HasAncestor(req.Path, path.Root("replication_specs")) {
return nil
}
if req.Changes.AttributeChanged("replication_specs") {
return []string{req.AttributeName}
}
return nil
}
func findDefinedElectableSpecInReplicationSpec(ctx context.Context, regionConfigs []TFRegionConfigsModel) *TFSpecsModel {
for i := range regionConfigs {
electableSpecs := conversion.TFModelObject[TFSpecsModel](ctx, regionConfigs[i].ElectableSpecs)
if electableSpecs != nil {
return electableSpecs
}
}
return nil
}
func autoScalingUsed(ctx context.Context, diags *diag.Diagnostics, state, plan *TFModel) (computedUsed, diskUsed bool) {
for _, model := range []*TFModel{state, plan} {
repSpecsTF := conversion.TFModelList[TFReplicationSpecsModel](ctx, diags, model.ReplicationSpecs)
for i := range repSpecsTF {
regiongConfigsTF := conversion.TFModelList[TFRegionConfigsModel](ctx, diags, repSpecsTF[i].RegionConfigs)
for j := range regiongConfigsTF {
for _, autoScalingTF := range []types.Object{regiongConfigsTF[j].AutoScaling, regiongConfigsTF[j].AnalyticsAutoScaling} {
autoscaling := conversion.TFModelObject[TFAutoScalingModel](ctx, autoScalingTF)
if autoscaling == nil {
continue
}
if autoscaling.ComputeEnabled.ValueBool() {
computedUsed = true
}
if autoscaling.DiskGBEnabled.ValueBool() {
diskUsed = true
}
}
}
}
}
return
}