@@ -29,6 +29,10 @@ import (
2929 "github.com/hashicorp/terraform-plugin-framework/attr"
3030 "github.com/hashicorp/terraform-plugin-framework/diag"
3131 "github.com/hashicorp/terraform-plugin-framework/resource"
32+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
33+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
34+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
35+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
3236 "github.com/hashicorp/terraform-plugin-framework/types"
3337 "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
3438)
@@ -43,8 +47,77 @@ func NewSecurityProjectResource() *Resource[resource_security_project.SecurityPr
4347
4448type securityModelReader struct {}
4549
50+ // productTypesOrderInsensitivePlanModifier ignores order differences in product_types list
51+ type productTypesOrderInsensitivePlanModifier struct {}
52+
53+ func (m productTypesOrderInsensitivePlanModifier ) Description (ctx context.Context ) string {
54+ return "Ignores order differences in product_types list when semantically equivalent"
55+ }
56+
57+ func (m productTypesOrderInsensitivePlanModifier ) MarkdownDescription (ctx context.Context ) string {
58+ return "Ignores order differences in product_types list when semantically equivalent"
59+ }
60+
61+ func (m productTypesOrderInsensitivePlanModifier ) PlanModifyList (ctx context.Context , req planmodifier.ListRequest , resp * planmodifier.ListResponse ) {
62+ // If either value is null or unknown, don't modify
63+ if req .PlanValue .IsNull () || req .PlanValue .IsUnknown () || req .StateValue .IsNull () || req .StateValue .IsUnknown () {
64+ return
65+ }
66+
67+ // Get both lists
68+ var planItems , stateItems []resource_security_project.ProductTypesValue
69+ req .PlanValue .ElementsAs (ctx , & planItems , false )
70+ req .StateValue .ElementsAs (ctx , & stateItems , false )
71+
72+ // If different lengths, they're actually different
73+ if len (planItems ) != len (stateItems ) {
74+ return
75+ }
76+
77+ // Create maps of product_line -> product_tier for comparison
78+ planMap := make (map [string ]string )
79+ for _ , item := range planItems {
80+ planMap [item .ProductLine .ValueString ()] = item .ProductTier .ValueString ()
81+ }
82+
83+ stateMap := make (map [string ]string )
84+ for _ , item := range stateItems {
85+ stateMap [item .ProductLine .ValueString ()] = item .ProductTier .ValueString ()
86+ }
87+
88+ // If maps are equal, use state value (same content, different order)
89+ mapsEqual := len (planMap ) == len (stateMap )
90+ if mapsEqual {
91+ for k , v := range planMap {
92+ if stateMap [k ] != v {
93+ mapsEqual = false
94+ break
95+ }
96+ }
97+ }
98+
99+ if mapsEqual {
100+ resp .PlanValue = req .StateValue
101+ }
102+ }
103+
46104func (sec securityModelReader ) Schema (ctx context.Context , _ resource.SchemaRequest , resp * resource.SchemaResponse ) {
47105 resp .Schema = resource_security_project .SecurityProjectResourceSchema (ctx )
106+
107+ // Add plan modifiers to admin_features_package and product_types to preserve state values
108+ // when these fields are not configured. The API returns these values, and they may change
109+ // over time (e.g., tier upgrades), but if not explicitly configured we should keep the
110+ // current state value rather than forcing a recomputation.
111+ adminFeaturesAttr := resp .Schema .Attributes ["admin_features_package" ].(schema.StringAttribute )
112+ adminFeaturesAttr .PlanModifiers = append (adminFeaturesAttr .PlanModifiers , stringplanmodifier .UseStateForUnknown ())
113+ resp .Schema .Attributes ["admin_features_package" ] = adminFeaturesAttr
114+
115+ productTypesAttr := resp .Schema .Attributes ["product_types" ].(schema.ListNestedAttribute )
116+ productTypesAttr .PlanModifiers = append (productTypesAttr .PlanModifiers ,
117+ listplanmodifier .UseStateForUnknown (),
118+ productTypesOrderInsensitivePlanModifier {},
119+ )
120+ resp .Schema .Attributes ["product_types" ] = productTypesAttr
48121}
49122
50123func (sec securityModelReader ) ReadFrom (ctx context.Context , getter modelGetter ) (* resource_security_project.SecurityProjectModel , diag.Diagnostics ) {
@@ -288,29 +361,80 @@ func (sec securityApi) Read(ctx context.Context, id string, model resource_secur
288361 model .RegionId = basetypes .NewStringValue (resp .JSON200 .RegionId )
289362 model .Type = basetypes .NewStringValue (string (resp .JSON200 .Type ))
290363
291- // Populate admin_features_package from API response (matching pattern used for suspended_reason)
292- var adminFeaturesPkg * string
364+ // Populate admin_features_package from API response when available
293365 if resp .JSON200 .AdminFeaturesPackage != nil {
294366 pkgStr := string (* resp .JSON200 .AdminFeaturesPackage )
295- adminFeaturesPkg = & pkgStr
367+ model .AdminFeaturesPackage = basetypes .NewStringValue (pkgStr )
368+ } else {
369+ model .AdminFeaturesPackage = basetypes .NewStringNull ()
296370 }
297- model .AdminFeaturesPackage = basetypes .NewStringPointerValue (adminFeaturesPkg )
298371
299- // Populate product_types from API response
372+ // Populate product_types from API response when available
300373 if resp .JSON200 .ProductTypes != nil {
374+ // If we have product_types in the state/config, we want to preserve that ordering
375+ // to avoid inconsistent results. Otherwise, use API ordering.
376+ var sourceProductTypes []resource_security_project.ProductTypesValue
377+ if ! model .ProductTypes .IsNull () && ! model .ProductTypes .IsUnknown () {
378+ model .ProductTypes .ElementsAs (ctx , & sourceProductTypes , false )
379+ }
380+
301381 productTypeValues := []attr.Value {}
302- for _ , pt := range * resp .JSON200 .ProductTypes {
303- productTypeValue , diags := resource_security_project .NewProductTypesValue (
304- resource_security_project.ProductTypesValue {}.AttributeTypes (ctx ),
305- map [string ]attr.Value {
306- "product_line" : basetypes .NewStringValue (string (pt .ProductLine )),
307- "product_tier" : basetypes .NewStringValue (string (pt .ProductTier )),
308- },
309- )
310- if diags .HasError () {
311- return false , model , diags
382+
383+ if len (sourceProductTypes ) > 0 {
384+ // Use the ordering from state/config, but with values from API
385+ apiProductTypesMap := make (map [string ]serverless.SecurityProductType )
386+ for _ , pt := range * resp .JSON200 .ProductTypes {
387+ apiProductTypesMap [string (pt .ProductLine )] = pt
388+ }
389+
390+ // Build result in the same order as source
391+ for _ , sourcePt := range sourceProductTypes {
392+ productLine := sourcePt .ProductLine .ValueString ()
393+ if apiPt , exists := apiProductTypesMap [productLine ]; exists {
394+ productTypeValue , diags := resource_security_project .NewProductTypesValue (
395+ resource_security_project.ProductTypesValue {}.AttributeTypes (ctx ),
396+ map [string ]attr.Value {
397+ "product_line" : basetypes .NewStringValue (string (apiPt .ProductLine )),
398+ "product_tier" : basetypes .NewStringValue (string (apiPt .ProductTier )),
399+ },
400+ )
401+ if diags .HasError () {
402+ return false , model , diags
403+ }
404+ productTypeValues = append (productTypeValues , productTypeValue )
405+ delete (apiProductTypesMap , productLine )
406+ }
407+ }
408+
409+ // Add any new product types from API that weren't in source
410+ for _ , apiPt := range apiProductTypesMap {
411+ productTypeValue , diags := resource_security_project .NewProductTypesValue (
412+ resource_security_project.ProductTypesValue {}.AttributeTypes (ctx ),
413+ map [string ]attr.Value {
414+ "product_line" : basetypes .NewStringValue (string (apiPt .ProductLine )),
415+ "product_tier" : basetypes .NewStringValue (string (apiPt .ProductTier )),
416+ },
417+ )
418+ if diags .HasError () {
419+ return false , model , diags
420+ }
421+ productTypeValues = append (productTypeValues , productTypeValue )
422+ }
423+ } else {
424+ // No source ordering, use API ordering
425+ for _ , pt := range * resp .JSON200 .ProductTypes {
426+ productTypeValue , diags := resource_security_project .NewProductTypesValue (
427+ resource_security_project.ProductTypesValue {}.AttributeTypes (ctx ),
428+ map [string ]attr.Value {
429+ "product_line" : basetypes .NewStringValue (string (pt .ProductLine )),
430+ "product_tier" : basetypes .NewStringValue (string (pt .ProductTier )),
431+ },
432+ )
433+ if diags .HasError () {
434+ return false , model , diags
435+ }
436+ productTypeValues = append (productTypeValues , productTypeValue )
312437 }
313- productTypeValues = append (productTypeValues , productTypeValue )
314438 }
315439
316440 productTypesList , diags := types .ListValue (
0 commit comments