Skip to content

Commit 37300d1

Browse files
committed
Preserve configured admin_features_package and product_types when API doesn't return them
1 parent c656c5b commit 37300d1

File tree

2 files changed

+109
-4
lines changed

2 files changed

+109
-4
lines changed

ec/ecresource/projectresource/security.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,17 @@ func (sec securityApi) Read(ctx context.Context, id string, model resource_secur
288288
model.RegionId = basetypes.NewStringValue(resp.JSON200.RegionId)
289289
model.Type = basetypes.NewStringValue(string(resp.JSON200.Type))
290290

291-
// Populate admin_features_package from API response (matching pattern used for suspended_reason)
292-
var adminFeaturesPkg *string
291+
// Populate admin_features_package from API response
293292
if resp.JSON200.AdminFeaturesPackage != nil {
294293
pkgStr := string(*resp.JSON200.AdminFeaturesPackage)
295-
adminFeaturesPkg = &pkgStr
294+
model.AdminFeaturesPackage = basetypes.NewStringValue(pkgStr)
295+
} else if !model.AdminFeaturesPackage.IsNull() && !model.AdminFeaturesPackage.IsUnknown() {
296+
// Preserve the configured value if API doesn't return it (Optional+Computed field behavior)
297+
// The value is already in model.AdminFeaturesPackage from the state/plan, so no assignment needed.
298+
// This handles cases where the API accepts the field on create but doesn't return it on read.
299+
} else {
300+
model.AdminFeaturesPackage = basetypes.NewStringNull()
296301
}
297-
model.AdminFeaturesPackage = basetypes.NewStringPointerValue(adminFeaturesPkg)
298302

299303
// Populate product_types from API response
300304
if resp.JSON200.ProductTypes != nil {
@@ -321,6 +325,10 @@ func (sec securityApi) Read(ctx context.Context, id string, model resource_secur
321325
return false, model, diags
322326
}
323327
model.ProductTypes = productTypesList
328+
} else if !model.ProductTypes.IsNull() && !model.ProductTypes.IsUnknown() {
329+
// Preserve the configured value if API doesn't return it (Optional+Computed field behavior)
330+
// The value is already in model.ProductTypes from the state/plan, so no assignment needed.
331+
// This handles cases where the API accepts the field on create but doesn't return it on read.
324332
} else {
325333
model.ProductTypes = types.ListNull(resource_security_project.ProductTypesValue{}.Type(ctx))
326334
}

ec/ecresource/projectresource/security_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,103 @@ func TestSecurityApi_Read(t *testing.T) {
10891089
ProductTypes: types.ListValueMust(resource_security_project.ProductTypesValue{}.Type(ctx), expectedProductTypes),
10901090
}
10911091

1092+
mockApiClient := mocks.NewMockClientWithResponsesInterface(ctrl)
1093+
mockApiClient.EXPECT().
1094+
GetSecurityProjectWithResponse(ctx, id).
1095+
Return(&serverless.GetSecurityProjectResponse{
1096+
JSON200: readModel,
1097+
}, nil)
1098+
1099+
return testData{
1100+
client: mockApiClient,
1101+
id: id,
1102+
initialModel: initialModel,
1103+
expectedModel: expectedModel,
1104+
expectedFound: true,
1105+
}
1106+
},
1107+
},
1108+
{
1109+
name: "should preserve configured admin_features_package and product_types when API doesn't return them",
1110+
testData: func(ctx context.Context) testData {
1111+
id := "project id"
1112+
1113+
// Initial model has configured values (simulating what comes from the plan/config)
1114+
configuredProductTypes := []attr.Value{
1115+
resource_security_project.NewProductTypesValueMust(
1116+
resource_security_project.ProductTypesValue{}.AttributeTypes(ctx),
1117+
map[string]attr.Value{
1118+
"product_line": basetypes.NewStringValue("security"),
1119+
"product_tier": basetypes.NewStringValue("essentials"),
1120+
},
1121+
),
1122+
resource_security_project.NewProductTypesValueMust(
1123+
resource_security_project.ProductTypesValue{}.AttributeTypes(ctx),
1124+
map[string]attr.Value{
1125+
"product_line": basetypes.NewStringValue("cloud"),
1126+
"product_tier": basetypes.NewStringValue("essentials"),
1127+
},
1128+
),
1129+
}
1130+
1131+
initialModel := resource_security_project.SecurityProjectModel{
1132+
Id: types.StringValue(id),
1133+
AdminFeaturesPackage: basetypes.NewStringValue("standard"),
1134+
ProductTypes: types.ListValueMust(resource_security_project.ProductTypesValue{}.Type(ctx), configuredProductTypes),
1135+
}
1136+
1137+
// API response doesn't include admin_features_package or product_types
1138+
readModel := &serverless.SecurityProject{
1139+
Id: id,
1140+
Alias: "expected-alias-" + id[0:6],
1141+
CloudId: "cloud-id",
1142+
Endpoints: serverless.SecurityProjectEndpoints{
1143+
Elasticsearch: "es-endpoint",
1144+
Kibana: "kib-endpoint",
1145+
Ingest: "ingest-endpoint",
1146+
},
1147+
Metadata: serverless.ProjectMetadata{
1148+
CreatedAt: time.Now(),
1149+
CreatedBy: "me",
1150+
OrganizationId: "1",
1151+
},
1152+
Name: "project-name",
1153+
RegionId: "nether",
1154+
Type: "security",
1155+
AdminFeaturesPackage: nil, // API doesn't return this
1156+
ProductTypes: nil, // API doesn't return this
1157+
}
1158+
1159+
// Expected model should preserve the configured values
1160+
expectedModel := resource_security_project.SecurityProjectModel{
1161+
Id: types.StringValue(id),
1162+
Alias: types.StringValue("expected-alias"),
1163+
CloudId: types.StringValue(readModel.CloudId),
1164+
Endpoints: resource_security_project.NewEndpointsValueMust(
1165+
initialModel.Endpoints.AttributeTypes(ctx),
1166+
map[string]attr.Value{
1167+
"elasticsearch": basetypes.NewStringValue(readModel.Endpoints.Elasticsearch),
1168+
"kibana": basetypes.NewStringValue(readModel.Endpoints.Kibana),
1169+
"ingest": basetypes.NewStringValue(readModel.Endpoints.Ingest),
1170+
},
1171+
),
1172+
Metadata: resource_security_project.NewMetadataValueMust(
1173+
initialModel.Metadata.AttributeTypes(ctx),
1174+
map[string]attr.Value{
1175+
"created_at": basetypes.NewStringValue(readModel.Metadata.CreatedAt.String()),
1176+
"created_by": basetypes.NewStringValue(readModel.Metadata.CreatedBy),
1177+
"organization_id": basetypes.NewStringValue(readModel.Metadata.OrganizationId),
1178+
"suspended_at": basetypes.NewStringNull(),
1179+
"suspended_reason": basetypes.NewStringNull(),
1180+
},
1181+
),
1182+
Name: types.StringValue(readModel.Name),
1183+
RegionId: types.StringValue(readModel.RegionId),
1184+
Type: types.StringValue(string(readModel.Type)),
1185+
AdminFeaturesPackage: basetypes.NewStringValue("standard"),
1186+
ProductTypes: types.ListValueMust(resource_security_project.ProductTypesValue{}.Type(ctx), configuredProductTypes),
1187+
}
1188+
10921189
mockApiClient := mocks.NewMockClientWithResponsesInterface(ctrl)
10931190
mockApiClient.EXPECT().
10941191
GetSecurityProjectWithResponse(ctx, id).

0 commit comments

Comments
 (0)