From d45e6bfd6f8838cac8edb085847933c1ce9c667a Mon Sep 17 00:00:00 2001 From: rchrabas Date: Wed, 12 Feb 2025 12:04:43 +0100 Subject: [PATCH] security_zones fix --- CHANGELOG.md | 4 + docs/guides/changelog.md | 4 + gen/definitions/security_zones.yaml | 1 + gen/generator.go | 16 +++ gen/templates/model.go | 132 +++++++++++++----- gen/templates/resource.go | 44 +++++- internal/provider/model_fmc_security_zones.go | 41 ++++++ .../ConditionalUseStateForUnknownString.go | 68 +++++++++ .../provider/resource_fmc_security_zones.go | 19 ++- .../resource_fmc_security_zones_test.go | 78 +++++++++++ templates/guides/changelog.md.tmpl | 4 + 11 files changed, 367 insertions(+), 44 deletions(-) create mode 100644 internal/provider/planmodifiers/ConditionalUseStateForUnknownString.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7ebda7..4b8335a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.0-beta2 (Unreleased) + +- (Fix) Change value of `interface_type` within `fmc_security_zones` item should replace just this object, not entire bulk resource + ## 2.0.0-beta1 - Initial release diff --git a/docs/guides/changelog.md b/docs/guides/changelog.md index bc4445c6..8a99ac9f 100644 --- a/docs/guides/changelog.md +++ b/docs/guides/changelog.md @@ -7,6 +7,10 @@ description: |- # Changelog +## 2.0.0-beta2 (Unreleased) + +- (Fix) Change value of `interface_type` within `fmc_security_zones` item should replace just this object, not entire bulk resource + ## 2.0.0-beta1 - Initial release diff --git a/gen/definitions/security_zones.yaml b/gen/definitions/security_zones.yaml index d63b5d29..9b3e9a63 100644 --- a/gen/definitions/security_zones.yaml +++ b/gen/definitions/security_zones.yaml @@ -28,6 +28,7 @@ attributes: to mode NONE of associated interfaces. enum_values: [PASSIVE, INLINE, SWITCHED, ROUTED, ASA] example: ROUTED + requires_replace: true - model_name: type type: String description: Type of the object; this value is always 'SecurityZone'. diff --git a/gen/generator.go b/gen/generator.go index 85271076..8b83a845 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -285,6 +285,21 @@ func HasResourceId(attributes []YamlConfigAttribute) bool { return false } +// Templating helper function to return true if any of the attributes has `requires_replace: true` +func HasRequiresReplace(attributes []YamlConfigAttribute) bool { + for _, attr := range attributes { + if attr.RequiresReplace { + return true + } + if len(attr.Attributes) > 0 { + if HasRequiresReplace(attr.Attributes) { + return true + } + } + } + return false +} + // Templating helper function to return true if type is a list or set without nested elements func IsListSet(attribute YamlConfigAttribute) bool { if (attribute.Type == "List" || attribute.Type == "Set") && attribute.ElementType != "" { @@ -403,6 +418,7 @@ var functions = template.FuncMap{ "hasId": HasId, "hasReference": HasReference, "hasResourceId": HasResourceId, + "hasRequiresReplace": HasRequiresReplace, "isListSet": IsListSet, "isList": IsList, "isSet": IsSet, diff --git a/gen/templates/model.go b/gen/templates/model.go index 99f4d9b4..9c8a1fa3 100644 --- a/gen/templates/model.go +++ b/gen/templates/model.go @@ -534,6 +534,97 @@ func (data *{{camelCase .Name}}) fromBodyUnknowns(ctx context.Context, res gjson } // End of section. //template:end fromBodyUnknowns + +// Section below is generated&owned by "gen/generator.go". //template:begin Clone + +{{if .IsBulk}} +func (data *{{camelCase .Name}}) Clone() {{camelCase .Name}} { + ret := *data + ret.Items = maps.Clone(data.Items) + + return ret +} +{{- end}} + +// End of section. //template:end Clone + +// Section below is generated&owned by "gen/generator.go". //template:begin toBodyNonBulk + +{{if .IsBulk}} +// Updates done one-by-one require different API body +func (data {{camelCase .Name}}) toBodyNonBulk(ctx context.Context, state {{camelCase .Name}}) string { + // This is one-by-one update, so only one element to update is expected + if len(data.Items) > 1 { + tflog.Error(ctx, "Found more than one element to chage. Only one will be changed.") + } + + // Utilize existing toBody function + body := data.toBody(ctx, state) + + // Get first element only + return gjson.Get(body, "0").String() +} +{{- end}} + +// End of section. //template:end toBodyNonBulk + +// Section below is generated&owned by "gen/generator.go". //template:begin findObjectsToBeReplaced + +{{if and .IsBulk (hasRequiresReplace .Attributes) }} +// Check if single object within bulk requires replace due to `requires_replace` +// Since here we assume object has changed, it must be present in both state and plan (data) +func (data {{camelCase .Name}}) findObjectsToBeReplaced(ctx context.Context, state {{camelCase .Name}}) {{camelCase .Name}} { + // Prepare empty object to be filled in with objects that require replace + var toBeReplaced {{camelCase .Name}} + toBeReplaced.Items = make(map[string]{{camelCase .Name}}Items) + + // Iterate over all objects in plan + for key, item := range data.Items { + // Check if object is present in state + if _, ok := state.Items[key]; !ok { + // Object is not present in state, hence it's not a candidate for replace + continue + } + + // Check if any field marked as `requires_replace` has changed + {{- range .Attributes}} + {{- if eq .TfName "items"}} + {{- range .Attributes}} + {{- if .RequiresReplace }} + {{- if (eq .Type "String")}} + if item.{{toGoName .TfName}} != state.Items[key].{{toGoName .TfName}} { + toBeReplaced.Items[key] = item + continue + } + {{- else}} + {{- errorf "requires_replace is not supported for %v" .Type }} + {{- end}} + {{- end}} + {{- end}} + {{- end}} + {{- end}} + } + + return toBeReplaced +} +{{- end}} + +// End of section. //template:end findObjectsToBeReplaced + +// Section below is generated&owned by "gen/generator.go". //template:begin clearItemIds + +{{if and .IsBulk (hasRequiresReplace .Attributes) }} +func (data *{{camelCase .Name}}) clearItemsIds(ctx context.Context) { + for key, value := range data.Items { + tmp := value + tmp.Id = types.StringNull() + data.Items[key] = tmp + } +} +{{- end}} + +// End of section. //template:end clearItemIds + {{- range .Attributes}} {{- if isNestedMap .}} {{- $found := false }} @@ -567,6 +658,9 @@ func (data *{{camelCase .Name}}) fromBodyUnknowns(ctx context.Context, res gjson {{- if hasResourceId .Attributes}} {{- errorf "resource_id not yet implemented at this depth"}} {{- end}} + {{- if and $.IsBulk .RequiresReplace}} + {{- errorf "requires_replace is not supported for nested objects in bulk operations" }} + {{- end}} {{- range .Attributes}} {{- if isNestedMap .}} @@ -581,40 +675,10 @@ func (data *{{camelCase .Name}}) fromBodyUnknowns(ctx context.Context, res gjson {{- range .Attributes}} {{- errorf "attributes not yet implemented at this depth"}} {{- end}} + {{- if and $.IsBulk .RequiresReplace}} + {{- errorf "requires_replace is not supported for nested objects in bulk operations" }} + {{- end}} {{- end}} {{- end}} {{- end}} -{{- end}} - -// Section below is generated&owned by "gen/generator.go". //template:begin Clone - -{{if .IsBulk}} -func (data *{{camelCase .Name}}) Clone() {{camelCase .Name}} { - ret := *data - ret.Items = maps.Clone(data.Items) - - return ret -} -{{- end}} - -// End of section. //template:end Clone - -// Section below is generated&owned by "gen/generator.go". //template:begin toBodyNonBulk - -{{if .IsBulk}} -// Updates done one-by-one require different API body -func (data {{camelCase .Name}}) toBodyNonBulk(ctx context.Context, state {{camelCase .Name}}) string { - // This is one-by-one update, so only one element to update is expected - if len(data.Items) > 1 { - tflog.Error(ctx, "Found more than one element to chage. Only one will be changed.") - } - - // Utilize existing toBody function - body := data.toBody(ctx, state) - - // Get first element only - return gjson.Get(body, "0").String() -} -{{- end}} - -// End of section. //template:end toBodyNonBulk \ No newline at end of file +{{- end}} \ No newline at end of file diff --git a/gen/templates/resource.go b/gen/templates/resource.go index 20387bf2..f07f45c5 100644 --- a/gen/templates/resource.go +++ b/gen/templates/resource.go @@ -200,7 +200,7 @@ func (r *{{camelCase .Name}}Resource) Schema(ctx context.Context, req resource.S {{- end}} {{- if or .Id .Reference .RequiresReplace (and .Computed (not .ComputedRefreshValue))}} PlanModifiers: []planmodifier.{{.Type}}{ - {{- if or .Id .Reference .RequiresReplace}} + {{- if or .Id .Reference (and .RequiresReplace (not $.IsBulk))}} {{snakeCase .Type}}planmodifier.RequiresReplace(), {{end}} {{- if and .Computed (not .ComputedRefreshValue)}} @@ -210,6 +210,7 @@ func (r *{{camelCase .Name}}Resource) Schema(ctx context.Context, req resource.S {{- end}} {{- if isNestedListMapSet .}} {{- $useStateForUnknown := isNestedMap .}} + {{- $itemsList := .}} NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ {{- range .Attributes}} @@ -281,9 +282,19 @@ func (r *{{camelCase .Name}}Resource) Schema(ctx context.Context, req resource.S {{- end}} {{- if or (and .ResourceId $useStateForUnknown) (and .Computed (not .ComputedRefreshValue))}} PlanModifiers: []planmodifier.{{.Type}}{ + {{- if and $.IsBulk (eq .TfName "id") (hasRequiresReplace $.Attributes) }} + {{- if eq .Type "String"}} + {{- range $itemsList.Attributes }} + {{- if .RequiresReplace}} + planmodifiers.ConditionalUseStateForUnknownString("{{.TfName}}"), + {{- end}} + {{- end }} + {{- end}} + {{- else}} {{snakeCase .Type}}planmodifier.UseStateForUnknown(), + {{- end}} }, - {{- else if .RequiresReplace}} + {{- else if and .RequiresReplace (not $.IsBulk)}} PlanModifiers: []planmodifier.{{.Type}}{ {{snakeCase .Type}}planmodifier.RequiresReplace(), }, @@ -360,7 +371,7 @@ func (r *{{camelCase .Name}}Resource) Schema(ctx context.Context, req resource.S {{- end}} {{- if or .RequiresReplace .Computed}} PlanModifiers: []planmodifier.{{.Type}}{ - {{- if .RequiresReplace}} + {{- if and .RequiresReplace (not $.IsBulk)}} {{snakeCase .Type}}planmodifier.RequiresReplace(), {{end}} {{- if and .Computed (not .ComputedRefreshValue)}} @@ -440,7 +451,7 @@ func (r *{{camelCase .Name}}Resource) Schema(ctx context.Context, req resource.S {{- end}} {{- if or .RequiresReplace .Computed}} PlanModifiers: []planmodifier.{{.Type}}{ - {{- if .RequiresReplace}} + {{- if and .RequiresReplace (not $.IsBulk)}} {{snakeCase .Type}}planmodifier.RequiresReplace(), {{end}} {{- if and .Computed (not .ComputedRefreshValue)}} @@ -758,10 +769,19 @@ func (r *{{camelCase .Name}}Resource) Update(ctx context.Context, req resource.U {{- if .IsBulk}} + {{- if hasRequiresReplace $.Attributes}} + // Get objects that need to be replaced due to `requires_replace` flag + toBeReplaced := plan.findObjectsToBeReplaced(ctx, state) + {{- end}} + // DELETE // Delete objects (that are present in state, but missing in plan) + {{- if hasRequiresReplace $.Attributes}} + toDelete := toBeReplaced.Clone() + {{- else}} var toDelete {{camelCase .Name}} toDelete.Items = make(map[string]{{camelCase .Name}}Items) + {{- end}} planOwnedIDs := make(map[string]string, len(plan.Items)) // Prepare list of ID that are in plan @@ -790,8 +810,13 @@ func (r *{{camelCase .Name}}Resource) Update(ctx context.Context, req resource.U // CREATE // Create new objects (objects that have missing IDs in plan) + {{- if hasRequiresReplace $.Attributes}} + toCreate := toBeReplaced.Clone() + toCreate.clearItemsIds(ctx) + {{- else}} var toCreate {{camelCase .Name}} toCreate.Items = make(map[string]{{camelCase .Name}}Items) + {{- end}} // Scan plan for items with no ID for k, v := range plan.Items { if v.Id.IsUnknown() || v.Id.IsNull() { @@ -817,7 +842,18 @@ func (r *{{camelCase .Name}}Resource) Update(ctx context.Context, req resource.U var toUpdate {{camelCase .Name}} toUpdate.Items = make(map[string]{{camelCase .Name}}Items) + {{- if hasRequiresReplace $.Attributes}} + + for tmp, valueState := range state.Items { + // Check if the ID from state is on toBeReplaced list + if _, ok := toBeReplaced.Items[tmp]; ok { + // If it is, skip it as it was handled by delete/create processes + continue + } + {{- else }} + for _, valueState := range state.Items { + {{- end}} // Check if the ID from plan exists on list of ID owned by state if keyState, ok := planOwnedIDs[valueState.Id.ValueString()]; ok { diff --git a/internal/provider/model_fmc_security_zones.go b/internal/provider/model_fmc_security_zones.go index c40190ce..fdf85708 100644 --- a/internal/provider/model_fmc_security_zones.go +++ b/internal/provider/model_fmc_security_zones.go @@ -240,3 +240,44 @@ func (data SecurityZones) toBodyNonBulk(ctx context.Context, state SecurityZones } // End of section. //template:end toBodyNonBulk + +// Section below is generated&owned by "gen/generator.go". //template:begin findObjectsToBeReplaced + +// Check if single object within bulk requires replace due to `requires_replace` +// Since here we assume object has changed, it must be present in both state and plan (data) +func (data SecurityZones) findObjectsToBeReplaced(ctx context.Context, state SecurityZones) SecurityZones { + // Prepare empty object to be filled in with objects that require replace + var toBeReplaced SecurityZones + toBeReplaced.Items = make(map[string]SecurityZonesItems) + + // Iterate over all objects in plan + for key, item := range data.Items { + // Check if object is present in state + if _, ok := state.Items[key]; !ok { + // Object is not present in state, hence it's not a candidate for replace + continue + } + + // Check if any field marked as `requires_replace` has changed + if item.InterfaceType != state.Items[key].InterfaceType { + toBeReplaced.Items[key] = item + continue + } + } + + return toBeReplaced +} + +// End of section. //template:end findObjectsToBeReplaced + +// Section below is generated&owned by "gen/generator.go". //template:begin clearItemIds + +func (data *SecurityZones) clearItemsIds(ctx context.Context) { + for key, value := range data.Items { + tmp := value + tmp.Id = types.StringNull() + data.Items[key] = tmp + } +} + +// End of section. //template:end clearItemIds diff --git a/internal/provider/planmodifiers/ConditionalUseStateForUnknownString.go b/internal/provider/planmodifiers/ConditionalUseStateForUnknownString.go new file mode 100644 index 00000000..e1080da5 --- /dev/null +++ b/internal/provider/planmodifiers/ConditionalUseStateForUnknownString.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +package planmodifiers + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// +// ConditionalUseStateForUnknownString sets value to `known after apply` if dependent attribute changes +// + +var _ planmodifier.String = conditionalUseStateForUnknownString{} + +func ConditionalUseStateForUnknownString(dependentAttribute string) planmodifier.String { + return conditionalUseStateForUnknownString{dependentAttribute: dependentAttribute} +} + +type conditionalUseStateForUnknownString struct { + dependentAttribute string +} + +func (c conditionalUseStateForUnknownString) Description(ctx context.Context) string { + return "Conditionally set to `known after apply` based on change of `dependentAttribute`" +} + +func (c conditionalUseStateForUnknownString) MarkdownDescription(ctx context.Context) string { + return c.Description(ctx) +} + +func (c conditionalUseStateForUnknownString) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Get path to the dependent attribute + dependentAttributePath := req.Path.ParentPath().AtName(c.dependentAttribute) + + // Get state value of dependent attribute + var stateValue types.String + req.State.GetAttribute(ctx, dependentAttributePath, &stateValue) + + // Get plan value of dependent attribute + var planValue types.String + req.Plan.GetAttribute(ctx, dependentAttributePath, &planValue) + + // If state and plan values don't match, do nothing (sets value to `known after apply`) + if stateValue.ValueString() != planValue.ValueString() { + return + } + + // If state and plan values match, set plan to state value + resp.PlanValue = req.StateValue +} diff --git a/internal/provider/resource_fmc_security_zones.go b/internal/provider/resource_fmc_security_zones.go index 7b784f3e..82172ddf 100644 --- a/internal/provider/resource_fmc_security_zones.go +++ b/internal/provider/resource_fmc_security_zones.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/CiscoDevNet/terraform-provider-fmc/internal/provider/helpers" + "github.com/CiscoDevNet/terraform-provider-fmc/internal/provider/planmodifiers" "github.com/google/uuid" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -95,7 +96,7 @@ func (r *SecurityZonesResource) Schema(ctx context.Context, req resource.SchemaR MarkdownDescription: helpers.NewAttributeDescription("Id of the managed Security Zone.").String, Computed: true, PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), + planmodifiers.ConditionalUseStateForUnknownString("interface_type"), }, }, "interface_type": schema.StringAttribute{ @@ -243,11 +244,12 @@ func (r *SecurityZonesResource) Update(ctx context.Context, req resource.UpdateR } tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Update", plan.Id.ValueString())) + // Get objects that need to be replaced due to `requires_replace` flag + toBeReplaced := plan.findObjectsToBeReplaced(ctx, state) // DELETE // Delete objects (that are present in state, but missing in plan) - var toDelete SecurityZones - toDelete.Items = make(map[string]SecurityZonesItems) + toDelete := toBeReplaced.Clone() planOwnedIDs := make(map[string]string, len(plan.Items)) // Prepare list of ID that are in plan @@ -276,8 +278,8 @@ func (r *SecurityZonesResource) Update(ctx context.Context, req resource.UpdateR // CREATE // Create new objects (objects that have missing IDs in plan) - var toCreate SecurityZones - toCreate.Items = make(map[string]SecurityZonesItems) + toCreate := toBeReplaced.Clone() + toCreate.clearItemsIds(ctx) // Scan plan for items with no ID for k, v := range plan.Items { if v.Id.IsUnknown() || v.Id.IsNull() { @@ -303,7 +305,12 @@ func (r *SecurityZonesResource) Update(ctx context.Context, req resource.UpdateR var toUpdate SecurityZones toUpdate.Items = make(map[string]SecurityZonesItems) - for _, valueState := range state.Items { + for tmp, valueState := range state.Items { + // Check if the ID from state is on toBeReplaced list + if _, ok := toBeReplaced.Items[tmp]; ok { + // If it is, skip it as it was handled by delete/create processes + continue + } // Check if the ID from plan exists on list of ID owned by state if keyState, ok := planOwnedIDs[valueState.Id.ValueString()]; ok { diff --git a/internal/provider/resource_fmc_security_zones_test.go b/internal/provider/resource_fmc_security_zones_test.go index c5e76ab4..c1af8299 100644 --- a/internal/provider/resource_fmc_security_zones_test.go +++ b/internal/provider/resource_fmc_security_zones_test.go @@ -22,7 +22,10 @@ import ( "os" "testing" + "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) // End of section. //template:end imports @@ -84,3 +87,78 @@ func testAccFmcSecurityZonesConfig_all() string { } // End of section. //template:end testAccConfigAll + +func TestAccFmcSecurityZones_Sequential(t *testing.T) { + + step_01 := `resource "fmc_security_zones" "test" {` + "\n" + + ` items = {` + "\n" + + ` "my_security_zones_1" = {` + "\n" + + ` interface_type = "ROUTED",` + "\n" + + ` },` + "\n" + + ` "my_security_zones_2" = {` + "\n" + + ` interface_type = "ROUTED",` + "\n" + + ` },` + "\n" + + ` "my_security_zones_3" = {` + "\n" + + ` interface_type = "ROUTED",` + "\n" + + ` },` + "\n" + + ` } ` + "\n" + + `}` + "\n" + + var checks_step01 []resource.TestCheckFunc + checks_step01 = append(checks_step01, resource.TestCheckResourceAttrSet("fmc_security_zones.test", "items.my_security_zones_1.id")) + checks_step01 = append(checks_step01, resource.TestCheckResourceAttr("fmc_security_zones.test", "items.my_security_zones_1.interface_type", "ROUTED")) + checks_step01 = append(checks_step01, resource.TestCheckResourceAttrSet("fmc_security_zones.test", "items.my_security_zones_2.id")) + checks_step01 = append(checks_step01, resource.TestCheckResourceAttr("fmc_security_zones.test", "items.my_security_zones_2.interface_type", "ROUTED")) + checks_step01 = append(checks_step01, resource.TestCheckResourceAttrSet("fmc_security_zones.test", "items.my_security_zones_3.id")) + checks_step01 = append(checks_step01, resource.TestCheckResourceAttr("fmc_security_zones.test", "items.my_security_zones_3.interface_type", "ROUTED")) + + step_02 := `resource "fmc_security_zones" "test" {` + "\n" + + ` items = {` + "\n" + + ` "my_security_zones_1" = {` + "\n" + + ` interface_type = "ROUTED",` + "\n" + + ` },` + "\n" + + ` "my_security_zones_2" = {` + "\n" + + ` interface_type = "INLINE",` + "\n" + + ` },` + "\n" + + ` "my_security_zones_4" = {` + "\n" + + ` interface_type = "ROUTED",` + "\n" + + ` },` + "\n" + + ` } ` + "\n" + + `}` + "\n" + + var checks_step02 []resource.TestCheckFunc + checks_step02 = append(checks_step02, resource.TestCheckResourceAttrSet("fmc_security_zones.test", "items.my_security_zones_1.id")) + checks_step02 = append(checks_step02, resource.TestCheckResourceAttr("fmc_security_zones.test", "items.my_security_zones_1.interface_type", "ROUTED")) + checks_step02 = append(checks_step02, resource.TestCheckResourceAttrSet("fmc_security_zones.test", "items.my_security_zones_2.id")) + checks_step02 = append(checks_step02, resource.TestCheckResourceAttr("fmc_security_zones.test", "items.my_security_zones_2.interface_type", "INLINE")) + checks_step02 = append(checks_step02, resource.TestCheckNoResourceAttr("fmc_security_zones.test", "items.my_security_zones_3.id")) + checks_step02 = append(checks_step02, resource.TestCheckResourceAttrSet("fmc_security_zones.test", "items.my_security_zones_4.id")) + checks_step02 = append(checks_step02, resource.TestCheckResourceAttr("fmc_security_zones.test", "items.my_security_zones_4.interface_type", "ROUTED")) + + // Zone 1 should not change + zone1Id := statecheck.CompareValue(compare.ValuesSame()) + // Zone 2 should be recreated, due to the change in interface_type + zone2Id := statecheck.CompareValue(compare.ValuesDiffer()) + + steps := []resource.TestStep{{ + Config: step_01, + Check: resource.ComposeTestCheckFunc(checks_step01...), + ConfigStateChecks: []statecheck.StateCheck{ + zone1Id.AddStateValue("fmc_security_zones.test", tfjsonpath.New("items").AtMapKey("my_security_zones_1").AtMapKey("id")), + zone2Id.AddStateValue("fmc_security_zones.test", tfjsonpath.New("items").AtMapKey("my_security_zones_2").AtMapKey("id")), + }, + }, { + Config: step_02, + Check: resource.ComposeTestCheckFunc(checks_step02...), + ConfigStateChecks: []statecheck.StateCheck{ + zone1Id.AddStateValue("fmc_security_zones.test", tfjsonpath.New("items").AtMapKey("my_security_zones_1").AtMapKey("id")), + zone2Id.AddStateValue("fmc_security_zones.test", tfjsonpath.New("items").AtMapKey("my_security_zones_2").AtMapKey("id")), + }, + }} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: steps, + }) +} diff --git a/templates/guides/changelog.md.tmpl b/templates/guides/changelog.md.tmpl index bc4445c6..8a99ac9f 100644 --- a/templates/guides/changelog.md.tmpl +++ b/templates/guides/changelog.md.tmpl @@ -7,6 +7,10 @@ description: |- # Changelog +## 2.0.0-beta2 (Unreleased) + +- (Fix) Change value of `interface_type` within `fmc_security_zones` item should replace just this object, not entire bulk resource + ## 2.0.0-beta1 - Initial release