Skip to content

Commit

Permalink
chore: Adds Create Request Dumping to advanced_cluster TPF (#2797)
Browse files Browse the repository at this point in the history
* use  for labels.(key|value)

* feat: initial NewAtlasReq method

* chore: minor fixes to avoid dumping empty objects

* test: ensure dumped request payload is as expected

* chore: small NilForUnknown improvement

* feat: support UpdateOnly field

* address PR comments
  • Loading branch information
EspenAlbert authored Nov 14, 2024
1 parent 897644c commit ddbe080
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 18 deletions.
19 changes: 19 additions & 0 deletions internal/common/conversion/type_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ func StringToTime(str string) (time.Time, bool) {
return ret, err == nil
}

func StringPtrToTimePtr(str *string) (*time.Time, bool) {
if str == nil {
return nil, true
}
res, ok := StringToTime(*str)
return &res, ok
}

func Int64PtrToIntPtr(i64 *int64) *int {
if i64 == nil {
return nil
Expand Down Expand Up @@ -67,3 +75,14 @@ func MongoDBRegionToAWSRegion(region string) string {
func AWSRegionToMongoDBRegion(region string) string {
return strings.ReplaceAll(strings.ToUpper(region), "-", "_")
}

type TFPrimitiveType interface {
IsUnknown() bool
}

func NilForUnknown[T any](primitiveAttr TFPrimitiveType, value *T) *T {
if primitiveAttr.IsUnknown() {
return nil
}
return value
}
29 changes: 29 additions & 0 deletions internal/common/schemafunc/plan_modifier_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package schemafunc

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

func PlanModifyStringUpdateOnly() UpdateOnlyString {
return UpdateOnlyString{}
}

type UpdateOnlyString struct{}

func (u UpdateOnlyString) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
if (req.PlanValue.IsUnknown() || !req.PlanValue.IsNull()) && req.State.Raw.IsNull() {
errMsg := fmt.Sprintf("Update only attribute set on create: %s", req.Path)
resp.Diagnostics.AddError(errMsg, errMsg)
}
}

func (u UpdateOnlyString) Description(ctx context.Context) string {
return u.MarkdownDescription(ctx)
}

func (u UpdateOnlyString) MarkdownDescription(ctx context.Context) string {
return "Checks the attribute is never set on create"
}
12 changes: 0 additions & 12 deletions internal/service/advancedclustertpf/model.go

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package advancedclustertpf

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
"go.mongodb.org/atlas-sdk/v20241023002/admin"
)

func NewAtlasReq(ctx context.Context, input *TFModel, diags *diag.Diagnostics) *admin.ClusterDescription20240805 {
acceptDataRisksAndForceReplicaSetReconfig, ok := conversion.StringPtrToTimePtr(input.AcceptDataRisksAndForceReplicaSetReconfig.ValueStringPointer())
if !ok {
diags.AddError("error converting AcceptDataRisksAndForceReplicaSetReconfig", fmt.Sprintf("not a valid time: %s", input.AcceptDataRisksAndForceReplicaSetReconfig.ValueString()))
}
return &admin.ClusterDescription20240805{
AcceptDataRisksAndForceReplicaSetReconfig: acceptDataRisksAndForceReplicaSetReconfig,
BackupEnabled: conversion.NilForUnknown(input.BackupEnabled, input.BackupEnabled.ValueBoolPointer()),
BiConnector: newBiConnector(ctx, input.BiConnectorConfig, diags),
ClusterType: input.ClusterType.ValueStringPointer(),
ConfigServerManagementMode: conversion.NilForUnknown(input.ConfigServerManagementMode, input.ConfigServerManagementMode.ValueStringPointer()),
EncryptionAtRestProvider: conversion.NilForUnknown(input.EncryptionAtRestProvider, input.EncryptionAtRestProvider.ValueStringPointer()),
GlobalClusterSelfManagedSharding: conversion.NilForUnknown(input.GlobalClusterSelfManagedSharding, input.GlobalClusterSelfManagedSharding.ValueBoolPointer()),
GroupId: input.ProjectID.ValueStringPointer(),
Labels: newComponentLabel(ctx, input.Labels, diags),
MongoDBMajorVersion: conversion.NilForUnknown(input.MongoDBMajorVersion, input.MongoDBMajorVersion.ValueStringPointer()),
Name: input.Name.ValueStringPointer(),
Paused: conversion.NilForUnknown(input.Paused, input.Paused.ValueBoolPointer()),
PitEnabled: conversion.NilForUnknown(input.PitEnabled, input.PitEnabled.ValueBoolPointer()),
RedactClientLogData: conversion.NilForUnknown(input.RedactClientLogData, input.RedactClientLogData.ValueBoolPointer()),
ReplicaSetScalingStrategy: conversion.NilForUnknown(input.ReplicaSetScalingStrategy, input.ReplicaSetScalingStrategy.ValueStringPointer()),
ReplicationSpecs: newReplicationSpec20240805(ctx, input.ReplicationSpecs, diags),
RootCertType: conversion.NilForUnknown(input.RootCertType, input.RootCertType.ValueStringPointer()),
Tags: newResourceTag(ctx, input.Tags, diags),
TerminationProtectionEnabled: conversion.NilForUnknown(input.TerminationProtectionEnabled, input.TerminationProtectionEnabled.ValueBoolPointer()),
VersionReleaseSystem: conversion.NilForUnknown(input.VersionReleaseSystem, input.VersionReleaseSystem.ValueStringPointer()),
}
}
func newBiConnector(ctx context.Context, input types.Object, diags *diag.Diagnostics) *admin.BiConnector {
var resp *admin.BiConnector
if input.IsUnknown() || input.IsNull() {
return resp
}
item := &TFBiConnectorModel{}
if localDiags := input.As(ctx, item, basetypes.ObjectAsOptions{}); len(localDiags) > 0 {
diags.Append(localDiags...)
return resp
}
return &admin.BiConnector{
Enabled: conversion.NilForUnknown(item.Enabled, item.Enabled.ValueBoolPointer()),
ReadPreference: conversion.NilForUnknown(item.ReadPreference, item.ReadPreference.ValueStringPointer()),
}
}
func newComponentLabel(ctx context.Context, input types.Set, diags *diag.Diagnostics) *[]admin.ComponentLabel {
if input.IsUnknown() || input.IsNull() {
return nil
}
elements := make([]TFLabelsModel, len(input.Elements()))
if localDiags := input.ElementsAs(ctx, &elements, false); len(localDiags) > 0 {
diags.Append(localDiags...)
return nil
}
resp := make([]admin.ComponentLabel, len(input.Elements()))
for i := range elements {
item := &elements[i]
resp[i] = admin.ComponentLabel{
Key: item.Key.ValueStringPointer(),
Value: item.Value.ValueStringPointer(),
}
}
return &resp
}
func newReplicationSpec20240805(ctx context.Context, input types.List, diags *diag.Diagnostics) *[]admin.ReplicationSpec20240805 {
if input.IsUnknown() || input.IsNull() {
return nil
}
elements := make([]TFReplicationSpecsModel, len(input.Elements()))
if localDiags := input.ElementsAs(ctx, &elements, false); len(localDiags) > 0 {
diags.Append(localDiags...)
return nil
}
resp := make([]admin.ReplicationSpec20240805, len(input.Elements()))
for i := range elements {
item := &elements[i]
resp[i] = admin.ReplicationSpec20240805{
RegionConfigs: newCloudRegionConfig20240805(ctx, item.RegionConfigs, diags),
ZoneName: item.ZoneName.ValueStringPointer(),
}
}
return &resp
}
func newResourceTag(ctx context.Context, input types.Set, diags *diag.Diagnostics) *[]admin.ResourceTag {
if input.IsUnknown() || input.IsNull() {
return nil
}
elements := make([]TFTagsModel, len(input.Elements()))
if localDiags := input.ElementsAs(ctx, &elements, false); len(localDiags) > 0 {
diags.Append(localDiags...)
return nil
}
resp := make([]admin.ResourceTag, len(input.Elements()))
for i := range elements {
item := &elements[i]
resp[i] = admin.ResourceTag{
Key: item.Key.ValueString(),
Value: item.Value.ValueString(),
}
}
return &resp
}
func newCloudRegionConfig20240805(ctx context.Context, input types.List, diags *diag.Diagnostics) *[]admin.CloudRegionConfig20240805 {
if input.IsUnknown() || input.IsNull() {
return nil
}
elements := make([]TFRegionConfigsModel, len(input.Elements()))
if localDiags := input.ElementsAs(ctx, &elements, false); len(localDiags) > 0 {
diags.Append(localDiags...)
return nil
}
resp := make([]admin.CloudRegionConfig20240805, len(input.Elements()))
for i := range elements {
item := &elements[i]
resp[i] = admin.CloudRegionConfig20240805{
AnalyticsAutoScaling: newAdvancedAutoScalingSettings(ctx, item.AnalyticsAutoScaling, diags),
AnalyticsSpecs: newDedicatedHardwareSpec20240805(ctx, item.AnalyticsSpecs, diags),
AutoScaling: newAdvancedAutoScalingSettings(ctx, item.AutoScaling, diags),
BackingProviderName: conversion.NilForUnknown(item.BackingProviderName, item.BackingProviderName.ValueStringPointer()),
ElectableSpecs: newHardwareSpec20240805(ctx, item.ElectableSpecs, diags),
Priority: conversion.Int64PtrToIntPtr(item.Priority.ValueInt64Pointer()),
ProviderName: item.ProviderName.ValueStringPointer(),
ReadOnlySpecs: newDedicatedHardwareSpec20240805(ctx, item.ReadOnlySpecs, diags),
RegionName: item.RegionName.ValueStringPointer(),
}
}
return &resp
}

func newAdvancedAutoScalingSettings(ctx context.Context, input types.Object, diags *diag.Diagnostics) *admin.AdvancedAutoScalingSettings {
var resp *admin.AdvancedAutoScalingSettings
if input.IsUnknown() || input.IsNull() {
return resp
}
item := &TFAutoScalingModel{}
if localDiags := input.As(ctx, item, basetypes.ObjectAsOptions{}); len(localDiags) > 0 {
diags.Append(localDiags...)
return resp
}
return &admin.AdvancedAutoScalingSettings{
Compute: newAdvancedComputeAutoScaling(ctx, input, diags),
DiskGB: newDiskGBAutoScaling(ctx, input, diags),
}
}
func newHardwareSpec20240805(ctx context.Context, input types.Object, diags *diag.Diagnostics) *admin.HardwareSpec20240805 {
var resp *admin.HardwareSpec20240805
if input.IsUnknown() || input.IsNull() {
return resp
}
item := &TFSpecsModel{}
if localDiags := input.As(ctx, item, basetypes.ObjectAsOptions{}); len(localDiags) > 0 {
diags.Append(localDiags...)
return resp
}
return &admin.HardwareSpec20240805{
DiskIOPS: conversion.NilForUnknown(item.DiskIops, conversion.Int64PtrToIntPtr(item.DiskIops.ValueInt64Pointer())),
DiskSizeGB: conversion.NilForUnknown(item.DiskSizeGb, item.DiskSizeGb.ValueFloat64Pointer()),
EbsVolumeType: conversion.NilForUnknown(item.EbsVolumeType, item.EbsVolumeType.ValueStringPointer()),
InstanceSize: conversion.NilForUnknown(item.InstanceSize, item.InstanceSize.ValueStringPointer()),
NodeCount: conversion.NilForUnknown(item.NodeCount, conversion.Int64PtrToIntPtr(item.NodeCount.ValueInt64Pointer())),
}
}
func newDedicatedHardwareSpec20240805(ctx context.Context, input types.Object, diags *diag.Diagnostics) *admin.DedicatedHardwareSpec20240805 {
var resp *admin.DedicatedHardwareSpec20240805
if input.IsUnknown() || input.IsNull() {
return resp
}
item := &TFSpecsModel{}
if localDiags := input.As(ctx, item, basetypes.ObjectAsOptions{}); len(localDiags) > 0 {
diags.Append(localDiags...)
return resp
}
return &admin.DedicatedHardwareSpec20240805{
DiskIOPS: conversion.NilForUnknown(item.DiskIops, conversion.Int64PtrToIntPtr(item.DiskIops.ValueInt64Pointer())),
DiskSizeGB: conversion.NilForUnknown(item.DiskSizeGb, item.DiskSizeGb.ValueFloat64Pointer()),
EbsVolumeType: conversion.NilForUnknown(item.EbsVolumeType, item.EbsVolumeType.ValueStringPointer()),
InstanceSize: conversion.NilForUnknown(item.InstanceSize, item.InstanceSize.ValueStringPointer()),
NodeCount: conversion.NilForUnknown(item.NodeCount, conversion.Int64PtrToIntPtr(item.NodeCount.ValueInt64Pointer())),
}
}

func newAdvancedComputeAutoScaling(ctx context.Context, input types.Object, diags *diag.Diagnostics) *admin.AdvancedComputeAutoScaling {
var resp *admin.AdvancedComputeAutoScaling
if input.IsUnknown() || input.IsNull() {
return resp
}
item := &TFAutoScalingModel{}
if localDiags := input.As(ctx, item, basetypes.ObjectAsOptions{}); len(localDiags) > 0 {
diags.Append(localDiags...)
return resp
}
return &admin.AdvancedComputeAutoScaling{
Enabled: conversion.NilForUnknown(item.ComputeEnabled, item.ComputeEnabled.ValueBoolPointer()),
ScaleDownEnabled: conversion.NilForUnknown(item.ComputeScaleDownEnabled, item.ComputeScaleDownEnabled.ValueBoolPointer()),
MaxInstanceSize: conversion.NilForUnknown(item.ComputeMaxInstanceSize, item.ComputeMaxInstanceSize.ValueStringPointer()),
MinInstanceSize: conversion.NilForUnknown(item.ComputeMinInstanceSize, item.ComputeMinInstanceSize.ValueStringPointer()),
}
}
func newDiskGBAutoScaling(ctx context.Context, input types.Object, diags *diag.Diagnostics) *admin.DiskGBAutoScaling {
var resp *admin.DiskGBAutoScaling
if input.IsUnknown() || input.IsNull() {
return resp
}
item := &TFAutoScalingModel{}
if localDiags := input.As(ctx, item, basetypes.ObjectAsOptions{}); len(localDiags) > 0 {
diags.Append(localDiags...)
return resp
}
return &admin.DiskGBAutoScaling{
Enabled: conversion.NilForUnknown(item.DiskGBEnabled, item.DiskGBEnabled.ValueBoolPointer()),
}
}
9 changes: 9 additions & 0 deletions internal/service/advancedclustertpf/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou
if resp.Diagnostics.HasError() {
return
}
sdkReq := NewAtlasReq(ctx, &plan, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
err := StoreCreatePayload(sdkReq)
if err != nil {
resp.Diagnostics.AddError("errorCreate", fmt.Sprintf(errorCreate, err.Error()))
return
}
tfNewModel, shouldReturn := mockedSDK(ctx, &resp.Diagnostics, plan.Timeouts)
if shouldReturn {
return
Expand Down
8 changes: 4 additions & 4 deletions internal/service/advancedclustertpf/resource_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/schemafunc"
)

func ResourceSchema(ctx context.Context) schema.Schema {
Expand All @@ -21,6 +22,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
Attributes: map[string]schema.Attribute{
"accept_data_risks_and_force_replica_set_reconfig": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{schemafunc.PlanModifyStringUpdateOnly()},
MarkdownDescription: "If reconfiguration is necessary to regain a primary due to a regional outage, submit this field alongside your topology reconfiguration to request a new regional outage resistant topology. Forced reconfigurations during an outage of the majority of electable nodes carry a risk of data loss if replicated writes (even majority committed writes) have not been replicated to the new primary node. MongoDB Atlas docs contain more information. To proceed with an operation which carries that risk, set **acceptDataRisksAndForceReplicaSetReconfig** to the current date.",
},
"backup_enabled": schema.BoolAttribute{
Expand Down Expand Up @@ -161,13 +163,11 @@ func ResourceSchema(ctx context.Context) schema.Schema {
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"key": schema.StringAttribute{
Computed: true,
Optional: true,
Required: true,
MarkdownDescription: "Key applied to tag and categorize this component.",
},
"value": schema.StringAttribute{
Computed: true,
Optional: true,
Required: true,
MarkdownDescription: "Value set to the Key applied to tag and categorize this component.",
},
},
Expand Down
20 changes: 20 additions & 0 deletions internal/service/advancedclustertpf/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package advancedclustertpf_test

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/sebdah/goldie/v2"

"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedclustertpf"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc"
Expand All @@ -23,6 +25,19 @@ func ChangeReponseNumber(responseNumber int) resource.TestCheckFunc {
return changer
}

func CheckRequestPayload(t *testing.T, requestName string) resource.TestCheckFunc {
t.Helper()
return func(state *terraform.State) error {
g := goldie.New(t, goldie.WithNameSuffix(".json"))
lastPayload, err := advancedclustertpf.ReadLastCreatePayload()
if err != nil {
return err
}
g.Assert(t, requestName, []byte(lastPayload))
return nil
}
}

func TestAccAdvancedCluster_basic(t *testing.T) {
var (
projectID = "111111111111111111111111"
Expand All @@ -31,11 +46,16 @@ func TestAccAdvancedCluster_basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
Steps: []resource.TestStep{
{
Config: configBasic(projectID, clusterName, "accept_data_risks_and_force_replica_set_reconfig = \"2006-01-02T15:04:05Z\""),
ExpectError: regexp.MustCompile("Update only attribute set on create: accept_data_risks_and_force_replica_set_reconfig"),
},
{
Config: configBasic(projectID, clusterName, ""),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "state_name", "CREATING"),
ChangeReponseNumber(2),
CheckRequestPayload(t, "create_payload_check1"),
),
},
{
Expand Down
Loading

0 comments on commit ddbe080

Please sign in to comment.