Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Breaking change of null keeper values between 3.3.2 and 3.4.0 #305

Merged
merged 16 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
name: 'Acc. Tests (OS: ${{ matrix.os }} / TF: ${{ matrix.terraform }})'
needs: build
runs-on: ${{ matrix.os }}
timeout-minutes: 15
timeout-minutes: 20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this pull request, but we can try to avoid this ever growing timeout issue by switching testing to using resource.ParallelTest() (which calls (*testing.T).Parallel() under the hood) and passing the -parallel flag to the go test command.

strategy:
fail-fast: false
matrix:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

BUG FIXES:

* all: Prevent `keeper` with `null` values from forcing replacement ([305](https://github.com/hashicorp/terraform-provider-random/pull/305)).
* resource/random_password: During upgrade state, ensure `min_upper` is populated ([304](https://github.com/hashicorp/terraform-provider-random/pull/304)).
* resource/random_string: During upgrade state, ensure `min_upper` is populated ([304](https://github.com/hashicorp/terraform-provider-random/pull/304)).

Expand Down
118 changes: 118 additions & 0 deletions internal/planmodifiers/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,121 @@ func (d *numberNumericAttributePlanModifier) Modify(ctx context.Context, req tfs
return
}
}

func RequiresReplaceIfValuesNotNull() tfsdk.AttributePlanModifier {
return requiresReplaceIfValuesNotNullModifier{}
}

type requiresReplaceIfValuesNotNullModifier struct{}

func (r requiresReplaceIfValuesNotNullModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) {
if req.AttributeConfig == nil || req.AttributePlan == nil || req.AttributeState == nil {
// shouldn't happen, but let's not panic if it does
return
}

if req.State.Raw.IsNull() {
// if we're creating the resource, no need to delete and
// recreate it
return
}

if req.Plan.Raw.IsNull() {
// if we're deleting the resource, no need to delete and
// recreate it
return
}

// If there are no differences, do not mark the resource for replacement
// and ensure the plan matches the configuration.
if req.AttributeConfig.Equal(req.AttributeState) {
return
}

if req.AttributeState.IsNull() {
// terraform-plugin-sdk would store maps as null if all keys had null
// values. To prevent unintentional replacement plans when migrating
// to terraform-plugin-framework, only trigger replacement when the
// prior state (map) is null and when there are not null map values.
allNullValues := true

configMap, ok := req.AttributeConfig.(types.Map)

if !ok {
return
}

for _, configValue := range configMap.Elems {
if !configValue.IsNull() {
allNullValues = false
}
}

if allNullValues {
return
}
} else {
// terraform-plugin-sdk would completely omit storing map keys with
// null values, so this also must prevent unintentional replacement
// in that case as well.
allNewNullValues := true

configMap, ok := req.AttributeConfig.(types.Map)

if !ok {
return
}

stateMap, ok := req.AttributeState.(types.Map)

if !ok {
return
}

for configKey, configValue := range configMap.Elems {
stateValue, ok := stateMap.Elems[configKey]

// If the key doesn't exist in state and the config value is
// null, do not trigger replacement.
if !ok && configValue.IsNull() {
continue
}

// If the state value exists and it is equal to the config value,
// do not trigger replacement.
if configValue.Equal(stateValue) {
continue
}

allNewNullValues = false
break
}

for stateKey := range stateMap.Elems {
_, ok := configMap.Elems[stateKey]

// If the key doesn't exist in the config, but there is a state
// value, trigger replacement.
if !ok {
allNewNullValues = false
break
}
}

if allNewNullValues {
return
}
}

resp.RequiresReplace = true
}

// Description returns a human-readable description of the plan modifier.
func (r requiresReplaceIfValuesNotNullModifier) Description(ctx context.Context) string {
return "If the value of this attribute changes, Terraform will destroy and recreate the resource."
}

// MarkdownDescription returns a markdown description of the plan modifier.
func (r requiresReplaceIfValuesNotNullModifier) MarkdownDescription(ctx context.Context) string {
return "If the value of this attribute changes, Terraform will destroy and recreate the resource."
}
32 changes: 28 additions & 4 deletions internal/provider/resource_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/terraform-providers/terraform-provider-random/internal/diagnostics"
"github.com/terraform-providers/terraform-provider-random/internal/planmodifiers"
)

var _ provider.ResourceType = (*idResourceType)(nil)
Expand Down Expand Up @@ -47,7 +48,7 @@ exist concurrently.
},
Optional: true,
PlanModifiers: []tfsdk.AttributePlanModifier{
resource.RequiresReplace(),
planmodifiers.RequiresReplaceIfValuesNotNull(),
},
},
"byte_length": {
Expand All @@ -73,27 +74,42 @@ exist concurrently.
"case-sensitive letters, digits and the characters `_` and `-`.",
Type: types.StringType,
Computed: true,
PlanModifiers: []tfsdk.AttributePlanModifier{
resource.UseStateForUnknown(),
},
},
"b64_std": {
Description: "The generated id presented in base64 without additional transformations.",
Type: types.StringType,
Computed: true,
PlanModifiers: []tfsdk.AttributePlanModifier{
resource.UseStateForUnknown(),
},
},
"hex": {
Description: "The generated id presented in padded hexadecimal digits. This result will " +
"always be twice as long as the requested byte length.",
Type: types.StringType,
Computed: true,
PlanModifiers: []tfsdk.AttributePlanModifier{
resource.UseStateForUnknown(),
},
},
"dec": {
Description: "The generated id presented in non-padded decimal digits.",
Type: types.StringType,
Computed: true,
PlanModifiers: []tfsdk.AttributePlanModifier{
resource.UseStateForUnknown(),
},
},
"id": {
Description: "The generated id presented in base64 without additional transformations or prefix.",
Type: types.StringType,
Computed: true,
PlanModifiers: []tfsdk.AttributePlanModifier{
resource.UseStateForUnknown(),
},
},
},
}, nil
Expand Down Expand Up @@ -163,9 +179,17 @@ func (r *idResource) Create(ctx context.Context, req resource.CreateRequest, res
func (r *idResource) Read(context.Context, resource.ReadRequest, *resource.ReadResponse) {
}

// Update is intentionally left blank as all required and optional attributes force replacement of the resource
// through the RequiresReplace AttributePlanModifier.
func (r *idResource) Update(context.Context, resource.UpdateRequest, *resource.UpdateResponse) {
// Update ensures the plan value is copied to the state to complete the update.
func (r *idResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var model idModelV0

resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)

if resp.Diagnostics.HasError() {
return
}

resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
}

// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the
Expand Down