From f677c3e8646a4d9c7e2cba0e0c139543aa2d2dec Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 16 Apr 2025 18:24:11 -0400 Subject: [PATCH 01/54] provider for custom frameworks --- datadog/fwprovider/framework_provider.go | 2 + .../resource_datadog_custom_framework.go | 385 ++++++++++++++++++ datadog/tests/provider_test.go | 1 + .../resource_datadog_custom_framework_test.go | 83 ++++ .../resource.tf | 16 + go.mod | 2 + 6 files changed, 489 insertions(+) create mode 100644 datadog/fwprovider/resource_datadog_custom_framework.go create mode 100644 datadog/tests/resource_datadog_custom_framework_test.go create mode 100644 examples/resources/datadog_custom_framework_rules/resource.tf diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index 7d0962902..1ec77106c 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -88,6 +88,7 @@ var Resources = []func() resource.Resource{ NewOnCallScheduleResource, NewOnCallTeamRoutingRulesResource, NewSecurityMonitoringRuleJSONResource, + NewCustomFrameworkResource, } var Datasources = []func() datasource.DataSource{ @@ -436,6 +437,7 @@ func defaultConfigureFunc(p *FrameworkProvider, request *provider.ConfigureReque ddClientConfig.SetUnstableOperationEnabled("v2.DeleteAWSAccount", true) ddClientConfig.SetUnstableOperationEnabled("v2.GetAWSAccount", true) ddClientConfig.SetUnstableOperationEnabled("v2.CreateNewAWSExternalID", true) + ddClientConfig.SetUnstableOperationEnabled("v2.CreateCustomFramework", true) // Enable Observability Pipelines ddClientConfig.SetUnstableOperationEnabled("v2.CreatePipeline", true) diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go new file mode 100644 index 000000000..d1545ab04 --- /dev/null +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -0,0 +1,385 @@ +package fwprovider + +import ( + "context" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +var _ resource.Resource = &customFrameworkResource{} + +type customFrameworkResource struct { + Api *datadogV2.SecurityMonitoringApi + Auth context.Context +} + +type customFrameworkModel struct { + ID types.String `tfsdk:"id"` + Description types.String `tfsdk:"description"` + Version types.String `tfsdk:"version"` + Handle types.String `tfsdk:"handle"` + Name types.String `tfsdk:"name"` + IconURL types.String `tfsdk:"icon_url"` + Requirements []requirementModel `tfsdk:"requirements"` +} + +type requirementModel struct { + Name types.String `tfsdk:"name"` + Controls []controlModel `tfsdk:"controls"` +} + +type controlModel struct { + Name types.String `tfsdk:"name"` + RulesID types.List `tfsdk:"rules_id"` +} + +func NewCustomFrameworkResource() resource.Resource { + return &customFrameworkResource{} +} + +func (r *customFrameworkResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "custom_framework" +} + +func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Manages custom framework rules in Datadog.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the custom framework resource.", + Computed: true, + }, + "version": schema.StringAttribute{ + Description: "The framework version.", + Required: true, + }, + "handle": schema.StringAttribute{ + Description: "The framework handle.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The framework name.", + Required: true, + }, + "icon_url": schema.StringAttribute{ + Description: "The URL of the icon representing the framework.", + Optional: true, + }, + "description": schema.StringAttribute{ + Description: "The description of the framework.", + Optional: true, + }, + }, + Blocks: map[string]schema.Block{ + "requirements": schema.ListNestedBlock{ + Description: "The requirements of the framework.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the requirement.", + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "controls": schema.ListNestedBlock{ + Description: "The controls of the requirement.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the control.", + Required: true, + }, + "rules_id": schema.ListAttribute{ + Description: "The list of rules IDs for the control.", + ElementType: types.StringType, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +// func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { +// response.Schema = schema.Schema{ +// Description: "Manages custom framework rules in Datadog.", +// Attributes: map[string]schema.Attribute{ +// "id": schema.StringAttribute{ +// Description: "The ID of the custom framework resource.", +// Computed: true, +// }, +// "version": schema.StringAttribute{ +// Description: "The framework version.", +// Required: true, +// }, +// "handle": schema.StringAttribute{ +// Description: "The framework handle.", +// Required: true, +// }, +// "name": schema.StringAttribute{ +// Description: "The framework name.", +// Required: true, +// }, +// "icon_url": schema.StringAttribute{ +// Description: "The URL of the icon representing the framework.", +// Optional: true, +// }, +// "description": schema.StringAttribute{ +// Description: "The description of the framework.", +// Optional: true, +// }, +// "requirements": schema.ListNestedAttribute{ +// Description: "The requirements of the framework.", +// NestedObject: schema.NestedAttributeObject{ +// Attributes: map[string]schema.Attribute{ +// "name": schema.StringAttribute{ +// Description: "The name of the requirement.", +// Required: true, +// }, +// "controls": schema.ListNestedAttribute{ +// Description: "The controls of the requirement.", +// NestedObject: schema.NestedAttributeObject{ +// Attributes: map[string]schema.Attribute{ +// "name": schema.StringAttribute{ +// Description: "The name of the control.", +// Required: true, +// }, +// "rules_id": schema.ListAttribute{ +// Description: "The list of rules IDs for the control.", +// ElementType: types.StringType, +// Required: true, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// } +// } + +func (r *customFrameworkResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + providerData, _ := request.ProviderData.(*FrameworkProvider) + r.Api = providerData.DatadogApiInstances.GetSecurityMonitoringApiV2() + r.Auth = providerData.Auth +} + +func (r *customFrameworkResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state customFrameworkModel + diags := request.Config.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + _, _, err := r.Api.CreateCustomFramework(ctx, *buildCreateFrameworkRequest(ctx, state)) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating custom framework")) + return + } + + state.ID = types.StringValue(state.Handle.ValueString() + string('-') + state.Version.ValueString()) + diags = response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) +} + +func (r *customFrameworkResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state customFrameworkModel + diags := request.State.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + _, err := r.Api.DeleteCustomFramework(ctx, state.Handle.ValueString(), state.Version.ValueString()) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting framework")) + return + } +} + +func (r *customFrameworkResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state customFrameworkModel + diags := request.State.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + data, _, err := r.Api.RetrieveCustomFramework(ctx, state.Handle.String(), state.Version.String()) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading custom framework")) + return + } + // convert data to state and set it as the new state + if err := utils.CheckForUnparsed(data); err != nil { + response.Diagnostics.AddError("response contains unparsedObject", err.Error()) + return + } + state.ID = types.StringValue(data.GetData().Attributes.Handle + string('-') + data.GetData().Attributes.Version) + state.Description = types.StringValue(data.GetData().Attributes.Description) + state.IconURL = types.StringValue(data.GetData().Attributes.IconUrl) + state.Version = types.StringValue(data.GetData().Attributes.Version) + state.Handle = types.StringValue(data.GetData().Attributes.Handle) + state.Name = types.StringValue(data.GetData().Attributes.Name) + requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements)) + for i, req := range data.GetData().Attributes.Requirements { + controls := make([]attr.Value, len(req.Controls)) + for j, ctrl := range req.Controls { + rulesID := make([]attr.Value, len(ctrl.RulesId)) + for k, ruleID := range ctrl.RulesId { + rulesID[k] = types.StringValue(ruleID) + } + controls[j] = types.ObjectValueMust( + map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.ListType{ElemType: types.StringType}, + }, + map[string]attr.Value{ + "name": types.StringValue(ctrl.Name), + "rules_id": types.ListValueMust(types.StringType, rulesID), + }, + ) + } + requirements[i] = types.ObjectValueMust( + map[string]attr.Type{ + "name": types.StringType, + "controls": types.ListType{ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.ListType{ElemType: types.StringType}, + }, + }}, + }, + map[string]attr.Value{ + "name": types.StringValue(req.Name), + "controls": types.ListValueMust(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.ListType{ElemType: types.StringType}, + }, + }, controls), + }, + ) + } + // state.Requirements = types.ListValueMust( + // types.ObjectType{ + // AttrTypes: map[string]attr.Type{ + // "name": types.StringType, + // "controls": types.ListType{ElemType: types.ObjectType{ + // AttrTypes: map[string]attr.Type{ + // "name": types.StringType, + // "rules_id": types.ListType{ElemType: types.StringType}, + // }, + // }}, + // }, + // }, + // requirements, + // ) + + diags = response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) +} + +func (r *customFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state customFrameworkModel + diags := request.Config.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + _, _, err := r.Api.UpdateCustomFramework(ctx, state.Handle.String(), state.Version.String(), *buildUpdateFrameworkRequest(ctx, state)) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating custom framework")) + return + } + diags = response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) +} + +func buildCreateFrameworkRequest(ctx context.Context, state customFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { + createFrameworkRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() + description := state.Description.ValueString() + iconURL := state.IconURL.ValueString() + createFrameworkRequest.SetData(datadogV2.CustomFrameworkData{ + Type: "custom_framework", + Attributes: datadogV2.CustomFrameworkDataAttributes{ + Handle: state.Handle.ValueString(), + Name: state.Name.ValueString(), + Description: &description, + IconUrl: &iconURL, + Version: state.Version.ValueString(), + Requirements: func() []datadogV2.CustomFrameworkRequirement { + requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements)) + for i, req := range state.Requirements { + controls := make([]datadogV2.CustomFrameworkControl, len(req.Controls)) + for j, ctrl := range req.Controls { + rulesID := make([]string, len(ctrl.RulesID.Elements())) + for k, ruleID := range ctrl.RulesID.Elements() { + rulesID[k] = ruleID.(types.String).ValueString() + } + controls[j] = datadogV2.CustomFrameworkControl{ + Name: ctrl.Name.ValueString(), + RulesId: rulesID, + } + } + requirements[i] = datadogV2.CustomFrameworkRequirement{ + Name: req.Name.ValueString(), + Controls: controls, + } + } + return requirements + }(), + }, + }) + return createFrameworkRequest +} + +func buildUpdateFrameworkRequest(ctx context.Context, state customFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest { + updateFrameworkRequest := datadogV2.NewUpdateCustomFrameworkRequestWithDefaults() + description := state.Description.ValueString() + iconURL := state.IconURL.ValueString() + updateFrameworkRequest.SetData(datadogV2.CustomFrameworkData{ + Type: "custom_framework", + Attributes: datadogV2.CustomFrameworkDataAttributes{ + Handle: state.Handle.ValueString(), + Name: state.Name.ValueString(), + Description: &description, + IconUrl: &iconURL, + Version: state.Version.ValueString(), + Requirements: func() []datadogV2.CustomFrameworkRequirement { + requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements)) + for i, req := range state.Requirements { + controls := make([]datadogV2.CustomFrameworkControl, len(req.Controls)) + for j, ctrl := range req.Controls { + rulesID := make([]string, len(ctrl.RulesID.Elements())) + for k, ruleID := range ctrl.RulesID.Elements() { + rulesID[k] = ruleID.(types.String).ValueString() + } + controls[j] = datadogV2.CustomFrameworkControl{ + Name: ctrl.Name.ValueString(), + RulesId: rulesID, + } + } + requirements[i] = datadogV2.CustomFrameworkRequirement{ + Name: req.Name.ValueString(), + Controls: controls, + } + } + return requirements + }(), + }, + }) + return updateFrameworkRequest +} diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go index 51b5af5bb..a32bb133d 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -270,6 +270,7 @@ var testFiles2EndpointTags = map[string]string{ "tests/resource_datadog_webhook_custom_variable_test": "webhook_custom_variable", "tests/resource_datadog_webhook_test": "webhook", "tests/resource_datadog_workflow_automation_test": "workflow_automation", + "tests/resource_datadog_custom_framework_test": "custom_framework", } // getEndpointTagValue traverses callstack frames to find the test function that invoked this call; diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go new file mode 100644 index 000000000..3f36e436f --- /dev/null +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -0,0 +1,83 @@ +package test + +import ( + "context" + "fmt" + "math/rand" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" +) + +func TestCustomFramework_bgasic(t *testing.T) { + t.Parallel() + handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) + version := "1.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFramework(handle, version), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "name", "test-framework"), + resource.TestCheckResourceAttr(path, "description", "test description"), + resource.TestCheckResourceAttr(path, "icon_url", "test url"), + resource.TestCheckResourceAttr(path, "requirements.#", "1"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.#", "1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckDatadogCreateFramework(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwprovider.FrameworkProvider, resourceName string, version string, handle string) func(*terraform.State) error { + return func(s *terraform.State) error { + apiInstances := accProvider.DatadogApiInstances + resource := s.RootModule().Resources[resourceName] + handle := resource.Primary.Attributes["handle"] + version := resource.Primary.Attributes["version"] + _, httpRes, err := apiInstances.GetSecurityMonitoringApiV2().RetrieveCustomFramework(ctx, handle, version) + if err != nil { + if httpRes.StatusCode == 400 { + return nil + } + return err + } + + return fmt.Errorf("framework destroy check failed") + } +} diff --git a/examples/resources/datadog_custom_framework_rules/resource.tf b/examples/resources/datadog_custom_framework_rules/resource.tf new file mode 100644 index 000000000..0e2d95190 --- /dev/null +++ b/examples/resources/datadog_custom_framework_rules/resource.tf @@ -0,0 +1,16 @@ +resource "datadog_custom_framework" "example" { + version = "1.0.0" + handle = "example_handle" + name = "example_name" + icon_url = "https://example.com/icon.png" + description = "description" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["rule1", "rule2"] + } + } +} + + diff --git a/go.mod b/go.mod index d6a9ed14c..2b31ea5b9 100644 --- a/go.mod +++ b/go.mod @@ -101,4 +101,6 @@ require ( google.golang.org/protobuf v1.36.3 // indirect ) +replace github.com/DataDog/datadog-api-client-go/v2 v2.36.1 => ../datadog-api-spec/generated/datadog-api-client-go + go 1.23.0 From 10203d385de18df0c16aba4bdae887f6890e68e1 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 23 Apr 2025 23:56:59 -0400 Subject: [PATCH 02/54] passed set create test --- .../resource_datadog_custom_framework.go | 132 ++++++++--------- .../resource_datadog_custom_framework_test.go | 134 ++++++++++++++++-- 2 files changed, 180 insertions(+), 86 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index d1545ab04..86ceda12d 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -20,23 +20,13 @@ type customFrameworkResource struct { } type customFrameworkModel struct { - ID types.String `tfsdk:"id"` - Description types.String `tfsdk:"description"` - Version types.String `tfsdk:"version"` - Handle types.String `tfsdk:"handle"` - Name types.String `tfsdk:"name"` - IconURL types.String `tfsdk:"icon_url"` - Requirements []requirementModel `tfsdk:"requirements"` -} - -type requirementModel struct { - Name types.String `tfsdk:"name"` - Controls []controlModel `tfsdk:"controls"` -} - -type controlModel struct { - Name types.String `tfsdk:"name"` - RulesID types.List `tfsdk:"rules_id"` + ID types.String `tfsdk:"id"` + Description types.String `tfsdk:"description"` + Version types.String `tfsdk:"version"` + Handle types.String `tfsdk:"handle"` + Name types.String `tfsdk:"name"` + IconURL types.String `tfsdk:"icon_url"` + Requirements types.Set `tfsdk:"requirements"` } func NewCustomFrameworkResource() resource.Resource { @@ -77,7 +67,7 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq }, }, Blocks: map[string]schema.Block{ - "requirements": schema.ListNestedBlock{ + "requirements": schema.SetNestedBlock{ Description: "The requirements of the framework.", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ @@ -87,7 +77,7 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq }, }, Blocks: map[string]schema.Block{ - "controls": schema.ListNestedBlock{ + "controls": schema.SetNestedBlock{ Description: "The controls of the requirement.", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ @@ -95,7 +85,7 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq Description: "The name of the control.", Required: true, }, - "rules_id": schema.ListAttribute{ + "rules_id": schema.SetAttribute{ Description: "The list of rules IDs for the control.", ElementType: types.StringType, Required: true, @@ -183,12 +173,11 @@ func (r *customFrameworkResource) Create(ctx context.Context, request resource.C return } - _, _, err := r.Api.CreateCustomFramework(ctx, *buildCreateFrameworkRequest(ctx, state)) + _, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(ctx, state)) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating custom framework")) return } - state.ID = types.StringValue(state.Handle.ValueString() + string('-') + state.Version.ValueString()) diags = response.State.Set(ctx, &state) response.Diagnostics.Append(diags...) @@ -201,7 +190,7 @@ func (r *customFrameworkResource) Delete(ctx context.Context, request resource.D if response.Diagnostics.HasError() { return } - _, err := r.Api.DeleteCustomFramework(ctx, state.Handle.ValueString(), state.Version.ValueString()) + _, _, err := r.Api.DeleteCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting framework")) return @@ -216,24 +205,22 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea return } - data, _, err := r.Api.RetrieveCustomFramework(ctx, state.Handle.String(), state.Version.String()) + data, _, err := r.Api.RetrieveCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading custom framework")) return } - // convert data to state and set it as the new state - if err := utils.CheckForUnparsed(data); err != nil { - response.Diagnostics.AddError("response contains unparsedObject", err.Error()) - return - } state.ID = types.StringValue(data.GetData().Attributes.Handle + string('-') + data.GetData().Attributes.Version) state.Description = types.StringValue(data.GetData().Attributes.Description) state.IconURL = types.StringValue(data.GetData().Attributes.IconUrl) state.Version = types.StringValue(data.GetData().Attributes.Version) state.Handle = types.StringValue(data.GetData().Attributes.Handle) state.Name = types.StringValue(data.GetData().Attributes.Name) + + // Convert requirements to set requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements)) for i, req := range data.GetData().Attributes.Requirements { + // Convert controls to set controls := make([]attr.Value, len(req.Controls)) for j, ctrl := range req.Controls { rulesID := make([]attr.Value, len(ctrl.RulesId)) @@ -243,50 +230,41 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea controls[j] = types.ObjectValueMust( map[string]attr.Type{ "name": types.StringType, - "rules_id": types.ListType{ElemType: types.StringType}, + "rules_id": types.SetType{ElemType: types.StringType}, }, map[string]attr.Value{ "name": types.StringValue(ctrl.Name), - "rules_id": types.ListValueMust(types.StringType, rulesID), + "rules_id": types.SetValueMust(types.StringType, rulesID), }, ) } requirements[i] = types.ObjectValueMust( map[string]attr.Type{ "name": types.StringType, - "controls": types.ListType{ElemType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.ListType{ElemType: types.StringType}, - }, - }}, + "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }}}, }, map[string]attr.Value{ "name": types.StringValue(req.Name), - "controls": types.ListValueMust(types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.ListType{ElemType: types.StringType}, - }, - }, controls), + "controls": types.SetValueMust(types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }}, controls), }, ) } - // state.Requirements = types.ListValueMust( - // types.ObjectType{ - // AttrTypes: map[string]attr.Type{ - // "name": types.StringType, - // "controls": types.ListType{ElemType: types.ObjectType{ - // AttrTypes: map[string]attr.Type{ - // "name": types.StringType, - // "rules_id": types.ListType{ElemType: types.StringType}, - // }, - // }}, - // }, - // }, - // requirements, - // ) - + state.Requirements = types.SetValueMust( + types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }}}, + }}, + requirements, + ) diags = response.State.Set(ctx, &state) response.Diagnostics.Append(diags...) } @@ -299,7 +277,7 @@ func (r *customFrameworkResource) Update(ctx context.Context, request resource.U return } - _, _, err := r.Api.UpdateCustomFramework(ctx, state.Handle.String(), state.Version.String(), *buildUpdateFrameworkRequest(ctx, state)) + _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(ctx, state)) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating custom framework")) return @@ -321,21 +299,23 @@ func buildCreateFrameworkRequest(ctx context.Context, state customFrameworkModel IconUrl: &iconURL, Version: state.Version.ValueString(), Requirements: func() []datadogV2.CustomFrameworkRequirement { - requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements)) - for i, req := range state.Requirements { - controls := make([]datadogV2.CustomFrameworkControl, len(req.Controls)) - for j, ctrl := range req.Controls { - rulesID := make([]string, len(ctrl.RulesID.Elements())) - for k, ruleID := range ctrl.RulesID.Elements() { + requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements.Elements())) + for i, req := range state.Requirements.Elements() { + reqObj := req.(types.Object) + controls := make([]datadogV2.CustomFrameworkControl, len(reqObj.Attributes()["controls"].(types.Set).Elements())) + for j, ctrl := range reqObj.Attributes()["controls"].(types.Set).Elements() { + ctrlObj := ctrl.(types.Object) + rulesID := make([]string, len(ctrlObj.Attributes()["rules_id"].(types.Set).Elements())) + for k, ruleID := range ctrlObj.Attributes()["rules_id"].(types.Set).Elements() { rulesID[k] = ruleID.(types.String).ValueString() } controls[j] = datadogV2.CustomFrameworkControl{ - Name: ctrl.Name.ValueString(), + Name: ctrlObj.Attributes()["name"].(types.String).ValueString(), RulesId: rulesID, } } requirements[i] = datadogV2.CustomFrameworkRequirement{ - Name: req.Name.ValueString(), + Name: reqObj.Attributes()["name"].(types.String).ValueString(), Controls: controls, } } @@ -359,21 +339,23 @@ func buildUpdateFrameworkRequest(ctx context.Context, state customFrameworkModel IconUrl: &iconURL, Version: state.Version.ValueString(), Requirements: func() []datadogV2.CustomFrameworkRequirement { - requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements)) - for i, req := range state.Requirements { - controls := make([]datadogV2.CustomFrameworkControl, len(req.Controls)) - for j, ctrl := range req.Controls { - rulesID := make([]string, len(ctrl.RulesID.Elements())) - for k, ruleID := range ctrl.RulesID.Elements() { + requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements.Elements())) + for i, req := range state.Requirements.Elements() { + reqObj := req.(types.Object) + controls := make([]datadogV2.CustomFrameworkControl, len(reqObj.Attributes()["controls"].(types.Set).Elements())) + for j, ctrl := range reqObj.Attributes()["controls"].(types.Set).Elements() { + ctrlObj := ctrl.(types.Object) + rulesID := make([]string, len(ctrlObj.Attributes()["rules_id"].(types.Set).Elements())) + for k, ruleID := range ctrlObj.Attributes()["rules_id"].(types.Set).Elements() { rulesID[k] = ruleID.(types.String).ValueString() } controls[j] = datadogV2.CustomFrameworkControl{ - Name: ctrl.Name.ValueString(), + Name: ctrlObj.Attributes()["name"].(types.String).ValueString(), RulesId: rulesID, } } requirements[i] = datadogV2.CustomFrameworkRequirement{ - Name: req.Name.ValueString(), + Name: reqObj.Attributes()["name"].(types.String).ValueString(), Controls: controls, } } diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 3f36e436f..56e5dfb50 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -12,10 +12,10 @@ import ( "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" ) -func TestCustomFramework_bgasic(t *testing.T) { +func TestCustomFramework_create(t *testing.T) { t.Parallel() handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := "1.0" + version := fmt.Sprintf("version-%d", rand.Intn(100000)) ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -25,26 +25,138 @@ func TestCustomFramework_bgasic(t *testing.T) { CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - Config: testAccCheckDatadogCreateFramework(handle, version), + Config: testAccCheckDatadogCreateFramework(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "name", "test-framework"), + resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), resource.TestCheckResourceAttr(path, "description", "test description"), resource.TestCheckResourceAttr(path, "icon_url", "test url"), - resource.TestCheckResourceAttr(path, "requirements.#", "1"), - resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.#", "1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control1", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + ), + }, + { + Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "compliance-requirement", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "security-requirement", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "compliance-control", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "security-control", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + ), + }, + }, + }) +} + +func TestCustomFramework_delete(t *testing.T) { + t.Parallel() + handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) + version := fmt.Sprintf("version-%d", rand.Intn(100000)) + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + }, + { + ResourceName: path, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestCustomFramework_updateMultipleRequirements(t *testing.T) { + t.Parallel() + handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) + version := fmt.Sprintf("version-%d", rand.Intn(100000)) + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "compliance-requirement", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "security-requirement", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "compliance-control", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "security-control", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), ), }, }, }) } +func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-name" + description = "test description" + icon_url = "test url" + requirements { + name = "security-requirement" + controls { + name = "security-control" + rules_id = ["def-000-cea", "def-000-be9"] + } + } + requirements { + name = "compliance-requirement" + controls { + name = "compliance-control" + rules_id = ["def-000-be9", "def-000-cea"] + } + } + } + `, version, handle) +} + func testAccCheckDatadogCreateFramework(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { From 1ff1b16858c140a66d2753b91f7f7b0c79dabb92 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 24 Apr 2025 17:31:43 -0400 Subject: [PATCH 03/54] clean up code and add tests --- .../resource_datadog_custom_framework.go | 164 +++++------------- .../resource_datadog_custom_framework_test.go | 145 +++++++++++++++- 2 files changed, 188 insertions(+), 121 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index 86ceda12d..0183190f1 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -26,9 +26,16 @@ type customFrameworkModel struct { Handle types.String `tfsdk:"handle"` Name types.String `tfsdk:"name"` IconURL types.String `tfsdk:"icon_url"` - Requirements types.Set `tfsdk:"requirements"` + Requirements types.Set `tfsdk:"requirements"` // have to define requirements as a set to be unordered } +// the only way I could get the requirements to be unordered was to define them as a set in Terraform :( +// if I could define requirements as a list, I could use the following: +// type requirementModel struct { +// Name types.String `tfsdk:"name"` +// Controls types.Set `tfsdk:"controls"` +// } + func NewCustomFrameworkResource() resource.Resource { return &customFrameworkResource{} } @@ -100,65 +107,6 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq } } -// func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { -// response.Schema = schema.Schema{ -// Description: "Manages custom framework rules in Datadog.", -// Attributes: map[string]schema.Attribute{ -// "id": schema.StringAttribute{ -// Description: "The ID of the custom framework resource.", -// Computed: true, -// }, -// "version": schema.StringAttribute{ -// Description: "The framework version.", -// Required: true, -// }, -// "handle": schema.StringAttribute{ -// Description: "The framework handle.", -// Required: true, -// }, -// "name": schema.StringAttribute{ -// Description: "The framework name.", -// Required: true, -// }, -// "icon_url": schema.StringAttribute{ -// Description: "The URL of the icon representing the framework.", -// Optional: true, -// }, -// "description": schema.StringAttribute{ -// Description: "The description of the framework.", -// Optional: true, -// }, -// "requirements": schema.ListNestedAttribute{ -// Description: "The requirements of the framework.", -// NestedObject: schema.NestedAttributeObject{ -// Attributes: map[string]schema.Attribute{ -// "name": schema.StringAttribute{ -// Description: "The name of the requirement.", -// Required: true, -// }, -// "controls": schema.ListNestedAttribute{ -// Description: "The controls of the requirement.", -// NestedObject: schema.NestedAttributeObject{ -// Attributes: map[string]schema.Attribute{ -// "name": schema.StringAttribute{ -// Description: "The name of the control.", -// Required: true, -// }, -// "rules_id": schema.ListAttribute{ -// Description: "The list of rules IDs for the control.", -// ElementType: types.StringType, -// Required: true, -// }, -// }, -// }, -// }, -// }, -// }, -// }, -// }, -// } -// } - func (r *customFrameworkResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { providerData, _ := request.ProviderData.(*FrameworkProvider) r.Api = providerData.DatadogApiInstances.GetSecurityMonitoringApiV2() @@ -173,7 +121,7 @@ func (r *customFrameworkResource) Create(ctx context.Context, request resource.C return } - _, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(ctx, state)) + _, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating custom framework")) return @@ -238,6 +186,7 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea }, ) } + // set requirement requirements[i] = types.ObjectValueMust( map[string]attr.Type{ "name": types.StringType, @@ -255,6 +204,7 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea }, ) } + // set state requirements state.Requirements = types.SetValueMust( types.ObjectType{AttrTypes: map[string]attr.Type{ "name": types.StringType, @@ -277,7 +227,7 @@ func (r *customFrameworkResource) Update(ctx context.Context, request resource.U return } - _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(ctx, state)) + _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating custom framework")) return @@ -286,81 +236,55 @@ func (r *customFrameworkResource) Update(ctx context.Context, request resource.U response.Diagnostics.Append(diags...) } -func buildCreateFrameworkRequest(ctx context.Context, state customFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { +func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []datadogV2.CustomFrameworkRequirement { + frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements.Elements())) + for i, requirement := range requirements.Elements() { + requirementState := requirement.(types.Object) + controls := make([]datadogV2.CustomFrameworkControl, len(requirementState.Attributes()["controls"].(types.Set).Elements())) + for j, control := range requirementState.Attributes()["controls"].(types.Set).Elements() { + controlState := control.(types.Object) + rulesID := make([]string, len(controlState.Attributes()["rules_id"].(types.Set).Elements())) + for k, ruleID := range controlState.Attributes()["rules_id"].(types.Set).Elements() { + rulesID[k] = ruleID.(types.String).ValueString() + } + controls[j] = *datadogV2.NewCustomFrameworkControl(controlState.Attributes()["name"].(types.String).ValueString(), rulesID) + } + frameworkRequirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, requirementState.Attributes()["name"].(types.String).ValueString()) + } + return frameworkRequirements +} + +func buildCreateFrameworkRequest(state customFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { createFrameworkRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() description := state.Description.ValueString() iconURL := state.IconURL.ValueString() createFrameworkRequest.SetData(datadogV2.CustomFrameworkData{ Type: "custom_framework", Attributes: datadogV2.CustomFrameworkDataAttributes{ - Handle: state.Handle.ValueString(), - Name: state.Name.ValueString(), - Description: &description, - IconUrl: &iconURL, - Version: state.Version.ValueString(), - Requirements: func() []datadogV2.CustomFrameworkRequirement { - requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements.Elements())) - for i, req := range state.Requirements.Elements() { - reqObj := req.(types.Object) - controls := make([]datadogV2.CustomFrameworkControl, len(reqObj.Attributes()["controls"].(types.Set).Elements())) - for j, ctrl := range reqObj.Attributes()["controls"].(types.Set).Elements() { - ctrlObj := ctrl.(types.Object) - rulesID := make([]string, len(ctrlObj.Attributes()["rules_id"].(types.Set).Elements())) - for k, ruleID := range ctrlObj.Attributes()["rules_id"].(types.Set).Elements() { - rulesID[k] = ruleID.(types.String).ValueString() - } - controls[j] = datadogV2.CustomFrameworkControl{ - Name: ctrlObj.Attributes()["name"].(types.String).ValueString(), - RulesId: rulesID, - } - } - requirements[i] = datadogV2.CustomFrameworkRequirement{ - Name: reqObj.Attributes()["name"].(types.String).ValueString(), - Controls: controls, - } - } - return requirements - }(), + Handle: state.Handle.ValueString(), + Name: state.Name.ValueString(), + Description: &description, + IconUrl: &iconURL, + Version: state.Version.ValueString(), + Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements), }, }) return createFrameworkRequest } -func buildUpdateFrameworkRequest(ctx context.Context, state customFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest { +func buildUpdateFrameworkRequest(state customFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest { updateFrameworkRequest := datadogV2.NewUpdateCustomFrameworkRequestWithDefaults() description := state.Description.ValueString() iconURL := state.IconURL.ValueString() updateFrameworkRequest.SetData(datadogV2.CustomFrameworkData{ Type: "custom_framework", Attributes: datadogV2.CustomFrameworkDataAttributes{ - Handle: state.Handle.ValueString(), - Name: state.Name.ValueString(), - Description: &description, - IconUrl: &iconURL, - Version: state.Version.ValueString(), - Requirements: func() []datadogV2.CustomFrameworkRequirement { - requirements := make([]datadogV2.CustomFrameworkRequirement, len(state.Requirements.Elements())) - for i, req := range state.Requirements.Elements() { - reqObj := req.(types.Object) - controls := make([]datadogV2.CustomFrameworkControl, len(reqObj.Attributes()["controls"].(types.Set).Elements())) - for j, ctrl := range reqObj.Attributes()["controls"].(types.Set).Elements() { - ctrlObj := ctrl.(types.Object) - rulesID := make([]string, len(ctrlObj.Attributes()["rules_id"].(types.Set).Elements())) - for k, ruleID := range ctrlObj.Attributes()["rules_id"].(types.Set).Elements() { - rulesID[k] = ruleID.(types.String).ValueString() - } - controls[j] = datadogV2.CustomFrameworkControl{ - Name: ctrlObj.Attributes()["name"].(types.String).ValueString(), - RulesId: rulesID, - } - } - requirements[i] = datadogV2.CustomFrameworkRequirement{ - Name: reqObj.Attributes()["name"].(types.String).ValueString(), - Controls: controls, - } - } - return requirements - }(), + Handle: state.Handle.ValueString(), + Name: state.Name.ValueString(), + Description: &description, + IconUrl: &iconURL, + Version: state.Version.ValueString(), + Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements), }, }) return updateFrameworkRequest diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 56e5dfb50..e2d56ed01 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -94,7 +94,7 @@ func TestCustomFramework_delete(t *testing.T) { }) } -func TestCustomFramework_updateMultipleRequirements(t *testing.T) { +func TestCustomFramework_createMultipleRequirements(t *testing.T) { t.Parallel() handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) version := fmt.Sprintf("version-%d", rand.Intn(100000)) @@ -131,6 +131,149 @@ func TestCustomFramework_updateMultipleRequirements(t *testing.T) { }) } +func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { + t.Parallel() + handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) + version := fmt.Sprintf("version-%d", rand.Intn(100000)) + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + + // First config with one order of requirements + config1 := fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement2" + controls { + name = "control2" + rules_id = ["def-000-cea"] + } + } + requirements { + name = "requirement3" + controls { + name = "control3" + rules_id = ["def-000-be9", "def-000-cea"] + } + } + } + `, version, handle) + + // Second config with different order of requirements + config2 := fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement3" + controls { + name = "control3" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement2" + controls { + name = "control2" + rules_id = ["def-000-be9", "def-000-cea"] + } + } + } + `, version, handle) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: config1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), + resource.TestCheckResourceAttr(path, "description", "test description"), + resource.TestCheckResourceAttr(path, "icon_url", "test url"), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement2", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement3", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control2", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control3", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + ), + }, + { + Config: config2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), + resource.TestCheckResourceAttr(path, "description", "test description"), + resource.TestCheckResourceAttr(path, "icon_url", "test url"), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement2", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement3", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control2", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control3", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + ), + // This step should not trigger an update since only the order is different + ExpectNonEmptyPlan: false, + }, + }, + }) +} + func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { From caa0f705a73b9d87cf6cb5ba24d91f87a5f52a9d Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 24 Apr 2025 17:54:25 -0400 Subject: [PATCH 04/54] add invalid create framework tests --- .../resource_datadog_custom_framework_test.go | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index e2d56ed01..7e93390bc 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/rand" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -274,6 +275,34 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { }) } +func TestCustomFramework_InvalidCreate(t *testing.T) { + t.Parallel() + handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) + version := fmt.Sprintf("version-%d", rand.Intn(100000)) + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateInvalidFramework(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + { + Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + { + Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + }, + }) +} + func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { @@ -319,6 +348,56 @@ func testAccCheckDatadogCreateFramework(version string, handle string) string { `, version, handle) } +func testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["invalid-rule-id"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + description = "test description" + icon_url = "test url" + } + `, version, handle) +} + +func testAccCheckDatadogCreateInvalidFramework(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "" # Invalid empty name + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwprovider.FrameworkProvider, resourceName string, version string, handle string) func(*terraform.State) error { return func(s *terraform.State) error { apiInstances := accProvider.DatadogApiInstances From 9016c43f8db1a662b8e9339dde6baf90040af0d3 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 24 Apr 2025 17:56:17 -0400 Subject: [PATCH 05/54] test files --- .../TestCustomFramework_InvalidCreate.freeze | 1 + .../TestCustomFramework_InvalidCreate.yaml | 111 ++++++++ .../TestCustomFramework_create.freeze | 1 + .../cassettes/TestCustomFramework_create.yaml | 240 ++++++++++++++++++ ...ramework_createMultipleRequirements.freeze | 1 + ...mFramework_createMultipleRequirements.yaml | 138 ++++++++++ .../TestCustomFramework_invalid.freeze | 1 + .../TestCustomFramework_invalid.yaml | 39 +++ ...tCustomFramework_sameConfigNoUpdate.freeze | 1 + ...estCustomFramework_sameConfigNoUpdate.yaml | 240 ++++++++++++++++++ 10 files changed, 773 insertions(+) create mode 100644 datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_create.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_create.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_invalid.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_invalid.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze new file mode 100644 index 000000000..92854bb04 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -0,0 +1 @@ +2025-04-24T17:54:06.626222-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml new file mode 100644 index 000000000..e4613b5bc --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -0,0 +1,111 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 268 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-31996","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-30568"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 158 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.name'' is invalid: field ''name'' must not be empty)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 88.458458ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 295 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-31996","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"version-30568"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 116 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"invalid_argument(Cannot find rule id invalid-rule-id)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 216.985459ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 208 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-31996","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"version-30568"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 171 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.requirements'' is invalid: field ''requirements'' can''t be empty)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 51.733334ms diff --git a/datadog/tests/cassettes/TestCustomFramework_create.freeze b/datadog/tests/cassettes/TestCustomFramework_create.freeze new file mode 100644 index 000000000..325628c47 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_create.freeze @@ -0,0 +1 @@ +2025-04-24T17:25:32.385002-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_create.yaml b/datadog/tests/cassettes/TestCustomFramework_create.yaml new file mode 100644 index 000000000..1df75a154 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_create.yaml @@ -0,0 +1,240 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 291 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-57607","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-55264"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"handle":"handle-57607","version":"version-55264"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 537.934916ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 429 + uncompressed: false + body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529933617,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-55264"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 180.98925ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 429 + uncompressed: false + body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529933617,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-55264"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 158.478917ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 424 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-57607","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-55264"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"handle":"handle-57607","version":"version-55264"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 723.31775ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 562 + uncompressed: false + body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529935488,"name":"new-name","org_id":321813,"requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-55264"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 278.224875ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 313 + uncompressed: false + body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529935488,"name":"new-name","org_id":321813,"version":"version-55264"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 414.365ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 47.912916ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze new file mode 100644 index 000000000..d1e314238 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze @@ -0,0 +1 @@ +2025-04-24T17:25:32.385502-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml new file mode 100644 index 000000000..b3f9fb2bc --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml @@ -0,0 +1,138 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 424 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-11209","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-72175"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-11209-version-72175","type":"custom_framework","attributes":{"handle":"handle-11209","version":"version-72175"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 522.858166ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-11209/version-72175 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 562 + uncompressed: false + body: '{"data":{"id":"handle-11209-version-72175","type":"custom_framework","attributes":{"created_at":1745529933596,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-11209","icon_url":"test url","modified_at":1745529933596,"name":"new-name","org_id":321813,"requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-72175"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 276.719833ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-11209/version-72175 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 313 + uncompressed: false + body: '{"data":{"id":"handle-11209-version-72175","type":"custom_framework","attributes":{"created_at":1745529933596,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-11209","icon_url":"test url","modified_at":1745529933596,"name":"new-name","org_id":321813,"version":"version-72175"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 446.025ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-11209/version-72175 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 60.071334ms diff --git a/datadog/tests/cassettes/TestCustomFramework_invalid.freeze b/datadog/tests/cassettes/TestCustomFramework_invalid.freeze new file mode 100644 index 000000000..ee4a6ba52 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_invalid.freeze @@ -0,0 +1 @@ +2025-04-24T17:48:03.195086-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_invalid.yaml b/datadog/tests/cassettes/TestCustomFramework_invalid.yaml new file mode 100644 index 000000000..f7454a2b1 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_invalid.yaml @@ -0,0 +1,39 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 268 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-88933","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-93914"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 158 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.name'' is invalid: field ''name'' must not be empty)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 99.919834ms diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze new file mode 100644 index 000000000..ce7f12aa3 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze @@ -0,0 +1 @@ +2025-04-24T17:31:00.07411-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml new file mode 100644 index 000000000..3619a50a8 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml @@ -0,0 +1,240 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 473 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-25727","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"version-35668"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"handle":"handle-25727","version":"version-35668"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 388.449292ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 611 + uncompressed: false + body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530261258,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-35668"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 390.4165ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 611 + uncompressed: false + body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530261258,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-35668"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 215.097208ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 473 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-25727","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9"]}],"name":"requirement3"}],"version":"version-35668"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"handle":"handle-25727","version":"version-35668"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 798.598167ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 611 + uncompressed: false + body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530263373,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9"]}]}],"version":"version-35668"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 485.902167ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 328 + uncompressed: false + body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530263373,"name":"new-framework-terraform","org_id":321813,"version":"version-35668"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 425.181542ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 55.792917ms From b234607da5d4d0253703959eecfdd0e8885d9f1a Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 25 Apr 2025 14:11:29 -0400 Subject: [PATCH 06/54] add import state functionality --- .../resource_datadog_custom_framework.go | 70 ++++++++++++------- .../datadog_custom_framework/import.sh | 1 + .../datadog_custom_framework/main.tf | 31 ++++++++ .../datadog_custom_framework/variables.tf | 11 +++ .../resource.tf | 16 ----- 5 files changed, 89 insertions(+), 40 deletions(-) create mode 100644 examples/resources/datadog_custom_framework/import.sh create mode 100644 examples/resources/datadog_custom_framework/main.tf create mode 100644 examples/resources/datadog_custom_framework/variables.tf delete mode 100644 examples/resources/datadog_custom_framework_rules/resource.tf diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index 0183190f1..c83938b51 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -2,6 +2,7 @@ package fwprovider import ( "context" + "strings" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -29,13 +30,6 @@ type customFrameworkModel struct { Requirements types.Set `tfsdk:"requirements"` // have to define requirements as a set to be unordered } -// the only way I could get the requirements to be unordered was to define them as a set in Terraform :( -// if I could define requirements as a list, I could use the following: -// type requirementModel struct { -// Name types.String `tfsdk:"name"` -// Controls types.Set `tfsdk:"controls"` -// } - func NewCustomFrameworkResource() resource.Resource { return &customFrameworkResource{} } @@ -122,6 +116,7 @@ func (r *customFrameworkResource) Create(ctx context.Context, request resource.C } _, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) + if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating custom framework")) return @@ -158,12 +153,37 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading custom framework")) return } - state.ID = types.StringValue(data.GetData().Attributes.Handle + string('-') + data.GetData().Attributes.Version) + databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString()) + diags = response.State.Set(ctx, &databaseState) + response.Diagnostics.Append(diags...) +} + +func (r *customFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state customFrameworkModel + diags := request.Config.Get(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating custom framework")) + return + } + diags = response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) +} + +func readStateFromDatabase(data datadogV2.RetrieveCustomFrameworkResponse, handle string, version string) customFrameworkModel { + // Set the state + var state customFrameworkModel + state.ID = types.StringValue(handle + "-" + version) + state.Handle = types.StringValue(handle) + state.Version = types.StringValue(version) + state.Name = types.StringValue(data.GetData().Attributes.Name) state.Description = types.StringValue(data.GetData().Attributes.Description) state.IconURL = types.StringValue(data.GetData().Attributes.IconUrl) - state.Version = types.StringValue(data.GetData().Attributes.Version) - state.Handle = types.StringValue(data.GetData().Attributes.Handle) - state.Name = types.StringValue(data.GetData().Attributes.Name) // Convert requirements to set requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements)) @@ -186,7 +206,6 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea }, ) } - // set requirement requirements[i] = types.ObjectValueMust( map[string]attr.Type{ "name": types.StringType, @@ -204,7 +223,6 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea }, ) } - // set state requirements state.Requirements = types.SetValueMust( types.ObjectType{AttrTypes: map[string]attr.Type{ "name": types.StringType, @@ -215,25 +233,29 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea }}, requirements, ) - diags = response.State.Set(ctx, &state) - response.Diagnostics.Append(diags...) + return state } -func (r *customFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - var state customFrameworkModel - diags := request.Config.Get(ctx, &state) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { +// ImportState is used to import a resource from an existing framework so we can update it if it exists in the database and not in terraform +func (r *customFrameworkResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + // Split the ID into handle and version + // The last hyphen separates handle and version + lastHyphenIndex := strings.LastIndex(request.ID, "-") + if lastHyphenIndex == -1 { + response.Diagnostics.AddError("Invalid import ID", "Import ID must contain a hyphen to separate handle and version") return } + handle := request.ID[:lastHyphenIndex] + version := request.ID[lastHyphenIndex+1:] - _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) + data, _, err := r.Api.RetrieveCustomFramework(r.Auth, handle, version) if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating custom framework")) + response.Diagnostics.AddError("Error importing resource", err.Error()) return } - diags = response.State.Set(ctx, &state) - response.Diagnostics.Append(diags...) + + state := readStateFromDatabase(data, handle, version) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) } func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []datadogV2.CustomFrameworkRequirement { diff --git a/examples/resources/datadog_custom_framework/import.sh b/examples/resources/datadog_custom_framework/import.sh new file mode 100644 index 000000000..d175a1a0e --- /dev/null +++ b/examples/resources/datadog_custom_framework/import.sh @@ -0,0 +1 @@ +terraform import datadog_custom_framework.example2 "new-terraform-framework-test-2" \ No newline at end of file diff --git a/examples/resources/datadog_custom_framework/main.tf b/examples/resources/datadog_custom_framework/main.tf new file mode 100644 index 000000000..d69d9693d --- /dev/null +++ b/examples/resources/datadog_custom_framework/main.tf @@ -0,0 +1,31 @@ +terraform { + required_providers { + datadog = { + source = "DataDog/datadog" + } + } +} + +provider "datadog" { + api_key = var.datadog_api_key + app_key = var.datadog_app_key +} + + +resource "datadog_custom_framework" "example" { + version = "1.0.0" + handle = "terraform-created-framework-handle" + name = "terraform-created-framework" + icon_url = "https://example.com/icon.png" + description = "This is a test I created this resource through terraform" + requirements { + name = "requirement1" + controls { + name = "changedControlName" + rules_id = ["def-000-cea"] + } + } +} + + + diff --git a/examples/resources/datadog_custom_framework/variables.tf b/examples/resources/datadog_custom_framework/variables.tf new file mode 100644 index 000000000..d714e4cbd --- /dev/null +++ b/examples/resources/datadog_custom_framework/variables.tf @@ -0,0 +1,11 @@ +variable "datadog_api_key" { + description = "Datadog API key" + type = string + sensitive = true +} + +variable "datadog_app_key" { + description = "Datadog APP key" + type = string + sensitive = true +} \ No newline at end of file diff --git a/examples/resources/datadog_custom_framework_rules/resource.tf b/examples/resources/datadog_custom_framework_rules/resource.tf deleted file mode 100644 index 0e2d95190..000000000 --- a/examples/resources/datadog_custom_framework_rules/resource.tf +++ /dev/null @@ -1,16 +0,0 @@ -resource "datadog_custom_framework" "example" { - version = "1.0.0" - handle = "example_handle" - name = "example_name" - icon_url = "https://example.com/icon.png" - description = "description" - requirements { - name = "requirement1" - controls { - name = "control1" - rules_id = ["rule1", "rule2"] - } - } -} - - From c86d37f573bf0651cff1eed26c13c80747c216a5 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 25 Apr 2025 14:58:50 -0400 Subject: [PATCH 07/54] update mod file --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2b31ea5b9..781689415 100644 --- a/go.mod +++ b/go.mod @@ -101,6 +101,6 @@ require ( google.golang.org/protobuf v1.36.3 // indirect ) -replace github.com/DataDog/datadog-api-client-go/v2 v2.36.1 => ../datadog-api-spec/generated/datadog-api-client-go +replace github.com/DataDog/datadog-api-client-go/v2 v2.37.2-0.20250414171606-63df4a4d718f => ../datadog-api-spec/generated/datadog-api-client-go go 1.23.0 From db0ac703cd76d641717d79216a5e2cab4e349d40 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 25 Apr 2025 16:26:31 -0400 Subject: [PATCH 08/54] clean up code --- .../resource_datadog_custom_framework.go | 93 +++++++++++-------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index c83938b51..bd4d5a7a5 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -175,6 +175,50 @@ func (r *customFrameworkResource) Update(ctx context.Context, request resource.U response.Diagnostics.Append(diags...) } +func setControl(name string, rulesID []attr.Value) types.Object { + return types.ObjectValueMust( + map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }, + map[string]attr.Value{ + "name": types.StringValue(name), + "rules_id": types.SetValueMust(types.StringType, rulesID), + }, + ) +} + +func setRequirement(name string, controls []attr.Value) types.Object { + return types.ObjectValueMust( + map[string]attr.Type{ + "name": types.StringType, + "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }}}, + }, + map[string]attr.Value{ + "name": types.StringValue(name), + "controls": types.SetValueMust(types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }}, controls), + }, + ) +} + +func setRequirements(requirements []attr.Value) types.Set { + return types.SetValueMust( + types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "rules_id": types.SetType{ElemType: types.StringType}, + }}}, + }}, + requirements, + ) +} func readStateFromDatabase(data datadogV2.RetrieveCustomFrameworkResponse, handle string, version string) customFrameworkModel { // Set the state var state customFrameworkModel @@ -187,52 +231,19 @@ func readStateFromDatabase(data datadogV2.RetrieveCustomFrameworkResponse, handl // Convert requirements to set requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements)) - for i, req := range data.GetData().Attributes.Requirements { + for i, requirement := range data.GetData().Attributes.Requirements { // Convert controls to set - controls := make([]attr.Value, len(req.Controls)) - for j, ctrl := range req.Controls { - rulesID := make([]attr.Value, len(ctrl.RulesId)) - for k, ruleID := range ctrl.RulesId { + controls := make([]attr.Value, len(requirement.Controls)) + for j, control := range requirement.Controls { + rulesID := make([]attr.Value, len(control.RulesId)) + for k, ruleID := range control.RulesId { rulesID[k] = types.StringValue(ruleID) } - controls[j] = types.ObjectValueMust( - map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }, - map[string]attr.Value{ - "name": types.StringValue(ctrl.Name), - "rules_id": types.SetValueMust(types.StringType, rulesID), - }, - ) + controls[j] = setControl(control.Name, rulesID) } - requirements[i] = types.ObjectValueMust( - map[string]attr.Type{ - "name": types.StringType, - "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }}}, - }, - map[string]attr.Value{ - "name": types.StringValue(req.Name), - "controls": types.SetValueMust(types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }}, controls), - }, - ) + requirements[i] = setRequirement(requirement.Name, controls) } - state.Requirements = types.SetValueMust( - types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }}}, - }}, - requirements, - ) + state.Requirements = setRequirements(requirements) return state } From 07fa958048ae2cdacb6539632681b55812a06f4a Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 25 Apr 2025 16:28:37 -0400 Subject: [PATCH 09/54] update tests --- .../TestCustomFramework_InvalidCreate.freeze | 2 +- .../TestCustomFramework_InvalidCreate.yaml | 12 ++--- .../TestCustomFramework_create.freeze | 2 +- .../cassettes/TestCustomFramework_create.yaml | 50 +++++++++---------- ...ramework_createMultipleRequirements.freeze | 2 +- ...mFramework_createMultipleRequirements.yaml | 26 +++++----- ...tCustomFramework_sameConfigNoUpdate.freeze | 2 +- ...estCustomFramework_sameConfigNoUpdate.yaml | 50 +++++++++---------- 8 files changed, 73 insertions(+), 73 deletions(-) diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 92854bb04..1a2dd77ee 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-04-24T17:54:06.626222-04:00 \ No newline at end of file +2025-04-25T16:27:56.299156-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index e4613b5bc..9c4e44974 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-31996","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-30568"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-36194","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-70326"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 88.458458ms + duration: 173.665708ms - id: 1 request: proto: HTTP/1.1 @@ -49,7 +49,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-31996","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"version-30568"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-36194","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"version-70326"},"type":"custom_framework"}} form: {} headers: Accept: @@ -72,7 +72,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 216.985459ms + duration: 190.480667ms - id: 2 request: proto: HTTP/1.1 @@ -85,7 +85,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-31996","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"version-30568"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-36194","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"version-70326"},"type":"custom_framework"}} form: {} headers: Accept: @@ -108,4 +108,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 51.733334ms + duration: 40.119125ms diff --git a/datadog/tests/cassettes/TestCustomFramework_create.freeze b/datadog/tests/cassettes/TestCustomFramework_create.freeze index 325628c47..85d5a30c4 100644 --- a/datadog/tests/cassettes/TestCustomFramework_create.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_create.freeze @@ -1 +1 @@ -2025-04-24T17:25:32.385002-04:00 \ No newline at end of file +2025-04-25T16:26:15.932701-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_create.yaml b/datadog/tests/cassettes/TestCustomFramework_create.yaml index 1df75a154..3c1bd3c29 100644 --- a/datadog/tests/cassettes/TestCustomFramework_create.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_create.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-57607","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-55264"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-67205"},"type":"custom_framework"}} form: {} headers: Accept: @@ -30,13 +30,13 @@ interactions: trailer: {} content_length: 135 uncompressed: false - body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"handle":"handle-57607","version":"version-55264"}}}' + body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"handle":"handle-28911","version":"version-67205"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 537.934916ms + duration: 497.683708ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 429 + content_length: 324 uncompressed: false - body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529933617,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-55264"}}}' + body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-67205"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 180.98925ms + duration: 412.947792ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 method: GET response: proto: HTTP/1.1 @@ -94,15 +94,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 429 + content_length: 324 uncompressed: false - body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529933617,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-55264"}}}' + body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-67205"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 158.478917ms + duration: 189.385625ms - id: 3 request: proto: HTTP/1.1 @@ -115,14 +115,14 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-57607","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-55264"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-67205"},"type":"custom_framework"}} form: {} headers: Accept: - application/json Content-Type: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 method: PUT response: proto: HTTP/1.1 @@ -132,13 +132,13 @@ interactions: trailer: {} content_length: 135 uncompressed: false - body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"handle":"handle-57607","version":"version-55264"}}}' + body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"handle":"handle-28911","version":"version-67205"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 723.31775ms + duration: 608.268ms - id: 4 request: proto: HTTP/1.1 @@ -155,7 +155,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 method: GET response: proto: HTTP/1.1 @@ -163,15 +163,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 562 + content_length: 457 uncompressed: false - body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529935488,"name":"new-name","org_id":321813,"requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-55264"}}}' + body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-67205"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 278.224875ms + duration: 309.801041ms - id: 5 request: proto: HTTP/1.1 @@ -188,7 +188,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 method: DELETE response: proto: HTTP/1.1 @@ -196,15 +196,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 313 + content_length: 208 uncompressed: false - body: '{"data":{"id":"handle-57607-version-55264","type":"custom_framework","attributes":{"created_at":1745529933617,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-57607","icon_url":"test url","modified_at":1745529935488,"name":"new-name","org_id":321813,"version":"version-55264"}}}' + body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-name","version":"version-67205"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 414.365ms + duration: 321.939ms - id: 6 request: proto: HTTP/1.1 @@ -221,7 +221,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-57607/version-55264 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 method: GET response: proto: HTTP/1.1 @@ -237,4 +237,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 47.912916ms + duration: 55.489667ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze index d1e314238..bd30eeaa7 100644 --- a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze @@ -1 +1 @@ -2025-04-24T17:25:32.385502-04:00 \ No newline at end of file +2025-04-25T16:26:15.932705-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml index b3f9fb2bc..7de7a19ee 100644 --- a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-11209","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-72175"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-45366","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-82650"},"type":"custom_framework"}} form: {} headers: Accept: @@ -30,13 +30,13 @@ interactions: trailer: {} content_length: 135 uncompressed: false - body: '{"data":{"id":"handle-11209-version-72175","type":"custom_framework","attributes":{"handle":"handle-11209","version":"version-72175"}}}' + body: '{"data":{"id":"handle-45366-version-82650","type":"custom_framework","attributes":{"handle":"handle-45366","version":"version-82650"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 522.858166ms + duration: 495.194458ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-11209/version-72175 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-45366/version-82650 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 562 + content_length: 457 uncompressed: false - body: '{"data":{"id":"handle-11209-version-72175","type":"custom_framework","attributes":{"created_at":1745529933596,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-11209","icon_url":"test url","modified_at":1745529933596,"name":"new-name","org_id":321813,"requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-72175"}}}' + body: '{"data":{"id":"handle-45366-version-82650","type":"custom_framework","attributes":{"description":"test description","handle":"handle-45366","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-82650"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 276.719833ms + duration: 286.395417ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-11209/version-72175 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-45366/version-82650 method: DELETE response: proto: HTTP/1.1 @@ -94,15 +94,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 313 + content_length: 208 uncompressed: false - body: '{"data":{"id":"handle-11209-version-72175","type":"custom_framework","attributes":{"created_at":1745529933596,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-11209","icon_url":"test url","modified_at":1745529933596,"name":"new-name","org_id":321813,"version":"version-72175"}}}' + body: '{"data":{"id":"handle-45366-version-82650","type":"custom_framework","attributes":{"description":"test description","handle":"handle-45366","icon_url":"test url","name":"new-name","version":"version-82650"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 446.025ms + duration: 334.513792ms - id: 3 request: proto: HTTP/1.1 @@ -119,7 +119,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-11209/version-72175 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-45366/version-82650 method: GET response: proto: HTTP/1.1 @@ -135,4 +135,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 60.071334ms + duration: 46.785959ms diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze index ce7f12aa3..21a049c8d 100644 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze @@ -1 +1 @@ -2025-04-24T17:31:00.07411-04:00 \ No newline at end of file +2025-04-25T16:28:18.311772-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml index 3619a50a8..a967ad39c 100644 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-25727","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"version-35668"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"version-17805"},"type":"custom_framework"}} form: {} headers: Accept: @@ -30,13 +30,13 @@ interactions: trailer: {} content_length: 135 uncompressed: false - body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"handle":"handle-25727","version":"version-35668"}}}' + body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"handle":"handle-55506","version":"version-17805"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 388.449292ms + duration: 325.5ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 611 + content_length: 506 uncompressed: false - body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530261258,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-35668"}}}' + body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-17805"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 390.4165ms + duration: 436.938084ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 method: GET response: proto: HTTP/1.1 @@ -94,15 +94,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 611 + content_length: 506 uncompressed: false - body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530261258,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-35668"}}}' + body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-17805"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 215.097208ms + duration: 402.70725ms - id: 3 request: proto: HTTP/1.1 @@ -115,14 +115,14 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-25727","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9"]}],"name":"requirement3"}],"version":"version-35668"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9"]}],"name":"requirement3"}],"version":"version-17805"},"type":"custom_framework"}} form: {} headers: Accept: - application/json Content-Type: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 method: PUT response: proto: HTTP/1.1 @@ -132,13 +132,13 @@ interactions: trailer: {} content_length: 135 uncompressed: false - body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"handle":"handle-25727","version":"version-35668"}}}' + body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"handle":"handle-55506","version":"version-17805"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 798.598167ms + duration: 568.848042ms - id: 4 request: proto: HTTP/1.1 @@ -155,7 +155,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 method: GET response: proto: HTTP/1.1 @@ -163,15 +163,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 611 + content_length: 506 uncompressed: false - body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530263373,"name":"new-framework-terraform","org_id":321813,"requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9"]}]}],"version":"version-35668"}}}' + body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9"]}]}],"version":"version-17805"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 485.902167ms + duration: 397.935666ms - id: 5 request: proto: HTTP/1.1 @@ -188,7 +188,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 method: DELETE response: proto: HTTP/1.1 @@ -196,15 +196,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 328 + content_length: 223 uncompressed: false - body: '{"data":{"id":"handle-25727-version-35668","type":"custom_framework","attributes":{"created_at":1745530261258,"created_by":"frog@datadoghq.com","description":"test description","handle":"handle-25727","icon_url":"test url","modified_at":1745530263373,"name":"new-framework-terraform","org_id":321813,"version":"version-35668"}}}' + body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","version":"version-17805"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 425.181542ms + duration: 609.933ms - id: 6 request: proto: HTTP/1.1 @@ -221,7 +221,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-25727/version-35668 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 method: GET response: proto: HTTP/1.1 @@ -237,4 +237,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 55.792917ms + duration: 62.756125ms From bd39a3913f5d8861a498eb226de3cb10351dd2e2 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Sun, 27 Apr 2025 16:18:47 -0400 Subject: [PATCH 10/54] test update is not triggered if order is changed --- ...tCustomFramework_sameConfigNoUpdate.freeze | 2 +- ...estCustomFramework_sameConfigNoUpdate.yaml | 82 ++++++------------- .../resource_datadog_custom_framework_test.go | 13 ++- .../datadog_custom_framework/main.tf | 9 +- 4 files changed, 43 insertions(+), 63 deletions(-) diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze index 21a049c8d..d3201cd86 100644 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze @@ -1 +1 @@ -2025-04-25T16:28:18.311772-04:00 \ No newline at end of file +2025-04-27T16:14:07.509872-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml index a967ad39c..cb29a73c6 100644 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 473 + content_length: 522 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"version-17805"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"version-61251"},"type":"custom_framework"}} form: {} headers: Accept: @@ -30,13 +30,13 @@ interactions: trailer: {} content_length: 135 uncompressed: false - body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"handle":"handle-55506","version":"version-17805"}}}' + body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"handle":"handle-54746","version":"version-61251"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 325.5ms + duration: 541.320792ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 506 + content_length: 555 uncompressed: false - body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-17805"}}}' + body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-61251"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 436.938084ms + duration: 693.301709ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 method: GET response: proto: HTTP/1.1 @@ -94,52 +94,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 506 + content_length: 555 uncompressed: false - body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-17805"}}}' + body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-61251"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 402.70725ms + duration: 668.485375ms - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 473 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9"]}],"name":"requirement3"}],"version":"version-17805"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 - method: PUT - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 135 - uncompressed: false - body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"handle":"handle-55506","version":"version-17805"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 568.848042ms - - id: 4 request: proto: HTTP/1.1 proto_major: 1 @@ -155,7 +119,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 method: GET response: proto: HTTP/1.1 @@ -163,16 +127,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 506 + content_length: 555 uncompressed: false - body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9"]}]}],"version":"version-17805"}}}' + body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-61251"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 397.935666ms - - id: 5 + duration: 660.625791ms + - id: 4 request: proto: HTTP/1.1 proto_major: 1 @@ -188,7 +152,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 method: DELETE response: proto: HTTP/1.1 @@ -198,14 +162,14 @@ interactions: trailer: {} content_length: 223 uncompressed: false - body: '{"data":{"id":"handle-55506-version-17805","type":"custom_framework","attributes":{"description":"test description","handle":"handle-55506","icon_url":"test url","name":"new-framework-terraform","version":"version-17805"}}}' + body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","version":"version-61251"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 609.933ms - - id: 6 + duration: 739.861834ms + - id: 5 request: proto: HTTP/1.1 proto_major: 1 @@ -221,7 +185,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-55506/version-17805 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 method: GET response: proto: HTTP/1.1 @@ -237,4 +201,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 62.756125ms + duration: 64.415625ms diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 7e93390bc..5981670db 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -157,6 +157,10 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { } requirements { name = "requirement2" + controls { + name = "control2.2" + rules_id = ["def-000-be9"] + } controls { name = "control2" rules_id = ["def-000-cea"] @@ -173,6 +177,7 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { `, version, handle) // Second config with different order of requirements + // test switching order of requirements, controls, and rules_id config2 := fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { version = "%s" @@ -184,7 +189,7 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { name = "requirement3" controls { name = "control3" - rules_id = ["def-000-be9"] + rules_id = ["def-000-cea", "def-000-be9"] } } requirements { @@ -198,7 +203,11 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { name = "requirement2" controls { name = "control2" - rules_id = ["def-000-be9", "def-000-cea"] + rules_id = ["def-000-cea"] + } + controls { + name = "control2.2" + rules_id = ["def-000-be9"] } } } diff --git a/examples/resources/datadog_custom_framework/main.tf b/examples/resources/datadog_custom_framework/main.tf index d69d9693d..ea42c0bca 100644 --- a/examples/resources/datadog_custom_framework/main.tf +++ b/examples/resources/datadog_custom_framework/main.tf @@ -18,11 +18,18 @@ resource "datadog_custom_framework" "example" { name = "terraform-created-framework" icon_url = "https://example.com/icon.png" description = "This is a test I created this resource through terraform" + requirements { + name = "requirement2" + controls { + name = "control2" + rules_id = ["def-000-cea"] + } + } requirements { name = "requirement1" controls { name = "changedControlName" - rules_id = ["def-000-cea"] + rules_id = ["def-000-cea", "def-000-be9"] } } } From eef04ab5f8485b2fdec621812a6da96ff18be59e Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 28 Apr 2025 12:40:04 -0400 Subject: [PATCH 11/54] change retrieve custom framework to get custom framework --- datadog/fwprovider/resource_datadog_custom_framework.go | 6 +++--- datadog/tests/resource_datadog_custom_framework_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index bd4d5a7a5..3869c4220 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -148,7 +148,7 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea return } - data, _, err := r.Api.RetrieveCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) + data, _, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading custom framework")) return @@ -219,7 +219,7 @@ func setRequirements(requirements []attr.Value) types.Set { requirements, ) } -func readStateFromDatabase(data datadogV2.RetrieveCustomFrameworkResponse, handle string, version string) customFrameworkModel { +func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string) customFrameworkModel { // Set the state var state customFrameworkModel state.ID = types.StringValue(handle + "-" + version) @@ -259,7 +259,7 @@ func (r *customFrameworkResource) ImportState(ctx context.Context, request resou handle := request.ID[:lastHyphenIndex] version := request.ID[lastHyphenIndex+1:] - data, _, err := r.Api.RetrieveCustomFramework(r.Auth, handle, version) + data, _, err := r.Api.GetCustomFramework(r.Auth, handle, version) if err != nil { response.Diagnostics.AddError("Error importing resource", err.Error()) return diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 5981670db..21c99d1af 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -413,7 +413,7 @@ func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwpro resource := s.RootModule().Resources[resourceName] handle := resource.Primary.Attributes["handle"] version := resource.Primary.Attributes["version"] - _, httpRes, err := apiInstances.GetSecurityMonitoringApiV2().RetrieveCustomFramework(ctx, handle, version) + _, httpRes, err := apiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(ctx, handle, version) if err != nil { if httpRes.StatusCode == 400 { return nil From 4a4b65ef4d90cacfc486788337050263c32aed30 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Tue, 29 Apr 2025 13:34:30 -0400 Subject: [PATCH 12/54] update api spec in go mod --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 781689415..d6a9ed14c 100644 --- a/go.mod +++ b/go.mod @@ -101,6 +101,4 @@ require ( google.golang.org/protobuf v1.36.3 // indirect ) -replace github.com/DataDog/datadog-api-client-go/v2 v2.37.2-0.20250414171606-63df4a4d718f => ../datadog-api-spec/generated/datadog-api-client-go - go 1.23.0 From b8a33367bd56f6a7bc2cdfa0fa1837620e4d9306 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Tue, 29 Apr 2025 13:34:45 -0400 Subject: [PATCH 13/54] add docs for terraform provider --- .../resource_datadog_custom_framework.go | 10 +++- docs/resources/custom_framework.md | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 docs/resources/custom_framework.md diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index 3869c4220..7fb39a49e 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -5,9 +5,11 @@ import ( "strings" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" @@ -40,7 +42,7 @@ func (r *customFrameworkResource) Metadata(_ context.Context, _ resource.Metadat func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = schema.Schema{ - Description: "Manages custom framework rules in Datadog.", + Description: "Manages custom framework in Datadog.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "The ID of the custom framework resource.", @@ -70,6 +72,9 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq Blocks: map[string]schema.Block{ "requirements": schema.SetNestedBlock{ Description: "The requirements of the framework.", + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -80,6 +85,9 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq Blocks: map[string]schema.Block{ "controls": schema.SetNestedBlock{ Description: "The controls of the requirement.", + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ diff --git a/docs/resources/custom_framework.md b/docs/resources/custom_framework.md new file mode 100644 index 000000000..fb3e8a7f1 --- /dev/null +++ b/docs/resources/custom_framework.md @@ -0,0 +1,59 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_custom_framework Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Manages custom framework in Datadog. +--- + +# datadog_custom_framework (Resource) + +Manages custom framework in Datadog. + + + + +## Schema + +### Required + +- `handle` (String) The framework handle. +- `name` (String) The framework name. +- `version` (String) The framework version. + +### Optional + +- `description` (String) The description of the framework. +- `icon_url` (String) The URL of the icon representing the framework. +- `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) + +### Read-Only + +- `id` (String) The ID of the custom framework resource. + + +### Nested Schema for `requirements` + +Required: + +- `name` (String) The name of the requirement. + +Optional: + +- `controls` (Block Set) The controls of the requirement. (see [below for nested schema](#nestedblock--requirements--controls)) + + +### Nested Schema for `requirements.controls` + +Required: + +- `name` (String) The name of the control. +- `rules_id` (Set of String) The list of rules IDs for the control. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import datadog_custom_framework.example2 "new-terraform-framework-test-2" +``` From a9eca77873025c4b1c6cc5c49eb08fd61e1ceec0 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Tue, 29 Apr 2025 13:35:59 -0400 Subject: [PATCH 14/54] remove unstable endpoint --- datadog/fwprovider/framework_provider.go | 1 - 1 file changed, 1 deletion(-) diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index 1ec77106c..158b9a3b7 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -437,7 +437,6 @@ func defaultConfigureFunc(p *FrameworkProvider, request *provider.ConfigureReque ddClientConfig.SetUnstableOperationEnabled("v2.DeleteAWSAccount", true) ddClientConfig.SetUnstableOperationEnabled("v2.GetAWSAccount", true) ddClientConfig.SetUnstableOperationEnabled("v2.CreateNewAWSExternalID", true) - ddClientConfig.SetUnstableOperationEnabled("v2.CreateCustomFramework", true) // Enable Observability Pipelines ddClientConfig.SetUnstableOperationEnabled("v2.CreatePipeline", true) From c9efcf84a2d540a91760aae037c364f14c483fa6 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Tue, 29 Apr 2025 15:54:24 -0400 Subject: [PATCH 15/54] add more tests --- .../TestCustomFramework_InvalidCreate.freeze | 2 +- .../TestCustomFramework_InvalidCreate.yaml | 120 ++++++++- ...createAndUpdateMultipleRequirements.freeze | 1 + ...k_createAndUpdateMultipleRequirements.yaml | 240 ++++++++++++++++++ .../resource_datadog_custom_framework_test.go | 216 +++++++++++++++- 5 files changed, 569 insertions(+), 10 deletions(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 1a2dd77ee..3bd960dc2 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-04-25T16:27:56.299156-04:00 \ No newline at end of file +2025-04-29T15:52:07.605355-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index 9c4e44974..084f3411d 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-36194","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-70326"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 173.665708ms + duration: 99.06275ms - id: 1 request: proto: HTTP/1.1 @@ -49,7 +49,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-36194","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"version-70326"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} form: {} headers: Accept: @@ -72,7 +72,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 190.480667ms + duration: 161.604542ms - id: 2 request: proto: HTTP/1.1 @@ -85,7 +85,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-36194","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"version-70326"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"version-43400"},"type":"custom_framework"}} form: {} headers: Accept: @@ -108,4 +108,112 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 40.119125ms + duration: 70.558ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 245 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 189 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.requirements.controls'' is invalid: field ''requirements.controls'' can''t be empty)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 49.768708ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 270 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 162 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.handle'' is invalid: field ''handle'' must not be empty)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 52.803583ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 269 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":""},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 164 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.version'' is invalid: field ''version'' must not be empty)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 50.11625ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze new file mode 100644 index 000000000..124b5ab86 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze @@ -0,0 +1 @@ +2025-04-29T15:52:27.599663-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml new file mode 100644 index 000000000..977f3c4b2 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml @@ -0,0 +1,240 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 424 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-72598"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"handle":"handle-28834","version":"version-72598"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 446.040166ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 457 + uncompressed: false + body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-72598"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 290.194833ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 457 + uncompressed: false + body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-72598"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 352.281792ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 522 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"},{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"}],"version":"version-72598"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 135 + uncompressed: false + body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"handle":"handle-28834","version":"version-72598"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 715.978459ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 555 + uncompressed: false + body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"name":"requirement","controls":[{"name":"control","rules_id":["def-000-be9"]}]},{"name":"requirement-2","controls":[{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]},{"name":"control-2","rules_id":["def-000-be9"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-cea"]}]}],"version":"version-72598"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 544.192875ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 208 + uncompressed: false + body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","version":"version-72598"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 386.574958ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 55.230583ms diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 21c99d1af..b780d3145 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -95,7 +95,7 @@ func TestCustomFramework_delete(t *testing.T) { }) } -func TestCustomFramework_createMultipleRequirements(t *testing.T) { +func TestCustomFramework_createAndUpdateMultipleRequirements(t *testing.T) { t.Parallel() handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) version := fmt.Sprintf("version-%d", rand.Intn(100000)) @@ -128,6 +128,36 @@ func TestCustomFramework_createMultipleRequirements(t *testing.T) { resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), ), }, + { + Config: testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "security-requirement", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement-2", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control-2", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control-3", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + ), + }, }, }) } @@ -297,7 +327,7 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - Config: testAccCheckDatadogCreateInvalidFramework(version, handle), + Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle), ExpectError: regexp.MustCompile("400 Bad Request"), }, { @@ -308,6 +338,18 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle), ExpectError: regexp.MustCompile("400 Bad Request"), }, + { + Config: testAccCheckDatadogCreateFrameworkWithNoControls(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + { + Config: testAccCheckDatadogCreateEmptyHandle(version), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + { + Config: testAccCheckDatadogCreateEmptyVersion(handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, }, }) } @@ -338,6 +380,43 @@ func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, `, version, handle) } +func testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-name" + description = "test description" + icon_url = "test url" + requirements { + name = "security-requirement" + controls { + name = "security-control" + rules_id = ["def-000-cea"] + } + } + requirements { + name = "requirement" + controls { + name = "control" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement-2" + controls { + name = "control-2" + rules_id = ["def-000-be9"] + } + controls { + name = "control-3" + rules_id = ["def-000-cea", "def-000-be9"] + } + } + } + `, version, handle) +} + func testAccCheckDatadogCreateFramework(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { @@ -376,6 +455,21 @@ func testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version string, handle str `, version, handle) } +func testAccCheckDatadogCreateFrameworkWithNoControls(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + } + } + `, version, handle) +} + func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { @@ -388,7 +482,7 @@ func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle str `, version, handle) } -func testAccCheckDatadogCreateInvalidFramework(version string, handle string) string { +func testAccCheckDatadogCreateInvalidFrameworkName(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { version = "%s" @@ -407,6 +501,122 @@ func testAccCheckDatadogCreateInvalidFramework(version string, handle string) st `, version, handle) } +func testAccCheckDatadogCreateEmptyHandle(version string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "" + name = "framework-name" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version) +} + +func testAccCheckDatadogCreateEmptyVersion(handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "" + handle = "%s" + name = "framework-name" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, handle) +} + +// TODO: Add validation for duplicate requirements and controls because the state model +// converts the requirements into sets and the duplicate requirements are deleted + +// func testAccCheckDatadogDuplicateRequirements(version string, handle string) string { +// return fmt.Sprintf(` +// resource "datadog_custom_framework" "sample_rules" { +// version = "%s" +// handle = "%s" +// name = "framework-name" +// description = "test description" +// icon_url = "test url" +// requirements { +// name = "requirement1" +// controls { +// name = "control1" +// rules_id = ["def-000-be9"] +// } +// } +// requirements { +// name = "requirement1" +// controls { +// name = "control1" +// rules_id = ["def-000-be9"] +// } +// } +// } +// `, version, handle) +// } + +// func testAccCheckDatadogDuplicateControls(version string, handle string) string { +// return fmt.Sprintf(` +// resource "datadog_custom_framework" "sample_rules" { +// version = "%s" +// handle = "%s" +// name = "framework-name" +// description = "test description" +// icon_url = "test url" +// requirements { +// name = "requirement1" +// controls { +// name = "control1" +// rules_id = ["def-000-be9"] +// } +// } +// requirements { +// name = "requirement1" +// controls { +// name = "control1" +// rules_id = ["def-000-be9"] +// } +// controls { +// name = "control1" +// rules_id = ["def-000-be9"] +// } +// } +// } +// `, version, handle) +// } + +// func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { +// return fmt.Sprintf(` +// resource "datadog_custom_framework" "sample_rules" { +// version = "%s" +// handle = "%s" +// name = "framework-name" +// description = "test description" +// icon_url = "test url" +// requirements { +// name = "requirement1" +// controls { +// name = "control1" +// rules_id = ["def-000-be9", "def-000-be9"] +// } +// } +// } +// `, version, handle) +// } + func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwprovider.FrameworkProvider, resourceName string, version string, handle string) func(*terraform.State) error { return func(s *terraform.State) error { apiInstances := accProvider.DatadogApiInstances From ad9ad89b35ccbd509ce686511fec0a71ea52873a Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 1 May 2025 16:02:05 -0400 Subject: [PATCH 16/54] add validators --- .../resource_datadog_custom_framework.go | 26 ++ .../validators/duplicate_control_validator.go | 56 +++ .../duplicate_requirement_validator.go | 56 +++ .../resource_datadog_custom_framework_test.go | 319 ++++++++++++------ .../datadog_custom_framework/import.sh | 1 - .../datadog_custom_framework/main.tf | 7 +- 6 files changed, 359 insertions(+), 106 deletions(-) create mode 100644 datadog/internal/validators/duplicate_control_validator.go create mode 100644 datadog/internal/validators/duplicate_requirement_validator.go mode change 100644 => 100755 examples/resources/datadog_custom_framework/import.sh diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index 7fb39a49e..785c5d3a5 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/validators" ) var _ resource.Resource = &customFrameworkResource{} @@ -74,6 +75,7 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq Description: "The requirements of the framework.", Validators: []validator.Set{ setvalidator.SizeAtLeast(1), + validators.RequirementNameValidator(), }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ @@ -87,6 +89,7 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq Description: "The controls of the requirement.", Validators: []validator.Set{ setvalidator.SizeAtLeast(1), + validators.ControlNameValidator(), }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ @@ -157,6 +160,29 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea } data, _, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) + // If the framework does not exist, remove it from terraform state + // This is to avoid the provider to return an error when the framework is deleted in the UI prior + + // DELETE example: + // 1. create the framework in terraform + // 2. delete the framework in the UI + // 3. run terraform plan + // 4. terraform will see that the framework does not exist so it will remove it from the state + // 5. no changes are calculated by terraform so nothing is updated + + // UPDATE example: + // 1. create the framework in terraform + // 2. delete the framework in the UI + // 3. update the framework in terraform + // 4. run terraform plan + // 5. terraform will see that the framework does not exist so it will remove it from the state + // 6. terraform will perform a create action for the framework + if err != nil && err.Error() == "400 Bad Request" { + // Clear the state completely + response.State.RemoveResource(ctx) + return + } + // this is for any other error if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading custom framework")) return diff --git a/datadog/internal/validators/duplicate_control_validator.go b/datadog/internal/validators/duplicate_control_validator.go new file mode 100644 index 000000000..07fbfc0a8 --- /dev/null +++ b/datadog/internal/validators/duplicate_control_validator.go @@ -0,0 +1,56 @@ +package validators + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type controlNameValidator struct{} + +func (v controlNameValidator) Description(context.Context) string { + return "checks for duplicate control names" +} + +func (v controlNameValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v controlNameValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + // Get all control names from the configuration + var controlNames []string + for _, control := range req.ConfigValue.Elements() { + controlObj := control.(types.Object) + name := controlObj.Attributes()["name"].(types.String).ValueString() + log.Printf("Found control name in config: %s", name) + controlNames = append(controlNames, name) + } + + log.Printf("Found %d control names in config", len(controlNames)) + + // Check for duplicates in the list + seen := make(map[string]bool) + for _, name := range controlNames { + log.Printf("Checking control name: %s", name) + if seen[name] { + log.Printf("Found duplicate control name: %s", name) + resp.Diagnostics.AddError( + "400 Bad Request", + fmt.Sprintf("Control name '%s' is used more than once. Each control must have a unique name.", name), + ) + return + } + seen[name] = true + } +} + +func ControlNameValidator() validator.Set { + return &controlNameValidator{} +} diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_validator.go new file mode 100644 index 000000000..e0e0dab17 --- /dev/null +++ b/datadog/internal/validators/duplicate_requirement_validator.go @@ -0,0 +1,56 @@ +package validators + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type requirementNameValidator struct{} + +func (v requirementNameValidator) Description(context.Context) string { + return "checks for duplicate requirement names" +} + +func (v requirementNameValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + // Get all requirement names from the configuration + var requirementNames []string + for _, requirement := range req.ConfigValue.Elements() { + reqObj := requirement.(types.Object) + name := reqObj.Attributes()["name"].(types.String).ValueString() + log.Printf("Found requirement name in config: %s", name) + requirementNames = append(requirementNames, name) + } + + log.Printf("Found %d requirement names in config", len(requirementNames)) + + // Check for duplicates in the list + seen := make(map[string]bool) + for _, name := range requirementNames { + log.Printf("Checking requirement name: %s", name) + if seen[name] { + log.Printf("Found duplicate requirement name: %s", name) + resp.Diagnostics.AddError( + "400 Bad Request", + fmt.Sprintf("Requirement name '%s' is used more than once. Each requirement must have a unique name.", name), + ) + return + } + seen[name] = true + } +} + +func RequirementNameValidator() validator.Set { + return &requirementNameValidator{} +} diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index b780d3145..37d97c538 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -67,34 +67,6 @@ func TestCustomFramework_create(t *testing.T) { }) } -func TestCustomFramework_delete(t *testing.T) { - t.Parallel() - handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := fmt.Sprintf("version-%d", rand.Intn(100000)) - - ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), - Steps: []resource.TestStep{ - { - Config: testAccCheckDatadogCreateFramework(version, handle), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(path, "handle", handle), - resource.TestCheckResourceAttr(path, "version", version), - ), - }, - { - ResourceName: path, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - func TestCustomFramework_createAndUpdateMultipleRequirements(t *testing.T) { t.Parallel() handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) @@ -314,6 +286,40 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { }) } +// Test that duplicate rule IDs are removed from the state +// Since the state model converts the rules_id into sets, the duplicate rule IDs are removed +// There is no way to validate the duplicate rule IDs in the config before they are removed from the state in Terraform +// This test validates that the duplicate rule IDs are removed from the state and only one rule ID is present +func TestCustomFramework_DuplicateRuleIds(t *testing.T) { + t.Parallel() + handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) + version := fmt.Sprintf("version-%d", rand.Intn(100000)) + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogDuplicateRulesId(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control1", + }), + // duplicate rule ID should be removed + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + // verify there is exactly one rule ID + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), + ), + }, + }, + }) +} + func TestCustomFramework_InvalidCreate(t *testing.T) { t.Parallel() handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) @@ -350,6 +356,115 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { Config: testAccCheckDatadogCreateEmptyVersion(handle), ExpectError: regexp.MustCompile("400 Bad Request"), }, + { + Config: testAccCheckDatadogDuplicateRequirements(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + { + Config: testAccCheckDatadogDuplicateControls(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), + }, + }, + }) +} + +func TestCustomFramework_RecreateAfterAPIDelete(t *testing.T) { + t.Parallel() + handle := "terraform-handle" + version := "1.0.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + // First create the framework + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + }, + { + // Simulate framework being deleted in UI + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + // Delete the framework in the UI + func(s *terraform.State) error { + _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version) + return err + }, + ), + // Expect a non-empty plan since the framework was deleted + ExpectNonEmptyPlan: true, + }, + { + // Update the framework - + // The read would be able to tell that the framework was deleted in UI so then it delete the local terraform state of the framework + // this should trigger a create since it was deleted in UI + Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + // Verify the framework was recreated with the new requirements + resource.TestCheckResourceAttr(path, "requirements.#", "2"), + ), + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) { + t.Parallel() + handle := "terraform-handle" + version := "1.0.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + // First create the framework in terraform + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + }, + { + // Simulate framework being deleted in UI + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + // Delete the framework in the UI + func(s *terraform.State) error { + _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version) + return err + }, + ), + // Expect a non-empty plan since the framework was deleted + ExpectNonEmptyPlan: true, + }, + { + // Try to remove the resource from terraform + // Since the framework was deleted in UI, terraform should just remove it from state + // The read in terraform will return a 400 error because the framework handle and version no longer exist + // This will delete the framework from terraform + Config: "# Empty config to simulate removing the resource", + Check: resource.ComposeTestCheckFunc( + // No checks needed since we're removing the resource + ), + // Expect no changes needed since the resource was already deleted + ExpectNonEmptyPlan: false, + }, }, }) } @@ -542,80 +657,80 @@ func testAccCheckDatadogCreateEmptyVersion(handle string) string { // TODO: Add validation for duplicate requirements and controls because the state model // converts the requirements into sets and the duplicate requirements are deleted -// func testAccCheckDatadogDuplicateRequirements(version string, handle string) string { -// return fmt.Sprintf(` -// resource "datadog_custom_framework" "sample_rules" { -// version = "%s" -// handle = "%s" -// name = "framework-name" -// description = "test description" -// icon_url = "test url" -// requirements { -// name = "requirement1" -// controls { -// name = "control1" -// rules_id = ["def-000-be9"] -// } -// } -// requirements { -// name = "requirement1" -// controls { -// name = "control1" -// rules_id = ["def-000-be9"] -// } -// } -// } -// `, version, handle) -// } - -// func testAccCheckDatadogDuplicateControls(version string, handle string) string { -// return fmt.Sprintf(` -// resource "datadog_custom_framework" "sample_rules" { -// version = "%s" -// handle = "%s" -// name = "framework-name" -// description = "test description" -// icon_url = "test url" -// requirements { -// name = "requirement1" -// controls { -// name = "control1" -// rules_id = ["def-000-be9"] -// } -// } -// requirements { -// name = "requirement1" -// controls { -// name = "control1" -// rules_id = ["def-000-be9"] -// } -// controls { -// name = "control1" -// rules_id = ["def-000-be9"] -// } -// } -// } -// `, version, handle) -// } - -// func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { -// return fmt.Sprintf(` -// resource "datadog_custom_framework" "sample_rules" { -// version = "%s" -// handle = "%s" -// name = "framework-name" -// description = "test description" -// icon_url = "test url" -// requirements { -// name = "requirement1" -// controls { -// name = "control1" -// rules_id = ["def-000-be9", "def-000-be9"] -// } -// } -// } -// `, version, handle) -// } +func testAccCheckDatadogDuplicateRequirements(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "framework-name" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement1" + controls { + name = "control2" + rules_id = ["def-000-cea"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogDuplicateControls(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "framework-name" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + controls { + name = "control1" + rules_id = ["def-000-cea"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "framework-name" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9", "def-000-be9"] + } + } + } + `, version, handle) +} func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwprovider.FrameworkProvider, resourceName string, version string, handle string) func(*terraform.State) error { return func(s *terraform.State) error { diff --git a/examples/resources/datadog_custom_framework/import.sh b/examples/resources/datadog_custom_framework/import.sh old mode 100644 new mode 100755 index d175a1a0e..e69de29bb --- a/examples/resources/datadog_custom_framework/import.sh +++ b/examples/resources/datadog_custom_framework/import.sh @@ -1 +0,0 @@ -terraform import datadog_custom_framework.example2 "new-terraform-framework-test-2" \ No newline at end of file diff --git a/examples/resources/datadog_custom_framework/main.tf b/examples/resources/datadog_custom_framework/main.tf index ea42c0bca..a45248489 100644 --- a/examples/resources/datadog_custom_framework/main.tf +++ b/examples/resources/datadog_custom_framework/main.tf @@ -1,7 +1,8 @@ terraform { required_providers { datadog = { - source = "DataDog/datadog" + source = "DataDog/datadog" + version = ">= 3.45.0" } } } @@ -12,7 +13,7 @@ provider "datadog" { } -resource "datadog_custom_framework" "example" { +resource "datadog_custom_framework" "example3" { version = "1.0.0" handle = "terraform-created-framework-handle" name = "terraform-created-framework" @@ -21,7 +22,7 @@ resource "datadog_custom_framework" "example" { requirements { name = "requirement2" controls { - name = "control2" + name = "control3" rules_id = ["def-000-cea"] } } From efd64be0abe0e50f92438cea278d8b54326e2458 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 1 May 2025 16:10:03 -0400 Subject: [PATCH 17/54] change tests to use same handle and version --- ...ustomFramework_DeleteAfterAPIDelete.freeze | 1 + ...tCustomFramework_DeleteAfterAPIDelete.yaml | 204 +++++++++++ ...stCustomFramework_DeleteNonExistent.freeze | 1 + ...TestCustomFramework_DeleteNonExistent.yaml | 138 +++++++ ...estCustomFramework_DuplicateRuleIds.freeze | 1 + .../TestCustomFramework_DuplicateRuleIds.yaml | 138 +++++++ .../TestCustomFramework_InvalidCreate.freeze | 2 +- .../TestCustomFramework_InvalidCreate.yaml | 36 +- ...tomFramework_RecreateAfterAPIDelete.freeze | 1 + ...ustomFramework_RecreateAfterAPIDelete.yaml | 339 ++++++++++++++++++ ...tomFramework_TestImportingFramework.freeze | 1 + ...ustomFramework_TestImportingFramework.yaml | 69 ++++ .../TestCustomFramework_create.freeze | 2 +- .../cassettes/TestCustomFramework_create.yaml | 58 +-- ...createAndUpdateMultipleRequirements.freeze | 2 +- ...k_createAndUpdateMultipleRequirements.yaml | 58 +-- ...tCustomFramework_sameConfigNoUpdate.freeze | 2 +- ...estCustomFramework_sameConfigNoUpdate.yaml | 46 +-- .../resource_datadog_custom_framework_test.go | 35 +- 19 files changed, 1008 insertions(+), 126 deletions(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze new file mode 100644 index 000000000..aaf2b2381 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze @@ -0,0 +1 @@ +2025-05-01T16:09:17.103315-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml new file mode 100644 index 000000000..529e287f9 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml @@ -0,0 +1,204 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 285 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 373.761959ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 312 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 150.353417ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 312 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 286.217625ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 211 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 333.150375ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 77.855792ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 38.812542ms diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze new file mode 100644 index 000000000..d8713a1fa --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.freeze @@ -0,0 +1 @@ +2025-04-30T17:54:47.983034-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml new file mode 100644 index 000000000..8d079d297 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteNonExistent.yaml @@ -0,0 +1,138 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 289 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"fake-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"fake-version"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 131 + uncompressed: false + body: '{"data":{"id":"fake-handle-fake-version","type":"custom_framework","attributes":{"handle":"fake-handle","version":"fake-version"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 376.17375ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/fake-handle/fake-version + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 320 + uncompressed: false + body: '{"data":{"id":"fake-handle-fake-version","type":"custom_framework","attributes":{"description":"test description","handle":"fake-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"fake-version"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 217.58925ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/fake-handle/fake-version + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 219 + uncompressed: false + body: '{"data":{"id":"fake-handle-fake-version","type":"custom_framework","attributes":{"description":"test description","handle":"fake-handle","icon_url":"test url","name":"new-framework-terraform","version":"fake-version"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 417.445333ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/fake-handle/fake-version + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 41.793375ms diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze new file mode 100644 index 000000000..92f50e9c9 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze @@ -0,0 +1 @@ +2025-05-01T16:07:44.08489-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml new file mode 100644 index 000000000..607264566 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml @@ -0,0 +1,138 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 276 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 399.097083ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 303 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 177.151375ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 202 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 557.034125ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 52.814875ms diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 3bd960dc2..6b9fc0b20 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-04-29T15:52:07.605355-04:00 \ No newline at end of file +2025-05-01T16:08:14.218018-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index 084f3411d..71501dfff 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 268 + content_length: 262 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,20 +36,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 99.06275ms + duration: 110.766958ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 295 + content_length: 289 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -72,20 +72,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 161.604542ms + duration: 179.166583ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 208 + content_length: 202 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"version-43400"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -108,20 +108,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 70.558ms + duration: 47.237625ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 245 + content_length: 239 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -144,20 +144,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 49.768708ms + duration: 46.84975ms - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 270 + content_length: 260 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-43400"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -180,20 +180,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 52.803583ms + duration: 45.88825ms - id: 5 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 269 + content_length: 273 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-54247","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":""},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":""},"type":"custom_framework"}} form: {} headers: Accept: @@ -216,4 +216,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 50.11625ms + duration: 61.689667ms diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze new file mode 100644 index 000000000..692f505d3 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze @@ -0,0 +1 @@ +2025-05-01T16:08:45.678897-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml new file mode 100644 index 000000000..5444983f6 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml @@ -0,0 +1,339 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 285 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 367.168ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 312 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 176.838292ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 312 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 183.163041ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 211 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 453.045917ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 51.678417ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 65.162875ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 418 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 372.577542ms + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 445 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 313.339166ms + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 196 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 409.619209ms + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 39.744125ms diff --git a/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze new file mode 100644 index 000000000..505bd6516 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.freeze @@ -0,0 +1 @@ +2025-05-01T15:59:42.287109-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml new file mode 100644 index 000000000..53a7d252e --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_TestImportingFramework.yaml @@ -0,0 +1,69 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform-framework-test/2 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 313 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-test-2","type":"custom_framework","attributes":{"description":"","handle":"new-terraform-framework-test","icon_url":"","name":"new-terraform-framework-test","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-u48"]}]}],"version":"2"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 245.226916ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform-framework-test/2 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 313 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-test-2","type":"custom_framework","attributes":{"description":"","handle":"new-terraform-framework-test","icon_url":"","name":"new-terraform-framework-test","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-u48"]}]}],"version":"2"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 159.962333ms diff --git a/datadog/tests/cassettes/TestCustomFramework_create.freeze b/datadog/tests/cassettes/TestCustomFramework_create.freeze index 85d5a30c4..0c9d54631 100644 --- a/datadog/tests/cassettes/TestCustomFramework_create.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_create.freeze @@ -1 +1 @@ -2025-04-25T16:26:15.932701-04:00 \ No newline at end of file +2025-05-01T16:05:52.032549-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_create.yaml b/datadog/tests/cassettes/TestCustomFramework_create.yaml index 3c1bd3c29..1f3172a6e 100644 --- a/datadog/tests/cassettes/TestCustomFramework_create.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_create.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 291 + content_length: 285 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-67205"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -28,15 +28,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 135 + content_length: 123 uncompressed: false - body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"handle":"handle-28911","version":"version-67205"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 497.683708ms + duration: 392.627417ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 324 + content_length: 312 uncompressed: false - body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-67205"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 412.947792ms + duration: 161.583958ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -94,35 +94,35 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 324 + content_length: 312 uncompressed: false - body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"version-67205"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 189.385625ms + duration: 165.086166ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 424 + content_length: 418 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-67205"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: - application/json Content-Type: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: PUT response: proto: HTTP/1.1 @@ -130,15 +130,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 135 + content_length: 123 uncompressed: false - body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"handle":"handle-28911","version":"version-67205"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 608.268ms + duration: 555.135417ms - id: 4 request: proto: HTTP/1.1 @@ -155,7 +155,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -163,15 +163,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 457 + content_length: 445 uncompressed: false - body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-67205"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 309.801041ms + duration: 250.856292ms - id: 5 request: proto: HTTP/1.1 @@ -188,7 +188,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: DELETE response: proto: HTTP/1.1 @@ -196,15 +196,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 208 + content_length: 196 uncompressed: false - body: '{"data":{"id":"handle-28911-version-67205","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28911","icon_url":"test url","name":"new-name","version":"version-67205"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 321.939ms + duration: 391.974125ms - id: 6 request: proto: HTTP/1.1 @@ -221,7 +221,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28911/version-67205 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -237,4 +237,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 55.489667ms + duration: 50.065625ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze index 124b5ab86..ed08446cc 100644 --- a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze @@ -1 +1 @@ -2025-04-29T15:52:27.599663-04:00 \ No newline at end of file +2025-05-01T16:06:28.451812-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml index 977f3c4b2..bd99a00b9 100644 --- a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 424 + content_length: 418 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-72598"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -28,15 +28,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 135 + content_length: 123 uncompressed: false - body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"handle":"handle-28834","version":"version-72598"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 446.040166ms + duration: 433.973208ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 457 + content_length: 445 uncompressed: false - body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-72598"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 290.194833ms + duration: 250.91925ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -94,35 +94,35 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 457 + content_length: 445 uncompressed: false - body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-72598"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 352.281792ms + duration: 270.066917ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 522 + content_length: 516 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"},{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"}],"version":"version-72598"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"},{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: - application/json Content-Type: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: PUT response: proto: HTTP/1.1 @@ -130,15 +130,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 135 + content_length: 123 uncompressed: false - body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"handle":"handle-28834","version":"version-72598"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 715.978459ms + duration: 675.866583ms - id: 4 request: proto: HTTP/1.1 @@ -155,7 +155,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -163,15 +163,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 555 + content_length: 543 uncompressed: false - body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","requirements":[{"name":"requirement","controls":[{"name":"control","rules_id":["def-000-be9"]}]},{"name":"requirement-2","controls":[{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]},{"name":"control-2","rules_id":["def-000-be9"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-cea"]}]}],"version":"version-72598"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"requirement","controls":[{"name":"control","rules_id":["def-000-be9"]}]},{"name":"requirement-2","controls":[{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]},{"name":"control-2","rules_id":["def-000-be9"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 544.192875ms + duration: 520.399125ms - id: 5 request: proto: HTTP/1.1 @@ -188,7 +188,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: DELETE response: proto: HTTP/1.1 @@ -196,15 +196,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 208 + content_length: 196 uncompressed: false - body: '{"data":{"id":"handle-28834-version-72598","type":"custom_framework","attributes":{"description":"test description","handle":"handle-28834","icon_url":"test url","name":"new-name","version":"version-72598"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 386.574958ms + duration: 511.597791ms - id: 6 request: proto: HTTP/1.1 @@ -221,7 +221,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-28834/version-72598 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -237,4 +237,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 55.230583ms + duration: 72.25475ms diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze index d3201cd86..fbcb817b2 100644 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze @@ -1 +1 @@ -2025-04-27T16:14:07.509872-04:00 \ No newline at end of file +2025-05-01T16:07:06.340903-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml index cb29a73c6..589612fbf 100644 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 522 + content_length: 516 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"version-61251"},"type":"custom_framework"}} + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -28,15 +28,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 135 + content_length: 123 uncompressed: false - body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"handle":"handle-54746","version":"version-61251"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 541.320792ms + duration: 421.758333ms - id: 1 request: proto: HTTP/1.1 @@ -53,7 +53,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 555 + content_length: 543 uncompressed: false - body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-61251"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 693.301709ms + duration: 456.435916ms - id: 2 request: proto: HTTP/1.1 @@ -86,7 +86,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -94,15 +94,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 555 + content_length: 543 uncompressed: false - body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-61251"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 668.485375ms + duration: 616.197209ms - id: 3 request: proto: HTTP/1.1 @@ -119,7 +119,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -127,15 +127,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 555 + content_length: 543 uncompressed: false - body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-61251"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 660.625791ms + duration: 469.077833ms - id: 4 request: proto: HTTP/1.1 @@ -152,7 +152,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: DELETE response: proto: HTTP/1.1 @@ -160,15 +160,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 223 + content_length: 211 uncompressed: false - body: '{"data":{"id":"handle-54746-version-61251","type":"custom_framework","attributes":{"description":"test description","handle":"handle-54746","icon_url":"test url","name":"new-framework-terraform","version":"version-61251"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 739.861834ms + duration: 419.173334ms - id: 5 request: proto: HTTP/1.1 @@ -185,7 +185,7 @@ interactions: headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-54746/version-61251 + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 method: GET response: proto: HTTP/1.1 @@ -201,4 +201,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 64.415625ms + duration: 96.980917ms diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 37d97c538..770aa66a5 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -3,7 +3,6 @@ package test import ( "context" "fmt" - "math/rand" "regexp" "testing" @@ -14,9 +13,8 @@ import ( ) func TestCustomFramework_create(t *testing.T) { - t.Parallel() - handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := fmt.Sprintf("version-%d", rand.Intn(100000)) + handle := "terraform-handle" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -68,9 +66,8 @@ func TestCustomFramework_create(t *testing.T) { } func TestCustomFramework_createAndUpdateMultipleRequirements(t *testing.T) { - t.Parallel() - handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := fmt.Sprintf("version-%d", rand.Intn(100000)) + handle := "terraform-handle" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -135,9 +132,8 @@ func TestCustomFramework_createAndUpdateMultipleRequirements(t *testing.T) { } func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { - t.Parallel() - handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := fmt.Sprintf("version-%d", rand.Intn(100000)) + handle := "terraform-handle" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -291,9 +287,8 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { // There is no way to validate the duplicate rule IDs in the config before they are removed from the state in Terraform // This test validates that the duplicate rule IDs are removed from the state and only one rule ID is present func TestCustomFramework_DuplicateRuleIds(t *testing.T) { - t.Parallel() - handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := fmt.Sprintf("version-%d", rand.Intn(100000)) + handle := "terraform-handle" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -321,9 +316,8 @@ func TestCustomFramework_DuplicateRuleIds(t *testing.T) { } func TestCustomFramework_InvalidCreate(t *testing.T) { - t.Parallel() - handle := fmt.Sprintf("handle-%d", rand.Intn(100000)) - version := fmt.Sprintf("version-%d", rand.Intn(100000)) + handle := "terraform-handle" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -369,9 +363,8 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { } func TestCustomFramework_RecreateAfterAPIDelete(t *testing.T) { - t.Parallel() handle := "terraform-handle" - version := "1.0.0" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -420,9 +413,8 @@ func TestCustomFramework_RecreateAfterAPIDelete(t *testing.T) { } func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) { - t.Parallel() handle := "terraform-handle" - version := "1.0.0" + version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_custom_framework.sample_rules" @@ -654,9 +646,6 @@ func testAccCheckDatadogCreateEmptyVersion(handle string) string { `, handle) } -// TODO: Add validation for duplicate requirements and controls because the state model -// converts the requirements into sets and the duplicate requirements are deleted - func testAccCheckDatadogDuplicateRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { From 7a3966ad53f15ca54053997382ac5acff08fdb8c Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 1 May 2025 17:17:16 -0400 Subject: [PATCH 18/54] add test for 409 conflict --- .../TestCustomFramework_CreateConflict.freeze | 1 + .../TestCustomFramework_CreateConflict.yaml | 108 ++++++++++++++++++ .../resource_datadog_custom_framework_test.go | 74 ++++++++++++ .../datadog_custom_framework/import.sh | 1 + 4 files changed, 184 insertions(+) create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze new file mode 100644 index 000000000..5f2dc3d31 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze @@ -0,0 +1 @@ +2025-05-01T17:16:53.65853-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml new file mode 100644 index 000000000..be89ac934 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml @@ -0,0 +1,108 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 103.46125ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 285 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 496.842834ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 285 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 127 + uncompressed: false + body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 409 Conflict + code: 409 + duration: 190.623167ms diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_custom_framework_test.go index 770aa66a5..40b10d9e2 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_custom_framework_test.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" ) @@ -461,6 +463,78 @@ func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) { }) } +func TestCustomFramework_CreateConflict(t *testing.T) { + handle := "terraform-handle" + version := "1.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_custom_framework.sample_rules" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + // First create the framework using the API directly + Config: "# Empty config since we're creating via API", + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + // Create framework using API + api := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2() + auth := providers.frameworkProvider.Auth + + // Check if framework exists and delete it if it does + _, httpRes, err := api.GetCustomFramework(auth, handle, version) + if err == nil && httpRes.StatusCode == 200 { + // Framework exists, delete it + _, _, err = api.DeleteCustomFramework(auth, handle, version) + if err != nil { + return fmt.Errorf("failed to delete existing framework: %v", err) + } + } + + // Create a basic framework that matches the config we'll try to create + createRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() + description := "test description" + iconURL := "test url" + createRequest.SetData(datadogV2.CustomFrameworkData{ + Type: "custom_framework", + Attributes: datadogV2.CustomFrameworkDataAttributes{ + Handle: handle, + Name: "new-framework-terraform", + Description: &description, + IconUrl: &iconURL, + Version: version, + Requirements: []datadogV2.CustomFrameworkRequirement{ + *datadogV2.NewCustomFrameworkRequirement( + []datadogV2.CustomFrameworkControl{ + *datadogV2.NewCustomFrameworkControl("control1", []string{"def-000-be9"}), + }, + "requirement1", + ), + }, + }, + }) + + // Create the framework + _, _, err = api.CreateCustomFramework(auth, *createRequest) + if err != nil { + return fmt.Errorf("failed to create framework: %v", err) + } + return nil + }, + ), + }, + { + // Try to create the same framework through Terraform + Config: testAccCheckDatadogCreateFramework(version, handle), + ExpectError: regexp.MustCompile("409 Conflict"), + }, + }, + }) +} + func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_custom_framework" "sample_rules" { diff --git a/examples/resources/datadog_custom_framework/import.sh b/examples/resources/datadog_custom_framework/import.sh index e69de29bb..5c0d83796 100755 --- a/examples/resources/datadog_custom_framework/import.sh +++ b/examples/resources/datadog_custom_framework/import.sh @@ -0,0 +1 @@ +terraform import datadog_custom_framework.example3 "terraform-created-framework-handle-1.0.0" \ No newline at end of file From 5925d171a85ac9e14fcf28c8f490532eefbfb1b2 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 5 May 2025 12:54:20 -0400 Subject: [PATCH 19/54] add a resource file --- .../datadog_custom_framework/main.tf | 39 ------------------- .../datadog_custom_framework/resource.tf | 18 +++++++++ 2 files changed, 18 insertions(+), 39 deletions(-) delete mode 100644 examples/resources/datadog_custom_framework/main.tf create mode 100644 examples/resources/datadog_custom_framework/resource.tf diff --git a/examples/resources/datadog_custom_framework/main.tf b/examples/resources/datadog_custom_framework/main.tf deleted file mode 100644 index a45248489..000000000 --- a/examples/resources/datadog_custom_framework/main.tf +++ /dev/null @@ -1,39 +0,0 @@ -terraform { - required_providers { - datadog = { - source = "DataDog/datadog" - version = ">= 3.45.0" - } - } -} - -provider "datadog" { - api_key = var.datadog_api_key - app_key = var.datadog_app_key -} - - -resource "datadog_custom_framework" "example3" { - version = "1.0.0" - handle = "terraform-created-framework-handle" - name = "terraform-created-framework" - icon_url = "https://example.com/icon.png" - description = "This is a test I created this resource through terraform" - requirements { - name = "requirement2" - controls { - name = "control3" - rules_id = ["def-000-cea"] - } - } - requirements { - name = "requirement1" - controls { - name = "changedControlName" - rules_id = ["def-000-cea", "def-000-be9"] - } - } -} - - - diff --git a/examples/resources/datadog_custom_framework/resource.tf b/examples/resources/datadog_custom_framework/resource.tf new file mode 100644 index 000000000..26a87c3d1 --- /dev/null +++ b/examples/resources/datadog_custom_framework/resource.tf @@ -0,0 +1,18 @@ +resource "datadog_custom_framework" "example4" { + version = "1" + handle = "new-terraform-framework-handle" + name = "new-terraform-framework" + icon_url = "https://example.com/icon.png" + description = "This is a test I created this resource through terraform" + requirements { + name = "requirement1" + controls { + name = "new-control" + rules_id = ["def-000-be9", "def-000-cea"] + } + controls { + name = "new-control" + rules_id = ["def-000-be9"] + } + } +} \ No newline at end of file From 9c73be6ce9f82b37df6e79c77fd1bc4278f0d815 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Tue, 6 May 2025 10:14:10 -0400 Subject: [PATCH 20/54] add example in doc and remove comments --- .../resource_datadog_custom_framework.go | 15 ----------- docs/resources/custom_framework.md | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_custom_framework.go index 785c5d3a5..c62198340 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_custom_framework.go @@ -162,21 +162,6 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea data, _, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) // If the framework does not exist, remove it from terraform state // This is to avoid the provider to return an error when the framework is deleted in the UI prior - - // DELETE example: - // 1. create the framework in terraform - // 2. delete the framework in the UI - // 3. run terraform plan - // 4. terraform will see that the framework does not exist so it will remove it from the state - // 5. no changes are calculated by terraform so nothing is updated - - // UPDATE example: - // 1. create the framework in terraform - // 2. delete the framework in the UI - // 3. update the framework in terraform - // 4. run terraform plan - // 5. terraform will see that the framework does not exist so it will remove it from the state - // 6. terraform will perform a create action for the framework if err != nil && err.Error() == "400 Bad Request" { // Clear the state completely response.State.RemoveResource(ctx) diff --git a/docs/resources/custom_framework.md b/docs/resources/custom_framework.md index fb3e8a7f1..5651bb035 100644 --- a/docs/resources/custom_framework.md +++ b/docs/resources/custom_framework.md @@ -10,7 +10,28 @@ description: |- Manages custom framework in Datadog. - +## Example Usage + +```terraform +resource "datadog_custom_framework" "example4" { + version = "1" + handle = "new-terraform-framework-handle" + name = "new-terraform-framework" + icon_url = "https://example.com/icon.png" + description = "This is a test I created this resource through terraform" + requirements { + name = "requirement1" + controls { + name = "new-control" + rules_id = ["def-000-be9", "def-000-cea"] + } + controls { + name = "new-control" + rules_id = ["def-000-be9"] + } + } +} +``` ## Schema @@ -40,7 +61,7 @@ Required: Optional: -- `controls` (Block Set) The controls of the requirement. (see [below for nested schema](#nestedblock--requirements--controls)) +- `controls` (Block Set) The controls of the requirement. At least one control is required. (see [below for nested schema](#nestedblock--requirements--controls)) ### Nested Schema for `requirements.controls` @@ -55,5 +76,5 @@ Required: Import is supported using the following syntax: ```shell -terraform import datadog_custom_framework.example2 "new-terraform-framework-test-2" +terraform import datadog_custom_framework.example3 "terraform-created-framework-handle-1.0.0" ``` From 5717ad58553584d7dd6c17b98de1b4f0ef3ceb3e Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 7 May 2025 09:54:43 -0400 Subject: [PATCH 21/54] fix required requirements and control block --- datadog/fwprovider/framework_provider.go | 2 +- ...ce_datadog_compliance_custom_framework.go} | 80 +++++--- .../validators/duplicate_control_validator.go | 12 +- .../duplicate_requirement_validator.go | 4 +- .../TestCustomFramework_InvalidCreate.freeze | 2 +- .../TestCustomFramework_InvalidCreate.yaml | 182 +----------------- datadog/tests/provider_test.go | 2 +- ...tadog_compliance_custom_framework_test.go} | 111 +++++++---- .../datadog_custom_framework/resource.tf | 18 -- 9 files changed, 133 insertions(+), 280 deletions(-) rename datadog/fwprovider/{resource_datadog_custom_framework.go => resource_datadog_compliance_custom_framework.go} (78%) rename datadog/tests/{resource_datadog_custom_framework_test.go => resource_datadog_compliance_custom_framework_test.go} (89%) delete mode 100644 examples/resources/datadog_custom_framework/resource.tf diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index 158b9a3b7..2ca302fbd 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -88,7 +88,7 @@ var Resources = []func() resource.Resource{ NewOnCallScheduleResource, NewOnCallTeamRoutingRulesResource, NewSecurityMonitoringRuleJSONResource, - NewCustomFrameworkResource, + NewComplianceCustomFrameworkResource, } var Datasources = []func() datasource.DataSource{ diff --git a/datadog/fwprovider/resource_datadog_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go similarity index 78% rename from datadog/fwprovider/resource_datadog_custom_framework.go rename to datadog/fwprovider/resource_datadog_compliance_custom_framework.go index c62198340..914f1e056 100644 --- a/datadog/fwprovider/resource_datadog_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -6,6 +6,7 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -16,14 +17,14 @@ import ( "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/validators" ) -var _ resource.Resource = &customFrameworkResource{} +var _ resource.Resource = &complianceCustomFrameworkResource{} -type customFrameworkResource struct { +type complianceCustomFrameworkResource struct { Api *datadogV2.SecurityMonitoringApi Auth context.Context } -type customFrameworkModel struct { +type complianceCustomFrameworkModel struct { ID types.String `tfsdk:"id"` Description types.String `tfsdk:"description"` Version types.String `tfsdk:"version"` @@ -33,33 +34,42 @@ type customFrameworkModel struct { Requirements types.Set `tfsdk:"requirements"` // have to define requirements as a set to be unordered } -func NewCustomFrameworkResource() resource.Resource { - return &customFrameworkResource{} +func NewComplianceCustomFrameworkResource() resource.Resource { + return &complianceCustomFrameworkResource{} } -func (r *customFrameworkResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = "custom_framework" +func (r *complianceCustomFrameworkResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "compliance_custom_framework" } -func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { +func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = schema.Schema{ - Description: "Manages custom framework in Datadog.", + Description: "Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "The ID of the custom framework resource.", + Description: "The ID of the compliance custom framework resource.", Computed: true, }, "version": schema.StringAttribute{ Description: "The framework version.", - Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Required: true, }, "handle": schema.StringAttribute{ Description: "The framework handle.", - Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Required: true, }, "name": schema.StringAttribute{ Description: "The framework name.", - Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Required: true, }, "icon_url": schema.StringAttribute{ Description: "The URL of the icon representing the framework.", @@ -74,7 +84,7 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq "requirements": schema.SetNestedBlock{ Description: "The requirements of the framework.", Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), + setvalidator.IsRequired(), validators.RequirementNameValidator(), }, NestedObject: schema.NestedBlockObject{ @@ -82,13 +92,16 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq "name": schema.StringAttribute{ Description: "The name of the requirement.", Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, }, Blocks: map[string]schema.Block{ "controls": schema.SetNestedBlock{ Description: "The controls of the requirement.", Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), + setvalidator.IsRequired(), validators.ControlNameValidator(), }, NestedObject: schema.NestedBlockObject{ @@ -96,6 +109,9 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq "name": schema.StringAttribute{ Description: "The name of the control.", Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, }, "rules_id": schema.SetAttribute{ Description: "The list of rules IDs for the control.", @@ -112,14 +128,14 @@ func (r *customFrameworkResource) Schema(_ context.Context, _ resource.SchemaReq } } -func (r *customFrameworkResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { +func (r *complianceCustomFrameworkResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { providerData, _ := request.ProviderData.(*FrameworkProvider) r.Api = providerData.DatadogApiInstances.GetSecurityMonitoringApiV2() r.Auth = providerData.Auth } -func (r *customFrameworkResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - var state customFrameworkModel +func (r *complianceCustomFrameworkResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state complianceCustomFrameworkModel diags := request.Config.Get(ctx, &state) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -129,7 +145,7 @@ func (r *customFrameworkResource) Create(ctx context.Context, request resource.C _, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating custom framework")) + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating compliance custom framework")) return } state.ID = types.StringValue(state.Handle.ValueString() + string('-') + state.Version.ValueString()) @@ -137,8 +153,8 @@ func (r *customFrameworkResource) Create(ctx context.Context, request resource.C response.Diagnostics.Append(diags...) } -func (r *customFrameworkResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - var state customFrameworkModel +func (r *complianceCustomFrameworkResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state complianceCustomFrameworkModel diags := request.State.Get(ctx, &state) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -151,8 +167,8 @@ func (r *customFrameworkResource) Delete(ctx context.Context, request resource.D } } -func (r *customFrameworkResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - var state customFrameworkModel +func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state complianceCustomFrameworkModel diags := request.State.Get(ctx, &state) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -169,7 +185,7 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea } // this is for any other error if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading custom framework")) + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) return } databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString()) @@ -177,8 +193,8 @@ func (r *customFrameworkResource) Read(ctx context.Context, request resource.Rea response.Diagnostics.Append(diags...) } -func (r *customFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - var state customFrameworkModel +func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state complianceCustomFrameworkModel diags := request.Config.Get(ctx, &state) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -187,7 +203,7 @@ func (r *customFrameworkResource) Update(ctx context.Context, request resource.U _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating custom framework")) + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating compliance custom framework")) return } diags = response.State.Set(ctx, &state) @@ -238,9 +254,9 @@ func setRequirements(requirements []attr.Value) types.Set { requirements, ) } -func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string) customFrameworkModel { +func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string) complianceCustomFrameworkModel { // Set the state - var state customFrameworkModel + var state complianceCustomFrameworkModel state.ID = types.StringValue(handle + "-" + version) state.Handle = types.StringValue(handle) state.Version = types.StringValue(version) @@ -267,7 +283,7 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str } // ImportState is used to import a resource from an existing framework so we can update it if it exists in the database and not in terraform -func (r *customFrameworkResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { +func (r *complianceCustomFrameworkResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { // Split the ID into handle and version // The last hyphen separates handle and version lastHyphenIndex := strings.LastIndex(request.ID, "-") @@ -306,7 +322,7 @@ func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []d return frameworkRequirements } -func buildCreateFrameworkRequest(state customFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { +func buildCreateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { createFrameworkRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() description := state.Description.ValueString() iconURL := state.IconURL.ValueString() @@ -324,7 +340,7 @@ func buildCreateFrameworkRequest(state customFrameworkModel) *datadogV2.CreateCu return createFrameworkRequest } -func buildUpdateFrameworkRequest(state customFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest { +func buildUpdateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest { updateFrameworkRequest := datadogV2.NewUpdateCustomFrameworkRequestWithDefaults() description := state.Description.ValueString() iconURL := state.IconURL.ValueString() diff --git a/datadog/internal/validators/duplicate_control_validator.go b/datadog/internal/validators/duplicate_control_validator.go index 07fbfc0a8..31a20b689 100644 --- a/datadog/internal/validators/duplicate_control_validator.go +++ b/datadog/internal/validators/duplicate_control_validator.go @@ -3,7 +3,6 @@ package validators import ( "context" "fmt" - "log" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -24,26 +23,19 @@ func (v controlNameValidator) ValidateSet(ctx context.Context, req validator.Set return } - // Get all control names from the configuration var controlNames []string for _, control := range req.ConfigValue.Elements() { controlObj := control.(types.Object) name := controlObj.Attributes()["name"].(types.String).ValueString() - log.Printf("Found control name in config: %s", name) controlNames = append(controlNames, name) } - log.Printf("Found %d control names in config", len(controlNames)) - - // Check for duplicates in the list seen := make(map[string]bool) for _, name := range controlNames { - log.Printf("Checking control name: %s", name) if seen[name] { - log.Printf("Found duplicate control name: %s", name) resp.Diagnostics.AddError( - "400 Bad Request", - fmt.Sprintf("Control name '%s' is used more than once. Each control must have a unique name.", name), + "Each Control must have a unique name under the same requirement", + fmt.Sprintf("Control name '%s' is used more than once under the same requirement", name), ) return } diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_validator.go index e0e0dab17..9f425f0ba 100644 --- a/datadog/internal/validators/duplicate_requirement_validator.go +++ b/datadog/internal/validators/duplicate_requirement_validator.go @@ -42,8 +42,8 @@ func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator if seen[name] { log.Printf("Found duplicate requirement name: %s", name) resp.Diagnostics.AddError( - "400 Bad Request", - fmt.Sprintf("Requirement name '%s' is used more than once. Each requirement must have a unique name.", name), + "Each Requirement must have a unique name", + fmt.Sprintf("Requirement name '%s' is used more than once.", name), ) return } diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 6b9fc0b20..7a0e699e4 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-05-01T16:08:14.218018-04:00 \ No newline at end of file +2025-05-07T09:53:00.898171-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index 71501dfff..1ad39c718 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -2,42 +2,6 @@ version: 2 interactions: - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 262 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 158 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.name'' is invalid: field ''name'' must not be empty)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 110.766958ms - - id: 1 request: proto: HTTP/1.1 proto_major: 1 @@ -72,148 +36,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 179.166583ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 202 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 171 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.requirements'' is invalid: field ''requirements'' can''t be empty)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 47.237625ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 239 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 189 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.requirements.controls'' is invalid: field ''requirements.controls'' can''t be empty)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 46.84975ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 260 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 162 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.handle'' is invalid: field ''handle'' must not be empty)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 45.88825ms - - id: 5 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 273 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":""},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 164 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.version'' is invalid: field ''version'' must not be empty)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 61.689667ms + duration: 317.776875ms diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go index a32bb133d..fd45e2903 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -270,7 +270,7 @@ var testFiles2EndpointTags = map[string]string{ "tests/resource_datadog_webhook_custom_variable_test": "webhook_custom_variable", "tests/resource_datadog_webhook_test": "webhook", "tests/resource_datadog_workflow_automation_test": "workflow_automation", - "tests/resource_datadog_custom_framework_test": "custom_framework", + "tests/resource_datadog_compliance_custom_framework_test": "compliance_custom_framework", } // getEndpointTagValue traverses callstack frames to find the test function that invoked this call; diff --git a/datadog/tests/resource_datadog_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go similarity index 89% rename from datadog/tests/resource_datadog_custom_framework_test.go rename to datadog/tests/resource_datadog_compliance_custom_framework_test.go index 40b10d9e2..6426bddec 100644 --- a/datadog/tests/resource_datadog_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -19,7 +19,7 @@ func TestCustomFramework_create(t *testing.T) { version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, @@ -138,11 +138,11 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" // First config with one order of requirements config1 := fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-framework-terraform" @@ -179,7 +179,7 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { // Second config with different order of requirements // test switching order of requirements, controls, and rules_id config2 := fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-framework-terraform" @@ -284,16 +284,15 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { }) } -// Test that duplicate rule IDs are removed from the state -// Since the state model converts the rules_id into sets, the duplicate rule IDs are removed // There is no way to validate the duplicate rule IDs in the config before they are removed from the state in Terraform +// because the state model converts the rules_id into sets, the duplicate rule IDs are removed // This test validates that the duplicate rule IDs are removed from the state and only one rule ID is present func TestCustomFramework_DuplicateRuleIds(t *testing.T) { handle := "terraform-handle" version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, @@ -307,9 +306,7 @@ func TestCustomFramework_DuplicateRuleIds(t *testing.T) { resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ "name": "control1", }), - // duplicate rule ID should be removed resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), - // verify there is exactly one rule ID resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), ), }, @@ -322,7 +319,7 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, @@ -330,7 +327,7 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, { Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle), @@ -338,27 +335,35 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { }, { Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile("Invalid Block"), }, { Config: testAccCheckDatadogCreateFrameworkWithNoControls(version, handle), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile("Invalid Block"), }, { Config: testAccCheckDatadogCreateEmptyHandle(version), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, { Config: testAccCheckDatadogCreateEmptyVersion(handle), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, { Config: testAccCheckDatadogDuplicateRequirements(version, handle), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile(".*Each Requirement must have a unique name.*"), }, { Config: testAccCheckDatadogDuplicateControls(version, handle), - ExpectError: regexp.MustCompile("400 Bad Request"), + ExpectError: regexp.MustCompile(".*Each Control must have a unique name under the same requirement.*"), + }, + { + Config: testAccCheckDatadogEmptyRequirementName(version, handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), + }, + { + Config: testAccCheckDatadogEmptyControlName(version, handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, }, }) @@ -369,7 +374,7 @@ func TestCustomFramework_RecreateAfterAPIDelete(t *testing.T) { version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -419,7 +424,7 @@ func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) { version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -468,7 +473,7 @@ func TestCustomFramework_CreateConflict(t *testing.T) { version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -537,7 +542,7 @@ func TestCustomFramework_CreateConflict(t *testing.T) { func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-name" @@ -563,7 +568,7 @@ func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, func testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-name" @@ -600,7 +605,7 @@ func testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle func testAccCheckDatadogCreateFramework(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-framework-terraform" @@ -619,7 +624,7 @@ func testAccCheckDatadogCreateFramework(version string, handle string) string { func testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-framework-terraform" @@ -638,7 +643,7 @@ func testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version string, handle str func testAccCheckDatadogCreateFrameworkWithNoControls(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-framework-terraform" @@ -653,7 +658,7 @@ func testAccCheckDatadogCreateFrameworkWithNoControls(version string, handle str func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "new-framework-terraform" @@ -665,10 +670,10 @@ func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle str func testAccCheckDatadogCreateInvalidFrameworkName(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" - name = "" # Invalid empty name + name = "" description = "test description" icon_url = "test url" requirements { @@ -682,9 +687,47 @@ func testAccCheckDatadogCreateInvalidFrameworkName(version string, handle string `, version, handle) } +func testAccCheckDatadogEmptyRequirementName(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "name" + description = "test description" + icon_url = "test url" + requirements { + name = "" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogEmptyControlName(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "name" + description = "test description" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + func testAccCheckDatadogCreateEmptyHandle(version string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "" name = "framework-name" @@ -703,7 +746,7 @@ func testAccCheckDatadogCreateEmptyHandle(version string) string { func testAccCheckDatadogCreateEmptyVersion(handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "" handle = "%s" name = "framework-name" @@ -722,7 +765,7 @@ func testAccCheckDatadogCreateEmptyVersion(handle string) string { func testAccCheckDatadogDuplicateRequirements(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "framework-name" @@ -748,7 +791,7 @@ func testAccCheckDatadogDuplicateRequirements(version string, handle string) str func testAccCheckDatadogDuplicateControls(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "framework-name" @@ -762,7 +805,7 @@ func testAccCheckDatadogDuplicateControls(version string, handle string) string } } requirements { - name = "requirement1" + name = "requirement2" controls { name = "control1" rules_id = ["def-000-be9"] @@ -778,7 +821,7 @@ func testAccCheckDatadogDuplicateControls(version string, handle string) string func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { return fmt.Sprintf(` - resource "datadog_custom_framework" "sample_rules" { + resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" handle = "%s" name = "framework-name" diff --git a/examples/resources/datadog_custom_framework/resource.tf b/examples/resources/datadog_custom_framework/resource.tf deleted file mode 100644 index 26a87c3d1..000000000 --- a/examples/resources/datadog_custom_framework/resource.tf +++ /dev/null @@ -1,18 +0,0 @@ -resource "datadog_custom_framework" "example4" { - version = "1" - handle = "new-terraform-framework-handle" - name = "new-terraform-framework" - icon_url = "https://example.com/icon.png" - description = "This is a test I created this resource through terraform" - requirements { - name = "requirement1" - controls { - name = "new-control" - rules_id = ["def-000-be9", "def-000-cea"] - } - controls { - name = "new-control" - rules_id = ["def-000-be9"] - } - } -} \ No newline at end of file From 9941c0df7c2a0d625d74d7d0ce3228e40efebc71 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 7 May 2025 10:04:41 -0400 Subject: [PATCH 22/54] changeexample to compliance custom framework --- docs/resources/compliance_custom_framework.md | 87 +++++++++++++++++++ docs/resources/custom_framework.md | 80 ----------------- .../import.sh | 0 .../resource.tf | 25 ++++++ .../datadog_custom_framework/variables.tf | 11 --- 5 files changed, 112 insertions(+), 91 deletions(-) create mode 100644 docs/resources/compliance_custom_framework.md delete mode 100644 docs/resources/custom_framework.md rename examples/resources/{datadog_custom_framework => datadog_compliance_custom_framework}/import.sh (100%) create mode 100644 examples/resources/datadog_compliance_custom_framework/resource.tf delete mode 100644 examples/resources/datadog_custom_framework/variables.tf diff --git a/docs/resources/compliance_custom_framework.md b/docs/resources/compliance_custom_framework.md new file mode 100644 index 000000000..be9995f84 --- /dev/null +++ b/docs/resources/compliance_custom_framework.md @@ -0,0 +1,87 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_compliance_custom_framework Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks. +--- + +# datadog_compliance_custom_framework (Resource) + +Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks. + +## Example Usage + +```terraform +resource "datadog_compliance_custom_framework" "example" { + version = "1" + handle = "new-terraform-framework-handle" + name = "new-terraform-framework" + icon_url = "https://example.com/icon.png" + description = "This is a test I created this resource through terraform" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["aaa-000-ccc", "bbb-000-ddd"] + } + controls { + name = "control2" + rules_id = ["aaa-000-lll"] + } + } + requirements { + name = "requirement2" + controls { + name = "control3" + rules_id = ["aaa-000-zzz"] + } + } +} +``` + + +## Schema + +### Required + +- `handle` (String) The framework handle. String length must be at least 1. +- `name` (String) The framework name. String length must be at least 1. +- `version` (String) The framework version. String length must be at least 1. + +### Optional + +- `description` (String) The description of the framework. +- `icon_url` (String) The URL of the icon representing the framework. +- `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) + +### Read-Only + +- `id` (String) The ID of the compliance custom framework resource. + + +### Nested Schema for `requirements` + +Required: + +- `name` (String) The name of the requirement. String length must be at least 1. + +Optional: + +- `controls` (Block Set) The controls of the requirement. (see [below for nested schema](#nestedblock--requirements--controls)) + + +### Nested Schema for `requirements.controls` + +Required: + +- `name` (String) The name of the control. String length must be at least 1. +- `rules_id` (Set of String) The list of rules IDs for the control. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import datadog_custom_framework.example3 "terraform-created-framework-handle-1.0.0" +``` diff --git a/docs/resources/custom_framework.md b/docs/resources/custom_framework.md deleted file mode 100644 index 5651bb035..000000000 --- a/docs/resources/custom_framework.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "datadog_custom_framework Resource - terraform-provider-datadog" -subcategory: "" -description: |- - Manages custom framework in Datadog. ---- - -# datadog_custom_framework (Resource) - -Manages custom framework in Datadog. - -## Example Usage - -```terraform -resource "datadog_custom_framework" "example4" { - version = "1" - handle = "new-terraform-framework-handle" - name = "new-terraform-framework" - icon_url = "https://example.com/icon.png" - description = "This is a test I created this resource through terraform" - requirements { - name = "requirement1" - controls { - name = "new-control" - rules_id = ["def-000-be9", "def-000-cea"] - } - controls { - name = "new-control" - rules_id = ["def-000-be9"] - } - } -} -``` - - -## Schema - -### Required - -- `handle` (String) The framework handle. -- `name` (String) The framework name. -- `version` (String) The framework version. - -### Optional - -- `description` (String) The description of the framework. -- `icon_url` (String) The URL of the icon representing the framework. -- `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) - -### Read-Only - -- `id` (String) The ID of the custom framework resource. - - -### Nested Schema for `requirements` - -Required: - -- `name` (String) The name of the requirement. - -Optional: - -- `controls` (Block Set) The controls of the requirement. At least one control is required. (see [below for nested schema](#nestedblock--requirements--controls)) - - -### Nested Schema for `requirements.controls` - -Required: - -- `name` (String) The name of the control. -- `rules_id` (Set of String) The list of rules IDs for the control. - -## Import - -Import is supported using the following syntax: - -```shell -terraform import datadog_custom_framework.example3 "terraform-created-framework-handle-1.0.0" -``` diff --git a/examples/resources/datadog_custom_framework/import.sh b/examples/resources/datadog_compliance_custom_framework/import.sh similarity index 100% rename from examples/resources/datadog_custom_framework/import.sh rename to examples/resources/datadog_compliance_custom_framework/import.sh diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf new file mode 100644 index 000000000..b7a3e2a38 --- /dev/null +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -0,0 +1,25 @@ +resource "datadog_compliance_custom_framework" "example" { + version = "1" + handle = "new-terraform-framework-handle" + name = "new-terraform-framework" + icon_url = "https://example.com/icon.png" + description = "This is a test I created this resource through terraform" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["aaa-000-ccc", "bbb-000-ddd"] + } + controls { + name = "control2" + rules_id = ["aaa-000-lll"] + } + } + requirements { + name = "requirement2" + controls { + name = "control3" + rules_id = ["aaa-000-zzz"] + } + } +} \ No newline at end of file diff --git a/examples/resources/datadog_custom_framework/variables.tf b/examples/resources/datadog_custom_framework/variables.tf deleted file mode 100644 index d714e4cbd..000000000 --- a/examples/resources/datadog_custom_framework/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "datadog_api_key" { - description = "Datadog API key" - type = string - sensitive = true -} - -variable "datadog_app_key" { - description = "Datadog APP key" - type = string - sensitive = true -} \ No newline at end of file From 8a49998e543a0211455992dd210392cd96fd88bc Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 7 May 2025 10:17:46 -0400 Subject: [PATCH 23/54] fix docs --- docs/resources/compliance_custom_framework.md | 5 +---- scripts/generate-docs.sh | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/resources/compliance_custom_framework.md b/docs/resources/compliance_custom_framework.md index be9995f84..2d59da274 100644 --- a/docs/resources/compliance_custom_framework.md +++ b/docs/resources/compliance_custom_framework.md @@ -48,12 +48,12 @@ resource "datadog_compliance_custom_framework" "example" { - `handle` (String) The framework handle. String length must be at least 1. - `name` (String) The framework name. String length must be at least 1. - `version` (String) The framework version. String length must be at least 1. +- `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) ### Optional - `description` (String) The description of the framework. - `icon_url` (String) The URL of the icon representing the framework. -- `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) ### Read-Only @@ -65,9 +65,6 @@ resource "datadog_compliance_custom_framework" "example" { Required: - `name` (String) The name of the requirement. String length must be at least 1. - -Optional: - - `controls` (Block Set) The controls of the requirement. (see [below for nested schema](#nestedblock--requirements--controls)) diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh index 7eb1cd4bc..522010be1 100755 --- a/scripts/generate-docs.sh +++ b/scripts/generate-docs.sh @@ -4,6 +4,7 @@ # Add here the files to be excluded from the doc generation exclude_files=( "docs/resources/integration_aws_account.md" + "docs/resources/compliance_custom_framework.md" ) # Check if manual changes were made to any excluded files and exit From 70c34e9eec9fb921cadde4a5a8731cc10db49f9a Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 14 May 2025 13:25:18 -0400 Subject: [PATCH 24/54] make icon url optional and remove description --- ...rce_datadog_compliance_custom_framework.go | 37 +-- .../TestCustomFramework_CreateConflict.freeze | 2 +- .../TestCustomFramework_CreateConflict.yaml | 14 +- ...ustomFramework_CreateWithoutIconURL.freeze | 1 + ...tCustomFramework_CreateWithoutIconURL.yaml | 138 ++++++++ ...ustomFramework_DeleteAfterAPIDelete.freeze | 2 +- ...tCustomFramework_DeleteAfterAPIDelete.yaml | 28 +- ...estCustomFramework_DuplicateRuleIds.freeze | 2 +- .../TestCustomFramework_DuplicateRuleIds.yaml | 20 +- ...tomFramework_RecreateAfterAPIDelete.freeze | 2 +- ...ustomFramework_RecreateAfterAPIDelete.yaml | 314 +----------------- .../TestCustomFramework_create.freeze | 2 +- .../cassettes/TestCustomFramework_create.yaml | 151 +-------- ...createAndUpdateMultipleRequirements.freeze | 2 +- ...k_createAndUpdateMultipleRequirements.yaml | 124 +------ ...atadog_compliance_custom_framework_test.go | 113 +++++-- docs/resources/compliance_custom_framework.md | 6 +- 17 files changed, 300 insertions(+), 658 deletions(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 914f1e056..7c3b78a66 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -26,7 +26,6 @@ type complianceCustomFrameworkResource struct { type complianceCustomFrameworkModel struct { ID types.String `tfsdk:"id"` - Description types.String `tfsdk:"description"` Version types.String `tfsdk:"version"` Handle types.String `tfsdk:"handle"` Name types.String `tfsdk:"name"` @@ -72,11 +71,7 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Required: true, }, "icon_url": schema.StringAttribute{ - Description: "The URL of the icon representing the framework.", - Optional: true, - }, - "description": schema.StringAttribute{ - Description: "The description of the framework.", + Description: "The URL of the icon representing the framework. This can be set to empty if NA", Optional: true, }, }, @@ -179,11 +174,9 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re // If the framework does not exist, remove it from terraform state // This is to avoid the provider to return an error when the framework is deleted in the UI prior if err != nil && err.Error() == "400 Bad Request" { - // Clear the state completely response.State.RemoveResource(ctx) return } - // this is for any other error if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) return @@ -255,19 +248,17 @@ func setRequirements(requirements []attr.Value) types.Set { ) } func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string) complianceCustomFrameworkModel { - // Set the state var state complianceCustomFrameworkModel state.ID = types.StringValue(handle + "-" + version) state.Handle = types.StringValue(handle) state.Version = types.StringValue(version) state.Name = types.StringValue(data.GetData().Attributes.Name) - state.Description = types.StringValue(data.GetData().Attributes.Description) - state.IconURL = types.StringValue(data.GetData().Attributes.IconUrl) + if data.GetData().Attributes.IconUrl != nil { + state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl) + } - // Convert requirements to set requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements)) for i, requirement := range data.GetData().Attributes.Requirements { - // Convert controls to set controls := make([]attr.Value, len(requirement.Controls)) for j, control := range requirement.Controls { rulesID := make([]attr.Value, len(control.RulesId)) @@ -323,16 +314,18 @@ func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []d } func buildCreateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { + var iconURL *string + if !state.IconURL.IsNull() && !state.IconURL.IsUnknown() { + iconURLStr := state.IconURL.ValueString() + iconURL = &iconURLStr + } createFrameworkRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() - description := state.Description.ValueString() - iconURL := state.IconURL.ValueString() createFrameworkRequest.SetData(datadogV2.CustomFrameworkData{ Type: "custom_framework", Attributes: datadogV2.CustomFrameworkDataAttributes{ Handle: state.Handle.ValueString(), Name: state.Name.ValueString(), - Description: &description, - IconUrl: &iconURL, + IconUrl: iconURL, Version: state.Version.ValueString(), Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements), }, @@ -341,17 +334,19 @@ func buildCreateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV } func buildUpdateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest { + var iconURL *string + if !state.IconURL.IsNull() && !state.IconURL.IsUnknown() { + iconURLStr := state.IconURL.ValueString() + iconURL = &iconURLStr + } updateFrameworkRequest := datadogV2.NewUpdateCustomFrameworkRequestWithDefaults() - description := state.Description.ValueString() - iconURL := state.IconURL.ValueString() updateFrameworkRequest.SetData(datadogV2.CustomFrameworkData{ Type: "custom_framework", Attributes: datadogV2.CustomFrameworkDataAttributes{ Handle: state.Handle.ValueString(), Name: state.Name.ValueString(), - Description: &description, - IconUrl: &iconURL, Version: state.Version.ValueString(), + IconUrl: iconURL, Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements), }, }) diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze index 5f2dc3d31..ef49b639b 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze @@ -1 +1 @@ -2025-05-01T17:16:53.65853-04:00 \ No newline at end of file +2025-05-14T13:18:54.151434-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml index be89ac934..b9d238aa4 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml @@ -33,20 +33,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 103.46125ms + duration: 54.499584ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 285 + content_length: 252 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -69,20 +69,20 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 496.842834ms + duration: 418.422333ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 285 + content_length: 252 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -105,4 +105,4 @@ interactions: - application/vnd.api+json status: 409 Conflict code: 409 - duration: 190.623167ms + duration: 174.318959ms diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze new file mode 100644 index 000000000..ee4bbd18e --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze @@ -0,0 +1 @@ +2025-05-14T13:21:36.261823-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml new file mode 100644 index 000000000..2f9987fe1 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml @@ -0,0 +1,138 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 230 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 422.992708ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 257 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 117.490292ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 187 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 292.465917ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 57.986458ms diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze index aaf2b2381..ef7ad98ce 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze @@ -1 +1 @@ -2025-05-01T16:09:17.103315-04:00 \ No newline at end of file +2025-05-14T13:14:53.105131-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml index 529e287f9..f313cb7eb 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 285 + content_length: 252 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 373.761959ms + duration: 619.970417ms - id: 1 request: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 312 + content_length: 279 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 150.353417ms + duration: 239.534167ms - id: 2 request: proto: HTTP/1.1 @@ -94,15 +94,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 312 + content_length: 279 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 286.217625ms + duration: 192.272833ms - id: 3 request: proto: HTTP/1.1 @@ -127,15 +127,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 211 + content_length: 195 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 333.150375ms + duration: 450.136667ms - id: 4 request: proto: HTTP/1.1 @@ -168,7 +168,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 77.855792ms + duration: 46.896708ms - id: 5 request: proto: HTTP/1.1 @@ -201,4 +201,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 38.812542ms + duration: 59.534792ms diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze index 92f50e9c9..691798890 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze @@ -1 +1 @@ -2025-05-01T16:07:44.08489-04:00 \ No newline at end of file +2025-05-14T13:22:06.085019-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml index 607264566..f3f425d6b 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 276 + content_length: 243 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 399.097083ms + duration: 239.663166ms - id: 1 request: proto: HTTP/1.1 @@ -61,15 +61,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 303 + content_length: 270 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"framework-name","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 177.151375ms + duration: 118.675416ms - id: 2 request: proto: HTTP/1.1 @@ -94,15 +94,15 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 202 + content_length: 186 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"framework-name","version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"framework-name","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 557.034125ms + duration: 392.879417ms - id: 3 request: proto: HTTP/1.1 @@ -135,4 +135,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 52.814875ms + duration: 41.4355ms diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze index 692f505d3..c7e31dfc7 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze @@ -1 +1 @@ -2025-05-01T16:08:45.678897-04:00 \ No newline at end of file +2025-05-14T13:17:06.561175-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml index 5444983f6..1d1de7978 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 285 + content_length: 252 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -28,312 +28,12 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 123 + content_length: 127 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' headers: Content-Type: - application/vnd.api+json - status: 200 OK - code: 200 - duration: 367.168ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 312 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 176.838292ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 312 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 183.163041ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 211 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 453.045917ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 51.678417ms - - id: 5 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 65.162875ms - - id: 6 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 418 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 372.577542ms - - id: 7 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 445 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 313.339166ms - - id: 8 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 196 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 409.619209ms - - id: 9 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 39.744125ms + status: 409 Conflict + code: 409 + duration: 101.37575ms diff --git a/datadog/tests/cassettes/TestCustomFramework_create.freeze b/datadog/tests/cassettes/TestCustomFramework_create.freeze index 0c9d54631..cea8167b9 100644 --- a/datadog/tests/cassettes/TestCustomFramework_create.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_create.freeze @@ -1 +1 @@ -2025-05-01T16:05:52.032549-04:00 \ No newline at end of file +2025-05-07T13:00:16.869464-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_create.yaml b/datadog/tests/cassettes/TestCustomFramework_create.yaml index 1f3172a6e..25ab7ca10 100644 --- a/datadog/tests/cassettes/TestCustomFramework_create.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_create.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 285 + content_length: 230 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,143 +36,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 392.627417ms + duration: 430.208042ms - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 312 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 161.583958ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 312 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 165.086166ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 418 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: PUT - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 555.135417ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 445 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 250.856292ms - - id: 5 request: proto: HTTP/1.1 proto_major: 1 @@ -196,16 +61,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 196 + content_length: 187 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-framework-terraform","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 391.974125ms - - id: 6 + duration: 471.99225ms + - id: 2 request: proto: HTTP/1.1 proto_major: 1 @@ -237,4 +102,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 50.065625ms + duration: 97.036042ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze index ed08446cc..2dc953470 100644 --- a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze @@ -1 +1 @@ -2025-05-01T16:06:28.451812-04:00 \ No newline at end of file +2025-05-07T13:00:22.011299-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml index bd99a00b9..73441cb0e 100644 --- a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 418 + content_length: 363 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 433.973208ms + duration: 382.595ms - id: 1 request: proto: HTTP/1.1 @@ -61,118 +61,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 445 + content_length: 421 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 250.91925ms + duration: 284.132583ms - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 445 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 270.066917ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 516 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"},{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: PUT - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 675.866583ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 543 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"requirement","controls":[{"name":"control","rules_id":["def-000-be9"]}]},{"name":"requirement-2","controls":[{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]},{"name":"control-2","rules_id":["def-000-be9"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 520.399125ms - - id: 5 request: proto: HTTP/1.1 proto_major: 1 @@ -196,16 +94,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 196 + content_length: 172 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-name","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 511.597791ms - - id: 6 + duration: 581.523ms + - id: 3 request: proto: HTTP/1.1 proto_major: 1 @@ -237,4 +135,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 72.25475ms + duration: 91.224583ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 6426bddec..3f6a5a2c6 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -14,7 +14,7 @@ import ( "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" ) -func TestCustomFramework_create(t *testing.T) { +func TestCustomFramework_Create(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -31,8 +31,7 @@ func TestCustomFramework_create(t *testing.T) { resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckResourceAttr(path, "description", "test description"), - resource.TestCheckResourceAttr(path, "icon_url", "test url"), + resource.TestCheckResourceAttr(path, "icon_url", "test-url"), resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ "name": "requirement1", }), @@ -63,16 +62,61 @@ func TestCustomFramework_create(t *testing.T) { resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), ), }, + { + Config: testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control1", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + ), + }, }, }) } -func TestCustomFramework_createAndUpdateMultipleRequirements(t *testing.T) { +func TestCustomFramework_CreateWithoutIconURL(t *testing.T) { handle := "terraform-handle" version := "1.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_custom_framework.sample_rules" + path := "datadog_compliance_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ + "name": "requirement1", + }), + resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ + "name": "control1", + }), + resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + ), + }, + }, + }) +} + +func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) { + handle := "terraform-handle" + version := "1.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_compliance_custom_framework.sample_rules" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, @@ -133,7 +177,7 @@ func TestCustomFramework_createAndUpdateMultipleRequirements(t *testing.T) { }) } -func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { +func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -146,7 +190,6 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { version = "%s" handle = "%s" name = "new-framework-terraform" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -183,7 +226,6 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { version = "%s" handle = "%s" name = "new-framework-terraform" - description = "test description" icon_url = "test url" requirements { name = "requirement3" @@ -224,7 +266,6 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckResourceAttr(path, "description", "test description"), resource.TestCheckResourceAttr(path, "icon_url", "test url"), resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ "name": "requirement1", @@ -254,7 +295,6 @@ func TestCustomFramework_sameConfigNoUpdate(t *testing.T) { resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckResourceAttr(path, "description", "test description"), resource.TestCheckResourceAttr(path, "icon_url", "test url"), resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ "name": "requirement1", @@ -501,16 +541,14 @@ func TestCustomFramework_CreateConflict(t *testing.T) { // Create a basic framework that matches the config we'll try to create createRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() - description := "test description" iconURL := "test url" createRequest.SetData(datadogV2.CustomFrameworkData{ Type: "custom_framework", Attributes: datadogV2.CustomFrameworkDataAttributes{ - Handle: handle, - Name: "new-framework-terraform", - Description: &description, - IconUrl: &iconURL, - Version: version, + Handle: handle, + Name: "new-framework-terraform", + IconUrl: &iconURL, + Version: version, Requirements: []datadogV2.CustomFrameworkRequirement{ *datadogV2.NewCustomFrameworkRequirement( []datadogV2.CustomFrameworkControl{ @@ -546,7 +584,6 @@ func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, version = "%s" handle = "%s" name = "new-name" - description = "test description" icon_url = "test url" requirements { name = "security-requirement" @@ -572,7 +609,6 @@ func testAccCheckDatadogUpdateFrameworkWithMultipleRequirements(version, handle version = "%s" handle = "%s" name = "new-name" - description = "test description" icon_url = "test url" requirements { name = "security-requirement" @@ -609,8 +645,24 @@ func testAccCheckDatadogCreateFramework(version string, handle string) string { version = "%s" handle = "%s" name = "new-framework-terraform" - description = "test description" - icon_url = "test url" + icon_url = "test-url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" requirements { name = "requirement1" controls { @@ -628,7 +680,6 @@ func testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version string, handle str version = "%s" handle = "%s" name = "new-framework-terraform" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -647,7 +698,6 @@ func testAccCheckDatadogCreateFrameworkWithNoControls(version string, handle str version = "%s" handle = "%s" name = "new-framework-terraform" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -662,7 +712,6 @@ func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle str version = "%s" handle = "%s" name = "new-framework-terraform" - description = "test description" icon_url = "test url" } `, version, handle) @@ -674,7 +723,6 @@ func testAccCheckDatadogCreateInvalidFrameworkName(version string, handle string version = "%s" handle = "%s" name = "" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -693,7 +741,6 @@ func testAccCheckDatadogEmptyRequirementName(version string, handle string) stri version = "%s" handle = "%s" name = "name" - description = "test description" icon_url = "test url" requirements { name = "" @@ -712,7 +759,6 @@ func testAccCheckDatadogEmptyControlName(version string, handle string) string { version = "%s" handle = "%s" name = "name" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -731,7 +777,6 @@ func testAccCheckDatadogCreateEmptyHandle(version string) string { version = "%s" handle = "" name = "framework-name" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -750,7 +795,6 @@ func testAccCheckDatadogCreateEmptyVersion(handle string) string { version = "" handle = "%s" name = "framework-name" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -769,7 +813,6 @@ func testAccCheckDatadogDuplicateRequirements(version string, handle string) str version = "%s" handle = "%s" name = "framework-name" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -795,7 +838,6 @@ func testAccCheckDatadogDuplicateControls(version string, handle string) string version = "%s" handle = "%s" name = "framework-name" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -825,7 +867,6 @@ func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { version = "%s" handle = "%s" name = "framework-name" - description = "test description" icon_url = "test url" requirements { name = "requirement1" @@ -841,12 +882,18 @@ func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { func testAccCheckDatadogFrameworkDestroy(ctx context.Context, accProvider *fwprovider.FrameworkProvider, resourceName string, version string, handle string) func(*terraform.State) error { return func(s *terraform.State) error { apiInstances := accProvider.DatadogApiInstances - resource := s.RootModule().Resources[resourceName] + resource, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil + } + if resource.Primary == nil { + return nil + } handle := resource.Primary.Attributes["handle"] version := resource.Primary.Attributes["version"] _, httpRes, err := apiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(ctx, handle, version) if err != nil { - if httpRes.StatusCode == 400 { + if httpRes != nil && httpRes.StatusCode == 400 { return nil } return err diff --git a/docs/resources/compliance_custom_framework.md b/docs/resources/compliance_custom_framework.md index 2d59da274..916b6c049 100644 --- a/docs/resources/compliance_custom_framework.md +++ b/docs/resources/compliance_custom_framework.md @@ -18,7 +18,6 @@ resource "datadog_compliance_custom_framework" "example" { handle = "new-terraform-framework-handle" name = "new-terraform-framework" icon_url = "https://example.com/icon.png" - description = "This is a test I created this resource through terraform" requirements { name = "requirement1" controls { @@ -50,10 +49,9 @@ resource "datadog_compliance_custom_framework" "example" { - `version` (String) The framework version. String length must be at least 1. - `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) -### Optional +### Optional -- `description` (String) The description of the framework. -- `icon_url` (String) The URL of the icon representing the framework. +- `icon_url` (String) The URL of the icon representing the framework. ### Read-Only From 5a26389da9faade05f979ca05f3928e51b1fe4d9 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 14 May 2025 13:28:05 -0400 Subject: [PATCH 25/54] add comment to describe why requirements is a set --- .../fwprovider/resource_datadog_compliance_custom_framework.go | 1 + 1 file changed, 1 insertion(+) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 7c3b78a66..90b89f221 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -295,6 +295,7 @@ func (r *complianceCustomFrameworkResource) ImportState(ctx context.Context, req response.Diagnostics.Append(response.State.Set(ctx, &state)...) } +// using sets for requirements in state to be unordered func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []datadogV2.CustomFrameworkRequirement { frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements.Elements())) for i, requirement := range requirements.Elements() { From eec537f2a35793c372e2fc771165e9ff5f093014 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 14 May 2025 13:29:20 -0400 Subject: [PATCH 26/54] remove description from resource example --- .../resources/datadog_compliance_custom_framework/resource.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf index b7a3e2a38..c05d0da2c 100644 --- a/examples/resources/datadog_compliance_custom_framework/resource.tf +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -3,7 +3,6 @@ resource "datadog_compliance_custom_framework" "example" { handle = "new-terraform-framework-handle" name = "new-terraform-framework" icon_url = "https://example.com/icon.png" - description = "This is a test I created this resource through terraform" requirements { name = "requirement1" controls { From 6b9916ce30e34dd2f66065cada8150645508e021 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 14 May 2025 13:35:21 -0400 Subject: [PATCH 27/54] remove comments and extra cassettes --- .../duplicate_requirement_validator.go | 2 - .../TestCustomFramework_create.freeze | 1 - .../cassettes/TestCustomFramework_create.yaml | 105 --------- ...createAndUpdateMultipleRequirements.freeze | 1 - ...k_createAndUpdateMultipleRequirements.yaml | 138 ------------ ...ramework_createMultipleRequirements.freeze | 1 - ...mFramework_createMultipleRequirements.yaml | 138 ------------ .../TestCustomFramework_invalid.freeze | 1 - .../TestCustomFramework_invalid.yaml | 39 ---- ...tCustomFramework_sameConfigNoUpdate.freeze | 1 - ...estCustomFramework_sameConfigNoUpdate.yaml | 204 ------------------ 11 files changed, 631 deletions(-) delete mode 100644 datadog/tests/cassettes/TestCustomFramework_create.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_create.yaml delete mode 100644 datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml delete mode 100644 datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml delete mode 100644 datadog/tests/cassettes/TestCustomFramework_invalid.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_invalid.yaml delete mode 100644 datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_validator.go index 9f425f0ba..5a8a96685 100644 --- a/datadog/internal/validators/duplicate_requirement_validator.go +++ b/datadog/internal/validators/duplicate_requirement_validator.go @@ -24,7 +24,6 @@ func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator return } - // Get all requirement names from the configuration var requirementNames []string for _, requirement := range req.ConfigValue.Elements() { reqObj := requirement.(types.Object) @@ -35,7 +34,6 @@ func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator log.Printf("Found %d requirement names in config", len(requirementNames)) - // Check for duplicates in the list seen := make(map[string]bool) for _, name := range requirementNames { log.Printf("Checking requirement name: %s", name) diff --git a/datadog/tests/cassettes/TestCustomFramework_create.freeze b/datadog/tests/cassettes/TestCustomFramework_create.freeze deleted file mode 100644 index cea8167b9..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_create.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-05-07T13:00:16.869464-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_create.yaml b/datadog/tests/cassettes/TestCustomFramework_create.yaml deleted file mode 100644 index 25ab7ca10..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_create.yaml +++ /dev/null @@ -1,105 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 230 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 430.208042ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 187 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-framework-terraform","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 471.99225ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 97.036042ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze deleted file mode 100644 index 2dc953470..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-05-07T13:00:22.011299-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml deleted file mode 100644 index 73441cb0e..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_createAndUpdateMultipleRequirements.yaml +++ /dev/null @@ -1,138 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 363 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 382.595ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 421 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 284.132583ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 172 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-name","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 581.523ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 91.224583ms diff --git a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze deleted file mode 100644 index bd30eeaa7..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-04-25T16:26:15.932705-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml deleted file mode 100644 index 7de7a19ee..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_createMultipleRequirements.yaml +++ /dev/null @@ -1,138 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 424 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"handle-45366","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"version-82650"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 135 - uncompressed: false - body: '{"data":{"id":"handle-45366-version-82650","type":"custom_framework","attributes":{"handle":"handle-45366","version":"version-82650"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 495.194458ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-45366/version-82650 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 457 - uncompressed: false - body: '{"data":{"id":"handle-45366-version-82650","type":"custom_framework","attributes":{"description":"test description","handle":"handle-45366","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"version-82650"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 286.395417ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-45366/version-82650 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 208 - uncompressed: false - body: '{"data":{"id":"handle-45366-version-82650","type":"custom_framework","attributes":{"description":"test description","handle":"handle-45366","icon_url":"test url","name":"new-name","version":"version-82650"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 334.513792ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/handle-45366/version-82650 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 46.785959ms diff --git a/datadog/tests/cassettes/TestCustomFramework_invalid.freeze b/datadog/tests/cassettes/TestCustomFramework_invalid.freeze deleted file mode 100644 index ee4a6ba52..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_invalid.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-04-24T17:48:03.195086-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_invalid.yaml b/datadog/tests/cassettes/TestCustomFramework_invalid.yaml deleted file mode 100644 index f7454a2b1..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_invalid.yaml +++ /dev/null @@ -1,39 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 268 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"handle-88933","icon_url":"test url","name":"","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"version-93914"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 158 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request","detail":"input_validation_error(Field ''data.attributes.name'' is invalid: field ''name'' must not be empty)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 99.919834ms diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze deleted file mode 100644 index fbcb817b2..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-05-01T16:07:06.340903-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml deleted file mode 100644 index 589612fbf..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_sameConfigNoUpdate.yaml +++ /dev/null @@ -1,204 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 516 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 421.758333ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 543 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 456.435916ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 543 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 616.197209ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 543 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 469.077833ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 211 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 419.173334ms - - id: 5 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 96.980917ms From 246f5a4e6f43eb6ae3c3091e9759ba186297b31c Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 14 May 2025 13:38:48 -0400 Subject: [PATCH 28/54] fix description of icon url --- .../fwprovider/resource_datadog_compliance_custom_framework.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 90b89f221..4f63c6422 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -71,7 +71,7 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Required: true, }, "icon_url": schema.StringAttribute{ - Description: "The URL of the icon representing the framework. This can be set to empty if NA", + Description: "The URL of the icon representing the framework", Optional: true, }, }, From c8c33c381c7dee441d5ff3fb74df386310c84944 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 14 May 2025 13:46:29 -0400 Subject: [PATCH 29/54] fix format --- .../cassettes/TestCustomFramework_CreateConflict.freeze | 2 +- .../cassettes/TestCustomFramework_CreateConflict.yaml | 6 +++--- .../TestCustomFramework_CreateWithoutIconURL.freeze | 2 +- .../TestCustomFramework_CreateWithoutIconURL.yaml | 8 ++++---- .../datadog_compliance_custom_framework/resource.tf | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze index ef49b639b..f84f20d3d 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze @@ -1 +1 @@ -2025-05-14T13:18:54.151434-04:00 \ No newline at end of file +2025-05-14T13:42:26.827286-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml index b9d238aa4..235ba4fef 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml @@ -33,7 +33,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 54.499584ms + duration: 36.967958ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 418.422333ms + duration: 358.66775ms - id: 2 request: proto: HTTP/1.1 @@ -105,4 +105,4 @@ interactions: - application/vnd.api+json status: 409 Conflict code: 409 - duration: 174.318959ms + duration: 94.920583ms diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze index ee4bbd18e..612c8952b 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze @@ -1 +1 @@ -2025-05-14T13:21:36.261823-04:00 \ No newline at end of file +2025-05-14T13:42:18.478133-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml index 2f9987fe1..ea2317501 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 422.992708ms + duration: 817.065625ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 117.490292ms + duration: 298.588417ms - id: 2 request: proto: HTTP/1.1 @@ -102,7 +102,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 292.465917ms + duration: 647.031209ms - id: 3 request: proto: HTTP/1.1 @@ -135,4 +135,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 57.986458ms + duration: 61.763916ms diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf index c05d0da2c..47c037bdf 100644 --- a/examples/resources/datadog_compliance_custom_framework/resource.tf +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -1,8 +1,8 @@ resource "datadog_compliance_custom_framework" "example" { - version = "1" - handle = "new-terraform-framework-handle" - name = "new-terraform-framework" - icon_url = "https://example.com/icon.png" + version = "1" + handle = "new-terraform-framework-handle" + name = "new-terraform-framework" + icon_url = "https://example.com/icon.png" requirements { name = "requirement1" controls { From 4edfbc2fe3244e64e1db1fb8c2468008d77a78f2 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 15 May 2025 13:17:29 -0400 Subject: [PATCH 30/54] delete framework in conflict test --- .../TestCustomFramework_CreateBasic.freeze | 1 + .../TestCustomFramework_CreateBasic.yaml | 342 ++++++++++++++++++ ...atadog_compliance_custom_framework_test.go | 38 +- 3 files changed, 370 insertions(+), 11 deletions(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze new file mode 100644 index 000000000..f1b3d1f4f --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze @@ -0,0 +1 @@ +2025-05-15T13:09:47.144841-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml new file mode 100644 index 000000000..e3a020e28 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml @@ -0,0 +1,342 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 252 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 503.217417ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 179.772625ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 262.21975ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 385 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 934.496958ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 412 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 357.582459ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 412 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 277.229541ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 230 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 523.012417ms + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 257 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 150.182542ms + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 187 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 521.493ms + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 74.374542ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 3f6a5a2c6..01f7152b5 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -14,7 +14,7 @@ import ( "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" ) -func TestCustomFramework_Create(t *testing.T) { +func TestCustomFramework_CreateBasic(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -529,15 +529,15 @@ func TestCustomFramework_CreateConflict(t *testing.T) { api := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2() auth := providers.frameworkProvider.Auth - // Check if framework exists and delete it if it does - _, httpRes, err := api.GetCustomFramework(auth, handle, version) - if err == nil && httpRes.StatusCode == 200 { - // Framework exists, delete it - _, _, err = api.DeleteCustomFramework(auth, handle, version) - if err != nil { - return fmt.Errorf("failed to delete existing framework: %v", err) - } - } + // // Check if framework exists and delete it if it does + // _, httpRes, err := api.GetCustomFramework(auth, handle, version) + // if err == nil && httpRes.StatusCode == 200 { + // // Framework exists, delete it + // _, _, err = api.DeleteCustomFramework(auth, handle, version) + // if err != nil { + // return fmt.Errorf("failed to delete existing framework: %v", err) + // } + // } // Create a basic framework that matches the config we'll try to create createRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() @@ -561,7 +561,7 @@ func TestCustomFramework_CreateConflict(t *testing.T) { }) // Create the framework - _, _, err = api.CreateCustomFramework(auth, *createRequest) + _, _, err := api.CreateCustomFramework(auth, *createRequest) if err != nil { return fmt.Errorf("failed to create framework: %v", err) } @@ -574,6 +574,22 @@ func TestCustomFramework_CreateConflict(t *testing.T) { Config: testAccCheckDatadogCreateFramework(version, handle), ExpectError: regexp.MustCompile("409 Conflict"), }, + { + Config: "# Empty config for cleanup", + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + api := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2() + auth := providers.frameworkProvider.Auth + + // Deleting the framework previously created + _, _, err := api.DeleteCustomFramework(auth, handle, version) + if err != nil { + return fmt.Errorf("failed to delete framework during cleanup: %v", err) + } + return nil + }, + ), + }, }, }) } From 41767fac4181caad76e15604d319242c8a7e54d2 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 12:32:42 -0400 Subject: [PATCH 31/54] remove import resource and update when create conflicts --- ...rce_datadog_compliance_custom_framework.go | 38 +-- ...CreateAndUpdateMultipleRequirements.freeze | 1 + ...k_CreateAndUpdateMultipleRequirements.yaml | 240 ++++++++++++++++++ .../TestCustomFramework_CreateConflict.freeze | 1 - .../TestCustomFramework_CreateConflict.yaml | 108 -------- ...omFramework_UpdateIfFrameworkExists.freeze | 1 + ...stomFramework_UpdateIfFrameworkExists.yaml | 210 +++++++++++++++ ...atadog_compliance_custom_framework_test.go | 49 +--- 8 files changed, 477 insertions(+), 171 deletions(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml delete mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml create mode 100644 datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 4f63c6422..fa5918100 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -2,7 +2,6 @@ package fwprovider import ( "context" - "strings" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -137,11 +136,18 @@ func (r *complianceCustomFrameworkResource) Create(ctx context.Context, request return } - _, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) - + _, httpResp, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating compliance custom framework")) - return + if httpResp != nil && httpResp.StatusCode == 409 { // if framework already exists, try to update it with the new state + _, _, updateErr := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) + if updateErr != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(updateErr, "error updating existing compliance custom framework")) + return + } + } else { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating compliance custom framework")) + return + } } state.ID = types.StringValue(state.Handle.ValueString() + string('-') + state.Version.ValueString()) diags = response.State.Set(ctx, &state) @@ -273,28 +279,6 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str return state } -// ImportState is used to import a resource from an existing framework so we can update it if it exists in the database and not in terraform -func (r *complianceCustomFrameworkResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { - // Split the ID into handle and version - // The last hyphen separates handle and version - lastHyphenIndex := strings.LastIndex(request.ID, "-") - if lastHyphenIndex == -1 { - response.Diagnostics.AddError("Invalid import ID", "Import ID must contain a hyphen to separate handle and version") - return - } - handle := request.ID[:lastHyphenIndex] - version := request.ID[lastHyphenIndex+1:] - - data, _, err := r.Api.GetCustomFramework(r.Auth, handle, version) - if err != nil { - response.Diagnostics.AddError("Error importing resource", err.Error()) - return - } - - state := readStateFromDatabase(data, handle, version) - response.Diagnostics.Append(response.State.Set(ctx, &state)...) -} - // using sets for requirements in state to be unordered func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []datadogV2.CustomFrameworkRequirement { frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements.Elements())) diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze new file mode 100644 index 000000000..c611e7eb0 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze @@ -0,0 +1 @@ +2025-05-15T13:09:31.384764-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml new file mode 100644 index 000000000..33c0f9c40 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml @@ -0,0 +1,240 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 385 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 417.707542ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 412 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 358.391ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 412 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 300.353917ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 483 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"},{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 676.958959ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 510 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"requirement","controls":[{"name":"control","rules_id":["def-000-be9"]}]},{"name":"requirement-2","controls":[{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]},{"name":"control-2","rules_id":["def-000-be9"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 668.5285ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 180 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 441.340792ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 43.623958ms diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze deleted file mode 100644 index f84f20d3d..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-05-14T13:42:26.827286-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml deleted file mode 100644 index 235ba4fef..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_CreateConflict.yaml +++ /dev/null @@ -1,108 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 36.967958ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 252 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 358.66775ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 252 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 127 - uncompressed: false - body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 409 Conflict - code: 409 - duration: 94.920583ms diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze new file mode 100644 index 000000000..c1fccb62d --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze @@ -0,0 +1 @@ +2025-05-16T12:30:43.514665-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml new file mode 100644 index 000000000..c51299f3d --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml @@ -0,0 +1,210 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 250 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 370.3955ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 252 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 127 + uncompressed: false + body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 409 Conflict + code: 409 + duration: 80.751916ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 252 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 515.457375ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 209.929417ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 195 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 398.002875ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 73.471375ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 01f7152b5..1e2b2897c 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -508,7 +508,7 @@ func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) { }) } -func TestCustomFramework_CreateConflict(t *testing.T) { +func TestCustomFramework_UpdateIfFrameworkExists(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -521,25 +521,11 @@ func TestCustomFramework_CreateConflict(t *testing.T) { CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - // First create the framework using the API directly Config: "# Empty config since we're creating via API", Check: resource.ComposeTestCheckFunc( func(s *terraform.State) error { - // Create framework using API api := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2() auth := providers.frameworkProvider.Auth - - // // Check if framework exists and delete it if it does - // _, httpRes, err := api.GetCustomFramework(auth, handle, version) - // if err == nil && httpRes.StatusCode == 200 { - // // Framework exists, delete it - // _, _, err = api.DeleteCustomFramework(auth, handle, version) - // if err != nil { - // return fmt.Errorf("failed to delete existing framework: %v", err) - // } - // } - - // Create a basic framework that matches the config we'll try to create createRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() iconURL := "test url" createRequest.SetData(datadogV2.CustomFrameworkData{ @@ -552,15 +538,15 @@ func TestCustomFramework_CreateConflict(t *testing.T) { Requirements: []datadogV2.CustomFrameworkRequirement{ *datadogV2.NewCustomFrameworkRequirement( []datadogV2.CustomFrameworkControl{ - *datadogV2.NewCustomFrameworkControl("control1", []string{"def-000-be9"}), + *datadogV2.NewCustomFrameworkControl("control", []string{"def-000-be9"}), }, - "requirement1", + "requirement", ), }, }, }) - // Create the framework + // Create the framework using the API outside of Terraform _, _, err := api.CreateCustomFramework(auth, *createRequest) if err != nil { return fmt.Errorf("failed to create framework: %v", err) @@ -570,24 +556,17 @@ func TestCustomFramework_CreateConflict(t *testing.T) { ), }, { - // Try to create the same framework through Terraform - Config: testAccCheckDatadogCreateFramework(version, handle), - ExpectError: regexp.MustCompile("409 Conflict"), - }, - { - Config: "# Empty config for cleanup", + // creating the same framework through Terraform should update the existing framework + Config: testAccCheckDatadogCreateFramework(version, handle), Check: resource.ComposeTestCheckFunc( - func(s *terraform.State) error { - api := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2() - auth := providers.frameworkProvider.Auth - - // Deleting the framework previously created - _, _, err := api.DeleteCustomFramework(auth, handle, version) - if err != nil { - return fmt.Errorf("failed to delete framework during cleanup: %v", err) - } - return nil - }, + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "requirements.#", "1"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.#", "1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, }, From 5376a3c017f509fcff7208ed2edf9db95f7d65c3 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 12:38:25 -0400 Subject: [PATCH 32/54] use real rule ids in the example resource --- .../datadog_compliance_custom_framework/resource.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf index 47c037bdf..55f0e1d6b 100644 --- a/examples/resources/datadog_compliance_custom_framework/resource.tf +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -7,18 +7,18 @@ resource "datadog_compliance_custom_framework" "example" { name = "requirement1" controls { name = "control1" - rules_id = ["aaa-000-ccc", "bbb-000-ddd"] + rules_id = ["04w-clb-3io", "0eg-j5g-xip"] } controls { name = "control2" - rules_id = ["aaa-000-lll"] + rules_id = ["def-000-11s"] } } requirements { name = "requirement2" controls { name = "control3" - rules_id = ["aaa-000-zzz"] + rules_id = [""def-000-1im"] } } } \ No newline at end of file From 311ecab80c31f582f729546f31d5788bce708eda Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 12:41:41 -0400 Subject: [PATCH 33/54] remove logs --- .../internal/validators/duplicate_requirement_validator.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_validator.go index 5a8a96685..f20971241 100644 --- a/datadog/internal/validators/duplicate_requirement_validator.go +++ b/datadog/internal/validators/duplicate_requirement_validator.go @@ -3,7 +3,6 @@ package validators import ( "context" "fmt" - "log" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -28,17 +27,12 @@ func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator for _, requirement := range req.ConfigValue.Elements() { reqObj := requirement.(types.Object) name := reqObj.Attributes()["name"].(types.String).ValueString() - log.Printf("Found requirement name in config: %s", name) requirementNames = append(requirementNames, name) } - log.Printf("Found %d requirement names in config", len(requirementNames)) - seen := make(map[string]bool) for _, name := range requirementNames { - log.Printf("Checking requirement name: %s", name) if seen[name] { - log.Printf("Found duplicate requirement name: %s", name) resp.Diagnostics.AddError( "Each Requirement must have a unique name", fmt.Sprintf("Requirement name '%s' is used more than once.", name), From 35462c1863fdd0af5a4a1e8673a8b9a95795d9d5 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 13:16:12 -0400 Subject: [PATCH 34/54] test same state framework id --- ...TestCustomFramework_SameFrameworkID.freeze | 1 + .../TestCustomFramework_SameFrameworkID.yaml | 408 ++++++++++++++++++ ...atadog_compliance_custom_framework_test.go | 62 +++ .../resource.tf | 2 +- 4 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze new file mode 100644 index 000000000..4c5fcff9c --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze @@ -0,0 +1 @@ +2025-05-16T13:09:12.197143-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml new file mode 100644 index 000000000..bebeeeffe --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml @@ -0,0 +1,408 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 259 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"framework-1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 137 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 459.425834ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 293 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 245.849583ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 293 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 175.900125ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 259 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"terraform-framework-1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 137 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","version":"terraform-framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 425.030625ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 209 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 534.074291ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new/terraform-framework-1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 293 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"terraform-framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 203.0945ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new/terraform-framework-1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 293 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"terraform-framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 281.544583ms + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new/terraform-framework-1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 209 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new","icon_url":"test-url","name":"new-framework-terraform","version":"terraform-framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 455.115958ms + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 259 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"framework-1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 137 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 494.117958ms + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 293 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 284.34925ms + - id: 10 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 209 + uncompressed: false + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","version":"framework-1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 316.266375ms + - id: 11 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 57.598ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 1e2b2897c..bc5a414b9 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -177,6 +177,50 @@ func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) { }) } +// these handle and version combinations would result in the same state ID however +// terraform would still be able to tell the framework states apart since they have unique resource names (which is required by terraform) +func TestCustomFramework_SameFrameworkID(t *testing.T) { + handle := "new-terraform" + version := "framework-1.0" + + handle2 := "new" + version2 := "terraform-framework-1.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_compliance_custom_framework.sample_rules" + path2 := "datadog_compliance_custom_framework.sample_framework" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + }, + { + Config: testAccCheckDatadogCreateSecondFramework(version2, handle2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path2, "handle", handle2), + resource.TestCheckResourceAttr(path2, "version", version2), + ), + }, + { + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + ExpectNonEmptyPlan: false, + }, + }, + }) +} + func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -652,6 +696,24 @@ func testAccCheckDatadogCreateFramework(version string, handle string) string { `, version, handle) } +func testAccCheckDatadogCreateSecondFramework(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_framework" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + icon_url = "test-url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + func testAccCheckDatadogCreateFrameworkWithoutOptionalFields(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf index 55f0e1d6b..d53b49800 100644 --- a/examples/resources/datadog_compliance_custom_framework/resource.tf +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -18,7 +18,7 @@ resource "datadog_compliance_custom_framework" "example" { name = "requirement2" controls { name = "control3" - rules_id = [""def-000-1im"] + rules_id = ["def-000-1im"] } } } \ No newline at end of file From 2314db8353a28e0adb9f77682c9bf573ca6ae128 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 13:47:55 -0400 Subject: [PATCH 35/54] add better comments for delete after delete case --- .../resource_datadog_compliance_custom_framework.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index fa5918100..15df5c1f0 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -176,10 +176,12 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re return } - data, _, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) + data, httpResp, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) // If the framework does not exist, remove it from terraform state // This is to avoid the provider to return an error when the framework is deleted in the UI prior - if err != nil && err.Error() == "400 Bad Request" { + if err != nil && httpResp != nil && httpResp.StatusCode == 400 { + // 400 could only mean the framework does not exist + // because terraform would have already validated the framework in the create function response.State.RemoveResource(ctx) return } From 99334a5ac82ec5fb0ea3ee31679b173d39b97137 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 14:43:12 -0400 Subject: [PATCH 36/54] add cassetes for same config no update test --- ...tCustomFramework_SameConfigNoUpdate.freeze | 1 + ...estCustomFramework_SameConfigNoUpdate.yaml | 240 ++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze new file mode 100644 index 000000000..1fd72bb93 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze @@ -0,0 +1 @@ +2025-05-16T14:42:25.957319-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml new file mode 100644 index 000000000..1cde62fb1 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml @@ -0,0 +1,240 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 483 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 127 + uncompressed: false + body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 409 Conflict + code: 409 + duration: 102.871083ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 483 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: PUT + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 477.118167ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 510 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 292.398291ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 510 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 555.781208ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 510 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 639.509125ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 195 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 337.378541ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 57.430667ms From df4420310a64fb16fa22eb6275926bc9513dc640 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 16:06:03 -0400 Subject: [PATCH 37/54] move around error handling --- .../resource_datadog_compliance_custom_framework.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 15df5c1f0..6c0942b7c 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -177,6 +177,10 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re } data, httpResp, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) + if err != nil && httpResp != nil && httpResp.StatusCode != 400 { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) + return + } // If the framework does not exist, remove it from terraform state // This is to avoid the provider to return an error when the framework is deleted in the UI prior if err != nil && httpResp != nil && httpResp.StatusCode == 400 { @@ -185,10 +189,6 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re response.State.RemoveResource(ctx) return } - if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) - return - } databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString()) diags = response.State.Set(ctx, &databaseState) response.Diagnostics.Append(diags...) From fcc02b30d17a49a79d6ef7df61ed6a7138882eb7 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 16:09:44 -0400 Subject: [PATCH 38/54] Revert "move around error handling" This reverts commit 367c92c6d65db8a83a1b9716b853b7b78f7fe185. --- .../resource_datadog_compliance_custom_framework.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 6c0942b7c..15df5c1f0 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -177,10 +177,6 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re } data, httpResp, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) - if err != nil && httpResp != nil && httpResp.StatusCode != 400 { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) - return - } // If the framework does not exist, remove it from terraform state // This is to avoid the provider to return an error when the framework is deleted in the UI prior if err != nil && httpResp != nil && httpResp.StatusCode == 400 { @@ -189,6 +185,10 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re response.State.RemoveResource(ctx) return } + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) + return + } databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString()) diags = response.State.Set(ctx, &databaseState) response.Diagnostics.Append(diags...) From e0c42cdeab7516c6757da6b49285827ad08ba1dc Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 16:21:15 -0400 Subject: [PATCH 39/54] remove err check --- .../fwprovider/resource_datadog_compliance_custom_framework.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 15df5c1f0..31fa4ca8c 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -179,7 +179,7 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re data, httpResp, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) // If the framework does not exist, remove it from terraform state // This is to avoid the provider to return an error when the framework is deleted in the UI prior - if err != nil && httpResp != nil && httpResp.StatusCode == 400 { + if httpResp != nil && httpResp.StatusCode == 400 { // 400 could only mean the framework does not exist // because terraform would have already validated the framework in the create function response.State.RemoveResource(ctx) From bb27a3b25ccec523f0a9f6089f2bd740b77af983 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Fri, 16 May 2025 16:29:09 -0400 Subject: [PATCH 40/54] add invalidcreate cassettes --- .../cassettes/TestCustomFramework_InvalidCreate.freeze | 2 +- .../tests/cassettes/TestCustomFramework_InvalidCreate.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 7a0e699e4..3e1141aaa 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-05-07T09:53:00.898171-04:00 \ No newline at end of file +2025-05-16T16:28:32.463436-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index 1ad39c718..0d2728c5c 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 289 + content_length: 256 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"description":"test description","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["invalid-rule-id"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,4 +36,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 317.776875ms + duration: 357.055791ms From ec79449cc9507d4bcc0081692fde6c05dc60cb37 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Sun, 18 May 2025 14:44:28 -0400 Subject: [PATCH 41/54] use real rule ids --- .../datadog_compliance_custom_framework/resource.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf index d53b49800..d8e63fddc 100644 --- a/examples/resources/datadog_compliance_custom_framework/resource.tf +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -7,18 +7,18 @@ resource "datadog_compliance_custom_framework" "example" { name = "requirement1" controls { name = "control1" - rules_id = ["04w-clb-3io", "0eg-j5g-xip"] + rules_id = ["def-000-k6h", "def-000-u48"] } controls { name = "control2" - rules_id = ["def-000-11s"] + rules_id = ["def-000-k8u"] } } requirements { name = "requirement2" controls { name = "control3" - rules_id = ["def-000-1im"] + rules_id = ["def-000-k6h"] } } } \ No newline at end of file From b21e347d6ccdfad6be659cf00e5431450d16ea84 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 19 May 2025 09:55:56 -0400 Subject: [PATCH 42/54] RecreateAfterAPIDelete cassettes --- ...tomFramework_RecreateAfterAPIDelete.freeze | 2 +- ...ustomFramework_RecreateAfterAPIDelete.yaml | 310 +++++++++++++++++- 2 files changed, 306 insertions(+), 6 deletions(-) diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze index c7e31dfc7..ece3bc3e3 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze @@ -1 +1 @@ -2025-05-14T13:17:06.561175-04:00 \ No newline at end of file +2025-05-19T09:53:17.525477-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml index 1d1de7978..567749fef 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml @@ -28,12 +28,312 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 127 + content_length: 123 uncompressed: false - body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json - status: 409 Conflict - code: 409 - duration: 101.37575ms + status: 200 OK + code: 200 + duration: 425.451125ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 183.696666ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 192.715042ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 195 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 793.678167ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 58.679167ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 53.259333ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 385 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 369.107917ms + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 412 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 359.949584ms + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 180 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 408.013833ms + - id: 9 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 57.676375ms From 75cd14ebee7046449e4b2c517798261917aa615c Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 19 May 2025 10:28:55 -0400 Subject: [PATCH 43/54] add immutable fields edge case --- ...rce_datadog_compliance_custom_framework.go | 8 + ...Framework_RecreateOnImmutableFields.freeze | 1 + ...omFramework_RecreateOnImmutableFields.yaml | 306 ++++++++++++++++++ ...atadog_compliance_custom_framework_test.go | 42 ++- 4 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 31fa4ca8c..ce9dc7c5a 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -54,6 +56,9 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource stringvalidator.LengthAtLeast(1), }, Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "handle": schema.StringAttribute{ Description: "The framework handle.", @@ -61,6 +66,9 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource stringvalidator.LengthAtLeast(1), }, Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "name": schema.StringAttribute{ Description: "The framework name.", diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze new file mode 100644 index 000000000..2562c2967 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze @@ -0,0 +1 @@ +2025-05-19T10:06:22.926152-04:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml new file mode 100644 index 000000000..499a1fe16 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml @@ -0,0 +1,306 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 252 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 414.967541ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 165.35625ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 365.5285ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 195 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 538.317542ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 256 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle-new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"2.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 131 + uncompressed: false + body: '{"data":{"id":"terraform-handle-new-2.0","type":"custom_framework","attributes":{"handle":"terraform-handle-new","version":"2.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 424.232125ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 41.628875ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle-new/2.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 287 + uncompressed: false + body: '{"data":{"id":"terraform-handle-new-2.0","type":"custom_framework","attributes":{"handle":"terraform-handle-new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"2.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 163.765083ms + - id: 7 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle-new/2.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 203 + uncompressed: false + body: '{"data":{"id":"terraform-handle-new-2.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle-new","icon_url":"test-url","name":"new-framework-terraform","version":"2.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 384.015ms + - id: 8 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle-new/2.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 47.263041ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index bc5a414b9..ed0c16ca9 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -284,7 +284,7 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { name = "control1" rules_id = ["def-000-be9"] } - } + } requirements { name = "requirement2" controls { @@ -617,6 +617,46 @@ func TestCustomFramework_UpdateIfFrameworkExists(t *testing.T) { }) } +func TestCustomFramework_RecreateOnImmutableFields(t *testing.T) { + handle := "terraform-handle" + version := "1.0" + newHandle := "terraform-handle-new" + newVersion := "2.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_compliance_custom_framework.sample_rules" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + }, + { + Config: testAccCheckDatadogCreateFramework(newVersion, newHandle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", newHandle), + resource.TestCheckResourceAttr(path, "version", newVersion), + // Verify old resource is deleted + func(s *terraform.State) error { + _, httpResp, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(providers.frameworkProvider.Auth, handle, version) + if err == nil || (httpResp != nil && httpResp.StatusCode != 400) { + return fmt.Errorf("old framework with handle %s and version %s still exists", handle, version) + } + return nil + }, + ), + }, + }, + }) +} + func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { From 1549d3b9d352d06d40583c734eca4358a3790241 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 21 May 2025 13:39:18 -0400 Subject: [PATCH 44/54] change requirements and controls to lists' --- ...rce_datadog_compliance_custom_framework.go | 320 ++++++++++---- .../validators/duplicate_control_validator.go | 4 +- .../duplicate_requirement_validator.go | 4 +- ...atadog_compliance_custom_framework_test.go | 406 +++++++----------- 4 files changed, 406 insertions(+), 328 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index ce9dc7c5a..1f5de2f50 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -4,7 +4,6 @@ import ( "context" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -15,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" - "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/validators" ) var _ resource.Resource = &complianceCustomFrameworkResource{} @@ -26,12 +24,146 @@ type complianceCustomFrameworkResource struct { } type complianceCustomFrameworkModel struct { - ID types.String `tfsdk:"id"` - Version types.String `tfsdk:"version"` - Handle types.String `tfsdk:"handle"` - Name types.String `tfsdk:"name"` - IconURL types.String `tfsdk:"icon_url"` - Requirements types.Set `tfsdk:"requirements"` // have to define requirements as a set to be unordered + ID types.String `tfsdk:"id"` + Version types.String `tfsdk:"version"` + Handle types.String `tfsdk:"handle"` + Name types.String `tfsdk:"name"` + IconURL types.String `tfsdk:"icon_url"` + Requirements []complianceCustomFrameworkRequirementsModel `tfsdk:"requirements"` +} + +type complianceCustomFrameworkRequirementsModel struct { + Name types.String `tfsdk:"name"` + Controls []complianceCustomFrameworkControlsModel `tfsdk:"controls"` +} + +type complianceCustomFrameworkControlsModel struct { + Name types.String `tfsdk:"name"` + RulesID types.Set `tfsdk:"rules_id"` +} + +// Custom plan modifier to handle list ordering consistently +type listOrderPlanModifier struct{} + +func (m listOrderPlanModifier) Description(ctx context.Context) string { + return "Preserves the order of list elements as specified in the configuration" +} + +func (m listOrderPlanModifier) MarkdownDescription(ctx context.Context) string { + return "Preserves the order of list elements as specified in the configuration" +} + +func (m listOrderPlanModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + // If the plan is null, we don't need to do anything + if req.PlanValue.IsNull() { + return + } + + // If the state is null, we don't need to do anything + if req.StateValue.IsNull() { + return + } + + // Get the config value + configValue := req.ConfigValue + + // If the config is null, we don't need to do anything + if configValue.IsNull() { + return + } + + // Get the state value + stateValue := req.StateValue + + // Check if the elements are the same (ignoring order) + configElems := configValue.Elements() + stateElems := stateValue.Elements() + + // If lengths are different, there's a real change + if len(configElems) != len(stateElems) { + resp.PlanValue = configValue + return + } + + // Create maps to track elements by their name + configMap := make(map[string]attr.Value) + stateMap := make(map[string]attr.Value) + + // Extract names from config elements + for _, elem := range configElems { + if obj, ok := elem.(types.Object); ok { + if name, ok := obj.Attributes()["name"]; ok { + if strName, ok := name.(types.String); ok { + configMap[strName.ValueString()] = elem + } + } + } + } + + // Extract names from state elements + for _, elem := range stateElems { + if obj, ok := elem.(types.Object); ok { + if name, ok := obj.Attributes()["name"]; ok { + if strName, ok := name.(types.String); ok { + stateMap[strName.ValueString()] = elem + } + } + } + } + + // Check if all elements exist in both maps + hasChanges := false + for name, configElem := range configMap { + stateElem, exists := stateMap[name] + if !exists { + hasChanges = true + break + } + // Compare the elements (excluding order of nested lists) + if !compareElements(configElem, stateElem) { + hasChanges = true + break + } + } + + // If there are real changes, use the config value + // Otherwise, use the state value to preserve existing order + if hasChanges { + resp.PlanValue = configValue + } else { + resp.PlanValue = stateValue + } +} + +// Helper function to compare elements while ignoring order of nested lists +func compareElements(config, state attr.Value) bool { + configObj, ok1 := config.(types.Object) + stateObj, ok2 := state.(types.Object) + if !ok1 || !ok2 { + return config.Equal(state) + } + + configAttrs := configObj.Attributes() + stateAttrs := stateObj.Attributes() + + // Compare all attributes except nested lists + for name, configAttr := range configAttrs { + stateAttr, exists := stateAttrs[name] + if !exists { + return false + } + + // Skip comparison of nested lists (they're handled by their own plan modifier) + if _, ok := configAttr.(types.List); ok { + continue + } + + if !configAttr.Equal(stateAttr) { + return false + } + } + + return true } func NewComplianceCustomFrameworkResource() resource.Resource { @@ -83,11 +215,10 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource }, }, Blocks: map[string]schema.Block{ - "requirements": schema.SetNestedBlock{ + "requirements": schema.ListNestedBlock{ Description: "The requirements of the framework.", - Validators: []validator.Set{ - setvalidator.IsRequired(), - validators.RequirementNameValidator(), + PlanModifiers: []planmodifier.List{ + listOrderPlanModifier{}, }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ @@ -100,11 +231,10 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource }, }, Blocks: map[string]schema.Block{ - "controls": schema.SetNestedBlock{ + "controls": schema.ListNestedBlock{ Description: "The controls of the requirement.", - Validators: []validator.Set{ - setvalidator.IsRequired(), - validators.ControlNameValidator(), + PlanModifiers: []planmodifier.List{ + listOrderPlanModifier{}, }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ @@ -116,7 +246,7 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource }, }, "rules_id": schema.SetAttribute{ - Description: "The list of rules IDs for the control.", + Description: "The set of rules IDs for the control.", ElementType: types.StringType, Required: true, }, @@ -138,6 +268,7 @@ func (r *complianceCustomFrameworkResource) Configure(_ context.Context, request func (r *complianceCustomFrameworkResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { var state complianceCustomFrameworkModel + diags := request.Config.Get(ctx, &state) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -197,7 +328,7 @@ func (r *complianceCustomFrameworkResource) Read(ctx context.Context, request re response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework")) return } - databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString()) + databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString(), &state) diags = response.State.Set(ctx, &databaseState) response.Diagnostics.Append(diags...) } @@ -219,51 +350,18 @@ func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request response.Diagnostics.Append(diags...) } -func setControl(name string, rulesID []attr.Value) types.Object { - return types.ObjectValueMust( - map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }, - map[string]attr.Value{ - "name": types.StringValue(name), - "rules_id": types.SetValueMust(types.StringType, rulesID), - }, - ) -} +func (r *complianceCustomFrameworkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + // If the plan is null, we don't need to do anything + if req.Plan.Raw.IsNull() { + return + } -func setRequirement(name string, controls []attr.Value) types.Object { - return types.ObjectValueMust( - map[string]attr.Type{ - "name": types.StringType, - "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }}}, - }, - map[string]attr.Value{ - "name": types.StringValue(name), - "controls": types.SetValueMust(types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }}, controls), - }, - ) + // Let the plan modifiers handle the ordering + // They will be called automatically for the requirements and controls lists + return } -func setRequirements(requirements []attr.Value) types.Set { - return types.SetValueMust( - types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ - "name": types.StringType, - "rules_id": types.SetType{ElemType: types.StringType}, - }}}, - }}, - requirements, - ) -} -func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string) complianceCustomFrameworkModel { +func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string, currentState *complianceCustomFrameworkModel) complianceCustomFrameworkModel { var state complianceCustomFrameworkModel state.ID = types.StringValue(handle + "-" + version) state.Handle = types.StringValue(handle) @@ -273,37 +371,93 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl) } - requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements)) - for i, requirement := range data.GetData().Attributes.Requirements { - controls := make([]attr.Value, len(requirement.Controls)) - for j, control := range requirement.Controls { - rulesID := make([]attr.Value, len(control.RulesId)) - for k, ruleID := range control.RulesId { - rulesID[k] = types.StringValue(ruleID) + // Create maps to track requirements and controls by name + reqMap := make(map[string]datadogV2.CustomFrameworkRequirement) + ctrlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) + + // Build maps of requirements and controls from API response + for _, req := range data.GetData().Attributes.Requirements { + reqMap[req.GetName()] = req + ctrlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl) + for _, ctrl := range req.GetControls() { + ctrlMap[req.GetName()][ctrl.GetName()] = ctrl + } + } + + // Check if API response has same elements as current state + if currentState != nil { + // Check if all requirements and controls match + hasChanges := false + stateReqMap := make(map[string]bool) + stateCtrlMap := make(map[string]map[string]bool) + + // Build maps of current state requirements and controls + for _, req := range currentState.Requirements { + reqName := req.Name.ValueString() + stateReqMap[reqName] = true + stateCtrlMap[reqName] = make(map[string]bool) + for _, ctrl := range req.Controls { + stateCtrlMap[reqName][ctrl.Name.ValueString()] = true + } + } + + // Check if API response matches current state + for reqName := range reqMap { + if !stateReqMap[reqName] { + hasChanges = true + break + } + for ctrlName := range ctrlMap[reqName] { + if !stateCtrlMap[reqName][ctrlName] { + hasChanges = true + break + } + } + } + + // If no changes, use current state + if !hasChanges { + state.Requirements = currentState.Requirements + return state + } + } + + // If there are changes or no current state, use API order + state.Requirements = make([]complianceCustomFrameworkRequirementsModel, len(data.GetData().Attributes.Requirements)) + for i, req := range data.GetData().Attributes.Requirements { + state.Requirements[i] = complianceCustomFrameworkRequirementsModel{ + Name: types.StringValue(req.GetName()), + Controls: make([]complianceCustomFrameworkControlsModel, len(req.GetControls())), + } + + for j, ctrl := range req.GetControls() { + rulesID := make([]attr.Value, len(ctrl.GetRulesId())) + for k, v := range ctrl.GetRulesId() { + rulesID[k] = types.StringValue(v) + } + + state.Requirements[i].Controls[j] = complianceCustomFrameworkControlsModel{ + Name: types.StringValue(ctrl.GetName()), + RulesID: types.SetValueMust(types.StringType, rulesID), } - controls[j] = setControl(control.Name, rulesID) } - requirements[i] = setRequirement(requirement.Name, controls) } - state.Requirements = setRequirements(requirements) + return state } -// using sets for requirements in state to be unordered -func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []datadogV2.CustomFrameworkRequirement { - frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements.Elements())) - for i, requirement := range requirements.Elements() { - requirementState := requirement.(types.Object) - controls := make([]datadogV2.CustomFrameworkControl, len(requirementState.Attributes()["controls"].(types.Set).Elements())) - for j, control := range requirementState.Attributes()["controls"].(types.Set).Elements() { - controlState := control.(types.Object) - rulesID := make([]string, len(controlState.Attributes()["rules_id"].(types.Set).Elements())) - for k, ruleID := range controlState.Attributes()["rules_id"].(types.Set).Elements() { - rulesID[k] = ruleID.(types.String).ValueString() +func convertStateRequirementsToFrameworkRequirements(requirements []complianceCustomFrameworkRequirementsModel) []datadogV2.CustomFrameworkRequirement { + frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements)) + for i, requirement := range requirements { + controls := make([]datadogV2.CustomFrameworkControl, len(requirement.Controls)) + for j, control := range requirement.Controls { + rulesID := make([]string, 0) + for _, v := range control.RulesID.Elements() { + rulesID = append(rulesID, v.(types.String).ValueString()) } - controls[j] = *datadogV2.NewCustomFrameworkControl(controlState.Attributes()["name"].(types.String).ValueString(), rulesID) + controls[j] = *datadogV2.NewCustomFrameworkControl(control.Name.ValueString(), rulesID) } - frameworkRequirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, requirementState.Attributes()["name"].(types.String).ValueString()) + frameworkRequirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, requirement.Name.ValueString()) } return frameworkRequirements } diff --git a/datadog/internal/validators/duplicate_control_validator.go b/datadog/internal/validators/duplicate_control_validator.go index 31a20b689..04a998971 100644 --- a/datadog/internal/validators/duplicate_control_validator.go +++ b/datadog/internal/validators/duplicate_control_validator.go @@ -18,7 +18,7 @@ func (v controlNameValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -func (v controlNameValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { +func (v controlNameValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } @@ -43,6 +43,6 @@ func (v controlNameValidator) ValidateSet(ctx context.Context, req validator.Set } } -func ControlNameValidator() validator.Set { +func ControlNameValidator() validator.List { return &controlNameValidator{} } diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_validator.go index f20971241..9aa259a12 100644 --- a/datadog/internal/validators/duplicate_requirement_validator.go +++ b/datadog/internal/validators/duplicate_requirement_validator.go @@ -18,7 +18,7 @@ func (v requirementNameValidator) MarkdownDescription(ctx context.Context) strin return v.Description(ctx) } -func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { +func (v requirementNameValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } @@ -43,6 +43,6 @@ func (v requirementNameValidator) ValidateSet(ctx context.Context, req validator } } -func RequirementNameValidator() validator.Set { +func RequirementNameValidator() validator.List { return &requirementNameValidator{} } diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index ed0c16ca9..05c3a63a6 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -6,14 +6,58 @@ import ( "regexp" "testing" + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" ) +type complianceCustomFrameworkModel struct { + Handle types.String + Version types.String + Name types.String + IconURL types.String + Requirements []complianceCustomFrameworkRequirementsModel +} + +type complianceCustomFrameworkRequirementsModel struct { + Name types.String + Controls []complianceCustomFrameworkControlsModel +} + +type complianceCustomFrameworkControlsModel struct { + Name types.String + RulesID []string +} + +func buildCreateFrameworkRequest(model complianceCustomFrameworkModel) *datadogV2.CreateCustomFrameworkRequest { + req := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() + iconURL := model.IconURL.ValueString() + req.SetData(datadogV2.CustomFrameworkData{ + Type: "custom_framework", + Attributes: datadogV2.CustomFrameworkDataAttributes{ + Handle: model.Handle.ValueString(), + Name: model.Name.ValueString(), + IconUrl: &iconURL, + Version: model.Version.ValueString(), + Requirements: func() []datadogV2.CustomFrameworkRequirement { + requirements := make([]datadogV2.CustomFrameworkRequirement, len(model.Requirements)) + for i, req := range model.Requirements { + controls := make([]datadogV2.CustomFrameworkControl, len(req.Controls)) + for j, ctrl := range req.Controls { + controls[j] = *datadogV2.NewCustomFrameworkControl(ctrl.Name.ValueString(), ctrl.RulesID) + } + requirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, req.Name.ValueString()) + } + return requirements + }(), + }, + }) + return req +} + func TestCustomFramework_CreateBasic(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -32,13 +76,9 @@ func TestCustomFramework_CreateBasic(t *testing.T) { resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), resource.TestCheckResourceAttr(path, "icon_url", "test-url"), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control1", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, { @@ -46,20 +86,12 @@ func TestCustomFramework_CreateBasic(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "compliance-requirement", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "security-requirement", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "compliance-control", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "security-control", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "compliance-requirement"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "security-requirement"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "compliance-control"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "security-control"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), ), }, { @@ -68,13 +100,9 @@ func TestCustomFramework_CreateBasic(t *testing.T) { resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control1", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, }, @@ -98,13 +126,9 @@ func TestCustomFramework_CreateWithoutIconURL(t *testing.T) { resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control1", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, }, @@ -127,20 +151,12 @@ func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "compliance-requirement", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "security-requirement", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "compliance-control", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "security-control", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "compliance-requirement"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "security-requirement"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "compliance-control"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "security-control"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), ), }, { @@ -148,29 +164,15 @@ func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "security-requirement", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement-2", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control-2", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control-3", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement-2"), + resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control"), + resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control-2"), + resource.TestCheckResourceAttr(path, "requirements.2.controls.1.name", "control-3"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), ), }, }, @@ -311,26 +313,14 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), resource.TestCheckResourceAttr(path, "icon_url", "test url"), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement2", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement3", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control2", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control3", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement2"), + resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement3"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control2"), + resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control3"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), ), }, { @@ -340,26 +330,14 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), resource.TestCheckResourceAttr(path, "icon_url", "test url"), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement2", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*", map[string]string{ - "name": "requirement3", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control1", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control2", - }), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control3", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement2"), + resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement3"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control2"), + resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control3"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), ), // This step should not trigger an update since only the order is different ExpectNonEmptyPlan: false, @@ -383,14 +361,12 @@ func TestCustomFramework_DuplicateRuleIds(t *testing.T) { CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - Config: testAccCheckDatadogDuplicateRulesId(version, handle), + Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckTypeSetElemNestedAttrs(path, "requirements.*.controls.*", map[string]string{ - "name": "control1", - }), - resource.TestCheckTypeSetElemAttr(path, "requirements.*.controls.*.rules_id.*", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.1.rules_id.0", "def-000-be9"), resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), ), }, @@ -409,45 +385,33 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { ProtoV5ProviderFactories: accProviders, CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ - { - Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle), - ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), - }, - { - Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle), - ExpectError: regexp.MustCompile("400 Bad Request"), - }, - { - Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle), - ExpectError: regexp.MustCompile("Invalid Block"), - }, { Config: testAccCheckDatadogCreateFrameworkWithNoControls(version, handle), - ExpectError: regexp.MustCompile("Invalid Block"), + ExpectError: regexp.MustCompile("controls is required"), }, { - Config: testAccCheckDatadogCreateEmptyHandle(version), - ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), + Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle), + ExpectError: regexp.MustCompile("requirements is required"), }, { - Config: testAccCheckDatadogCreateEmptyVersion(handle), - ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), + Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle), + ExpectError: regexp.MustCompile("name is required"), }, { - Config: testAccCheckDatadogDuplicateRequirements(version, handle), - ExpectError: regexp.MustCompile(".*Each Requirement must have a unique name.*"), + Config: testAccCheckDatadogEmptyRequirementName(version, handle), + ExpectError: regexp.MustCompile("name is required"), }, { - Config: testAccCheckDatadogDuplicateControls(version, handle), - ExpectError: regexp.MustCompile(".*Each Control must have a unique name under the same requirement.*"), + Config: testAccCheckDatadogEmptyControlName(version, handle), + ExpectError: regexp.MustCompile("name is required"), }, { - Config: testAccCheckDatadogEmptyRequirementName(version, handle), - ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), + Config: testAccCheckDatadogCreateEmptyHandle(version), + ExpectError: regexp.MustCompile("handle is required"), }, { - Config: testAccCheckDatadogEmptyControlName(version, handle), - ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), + Config: testAccCheckDatadogCreateEmptyVersion(handle), + ExpectError: regexp.MustCompile("version is required"), }, }, }) @@ -459,45 +423,37 @@ func TestCustomFramework_RecreateAfterAPIDelete(t *testing.T) { ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_compliance_custom_framework.sample_rules" - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - // First create the framework Config: testAccCheckDatadogCreateFramework(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, { - // Simulate framework being deleted in UI + PreConfig: func() { + // Delete the framework directly via API + _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version) + if err != nil { + t.Fatalf("Failed to delete framework: %v", err) + } + }, Config: testAccCheckDatadogCreateFramework(version, handle), - Check: resource.ComposeTestCheckFunc( - // Delete the framework in the UI - func(s *terraform.State) error { - _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version) - return err - }, - ), - // Expect a non-empty plan since the framework was deleted - ExpectNonEmptyPlan: true, - }, - { - // Update the framework - - // The read would be able to tell that the framework was deleted in UI so then it delete the local terraform state of the framework - // this should trigger a create since it was deleted in UI - Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - // Verify the framework was recreated with the new requirements - resource.TestCheckResourceAttr(path, "requirements.#", "2"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), - ExpectNonEmptyPlan: false, }, }, }) @@ -509,44 +465,37 @@ func TestCustomFramework_DeleteAfterAPIDelete(t *testing.T) { ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_compliance_custom_framework.sample_rules" - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - // First create the framework in terraform Config: testAccCheckDatadogCreateFramework(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, { - // Simulate framework being deleted in UI + PreConfig: func() { + // Delete the framework directly via API + _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version) + if err != nil { + t.Fatalf("Failed to delete framework: %v", err) + } + }, Config: testAccCheckDatadogCreateFramework(version, handle), Check: resource.ComposeTestCheckFunc( - // Delete the framework in the UI - func(s *terraform.State) error { - _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().DeleteCustomFramework(providers.frameworkProvider.Auth, handle, version) - return err - }, - ), - // Expect a non-empty plan since the framework was deleted - ExpectNonEmptyPlan: true, - }, - { - // Try to remove the resource from terraform - // Since the framework was deleted in UI, terraform should just remove it from state - // The read in terraform will return a 400 error because the framework handle and version no longer exist - // This will delete the framework from terraform - Config: "# Empty config to simulate removing the resource", - Check: resource.ComposeTestCheckFunc( - // No checks needed since we're removing the resource + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), - // Expect no changes needed since the resource was already deleted - ExpectNonEmptyPlan: false, }, }, }) @@ -558,58 +507,40 @@ func TestCustomFramework_UpdateIfFrameworkExists(t *testing.T) { ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_compliance_custom_framework.sample_rules" - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - Config: "# Empty config since we're creating via API", - Check: resource.ComposeTestCheckFunc( - func(s *terraform.State) error { - api := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2() - auth := providers.frameworkProvider.Auth - createRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults() - iconURL := "test url" - createRequest.SetData(datadogV2.CustomFrameworkData{ - Type: "custom_framework", - Attributes: datadogV2.CustomFrameworkDataAttributes{ - Handle: handle, - Name: "new-framework-terraform", - IconUrl: &iconURL, - Version: version, - Requirements: []datadogV2.CustomFrameworkRequirement{ - *datadogV2.NewCustomFrameworkRequirement( - []datadogV2.CustomFrameworkControl{ - *datadogV2.NewCustomFrameworkControl("control", []string{"def-000-be9"}), - }, - "requirement", - ), + PreConfig: func() { + // Create the framework directly via API + _, _, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().CreateCustomFramework(providers.frameworkProvider.Auth, *buildCreateFrameworkRequest(complianceCustomFrameworkModel{ + Handle: types.StringValue(handle), + Version: types.StringValue(version), + Name: types.StringValue("existing-framework"), + Requirements: []complianceCustomFrameworkRequirementsModel{ + { + Name: types.StringValue("existing-requirement"), + Controls: []complianceCustomFrameworkControlsModel{ + { + Name: types.StringValue("existing-control"), + RulesID: []string{"def-000-be9"}, + }, }, }, - }) - - // Create the framework using the API outside of Terraform - _, _, err := api.CreateCustomFramework(auth, *createRequest) - if err != nil { - return fmt.Errorf("failed to create framework: %v", err) - } - return nil - }, - ), - }, - { - // creating the same framework through Terraform should update the existing framework + }, + })) + if err != nil { + t.Fatalf("Failed to create framework: %v", err) + } + }, Config: testAccCheckDatadogCreateFramework(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "requirements.#", "1"), resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.#", "1"), resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, @@ -620,12 +551,9 @@ func TestCustomFramework_UpdateIfFrameworkExists(t *testing.T) { func TestCustomFramework_RecreateOnImmutableFields(t *testing.T) { handle := "terraform-handle" version := "1.0" - newHandle := "terraform-handle-new" - newVersion := "2.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_compliance_custom_framework.sample_rules" - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, @@ -636,22 +564,18 @@ func TestCustomFramework_RecreateOnImmutableFields(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, { - Config: testAccCheckDatadogCreateFramework(newVersion, newHandle), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(path, "handle", newHandle), - resource.TestCheckResourceAttr(path, "version", newVersion), - // Verify old resource is deleted - func(s *terraform.State) error { - _, httpResp, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(providers.frameworkProvider.Auth, handle, version) - if err == nil || (httpResp != nil && httpResp.StatusCode != 400) { - return fmt.Errorf("old framework with handle %s and version %s still exists", handle, version) - } - return nil - }, - ), + Config: testAccCheckDatadogCreateFramework("2.0", handle), + ExpectError: regexp.MustCompile("version cannot be changed"), + }, + { + Config: testAccCheckDatadogCreateFramework(version, "new-handle"), + ExpectError: regexp.MustCompile("handle cannot be changed"), }, }, }) From 6443f007e2e3e171c784e578a122ba12e7b49d5b Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 21 May 2025 13:53:57 -0400 Subject: [PATCH 45/54] fix modify plan --- ...rce_datadog_compliance_custom_framework.go | 226 +++++++----------- 1 file changed, 84 insertions(+), 142 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 1f5de2f50..80f99e6c6 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -42,130 +42,6 @@ type complianceCustomFrameworkControlsModel struct { RulesID types.Set `tfsdk:"rules_id"` } -// Custom plan modifier to handle list ordering consistently -type listOrderPlanModifier struct{} - -func (m listOrderPlanModifier) Description(ctx context.Context) string { - return "Preserves the order of list elements as specified in the configuration" -} - -func (m listOrderPlanModifier) MarkdownDescription(ctx context.Context) string { - return "Preserves the order of list elements as specified in the configuration" -} - -func (m listOrderPlanModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { - // If the plan is null, we don't need to do anything - if req.PlanValue.IsNull() { - return - } - - // If the state is null, we don't need to do anything - if req.StateValue.IsNull() { - return - } - - // Get the config value - configValue := req.ConfigValue - - // If the config is null, we don't need to do anything - if configValue.IsNull() { - return - } - - // Get the state value - stateValue := req.StateValue - - // Check if the elements are the same (ignoring order) - configElems := configValue.Elements() - stateElems := stateValue.Elements() - - // If lengths are different, there's a real change - if len(configElems) != len(stateElems) { - resp.PlanValue = configValue - return - } - - // Create maps to track elements by their name - configMap := make(map[string]attr.Value) - stateMap := make(map[string]attr.Value) - - // Extract names from config elements - for _, elem := range configElems { - if obj, ok := elem.(types.Object); ok { - if name, ok := obj.Attributes()["name"]; ok { - if strName, ok := name.(types.String); ok { - configMap[strName.ValueString()] = elem - } - } - } - } - - // Extract names from state elements - for _, elem := range stateElems { - if obj, ok := elem.(types.Object); ok { - if name, ok := obj.Attributes()["name"]; ok { - if strName, ok := name.(types.String); ok { - stateMap[strName.ValueString()] = elem - } - } - } - } - - // Check if all elements exist in both maps - hasChanges := false - for name, configElem := range configMap { - stateElem, exists := stateMap[name] - if !exists { - hasChanges = true - break - } - // Compare the elements (excluding order of nested lists) - if !compareElements(configElem, stateElem) { - hasChanges = true - break - } - } - - // If there are real changes, use the config value - // Otherwise, use the state value to preserve existing order - if hasChanges { - resp.PlanValue = configValue - } else { - resp.PlanValue = stateValue - } -} - -// Helper function to compare elements while ignoring order of nested lists -func compareElements(config, state attr.Value) bool { - configObj, ok1 := config.(types.Object) - stateObj, ok2 := state.(types.Object) - if !ok1 || !ok2 { - return config.Equal(state) - } - - configAttrs := configObj.Attributes() - stateAttrs := stateObj.Attributes() - - // Compare all attributes except nested lists - for name, configAttr := range configAttrs { - stateAttr, exists := stateAttrs[name] - if !exists { - return false - } - - // Skip comparison of nested lists (they're handled by their own plan modifier) - if _, ok := configAttr.(types.List); ok { - continue - } - - if !configAttr.Equal(stateAttr) { - return false - } - } - - return true -} - func NewComplianceCustomFrameworkResource() resource.Resource { return &complianceCustomFrameworkResource{} } @@ -217,9 +93,6 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Blocks: map[string]schema.Block{ "requirements": schema.ListNestedBlock{ Description: "The requirements of the framework.", - PlanModifiers: []planmodifier.List{ - listOrderPlanModifier{}, - }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -233,9 +106,6 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Blocks: map[string]schema.Block{ "controls": schema.ListNestedBlock{ Description: "The controls of the requirement.", - PlanModifiers: []planmodifier.List{ - listOrderPlanModifier{}, - }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -351,14 +221,94 @@ func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request } func (r *complianceCustomFrameworkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - // If the plan is null, we don't need to do anything if req.Plan.Raw.IsNull() { return } - // Let the plan modifiers handle the ordering - // They will be called automatically for the requirements and controls lists - return + if req.State.Raw.IsNull() { + return + } + + var plan, state complianceCustomFrameworkModel + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if len(plan.Requirements) != len(state.Requirements) { + return + } + + planReqMap := make(map[string]complianceCustomFrameworkRequirementsModel) + stateReqMap := make(map[string]complianceCustomFrameworkRequirementsModel) + + for _, req := range plan.Requirements { + planReqMap[req.Name.ValueString()] = req + } + for _, req := range state.Requirements { + stateReqMap[req.Name.ValueString()] = req + } + + hasChanges := false + for name, planReq := range planReqMap { + stateReq, exists := stateReqMap[name] + if !exists { + hasChanges = true + break + } + + if len(planReq.Controls) != len(stateReq.Controls) { + hasChanges = true + break + } + + planCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) + stateCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) + + for _, ctrl := range planReq.Controls { + planCtrlMap[ctrl.Name.ValueString()] = ctrl + } + for _, ctrl := range stateReq.Controls { + stateCtrlMap[ctrl.Name.ValueString()] = ctrl + } + + for ctrlName, planCtrl := range planCtrlMap { + stateCtrl, exists := stateCtrlMap[ctrlName] + if !exists { + hasChanges = true + break + } + + if !planCtrl.RulesID.Equal(stateCtrl.RulesID) { + hasChanges = true + break + } + } + if hasChanges { + break + } + } + + if !hasChanges { + newPlan := complianceCustomFrameworkModel{ + ID: state.ID, + Version: plan.Version, + Handle: plan.Handle, + Name: plan.Name, + IconURL: plan.IconURL, + Requirements: state.Requirements, + } + diags = resp.Plan.Set(ctx, &newPlan) + resp.Diagnostics.Append(diags...) + } } func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string, currentState *complianceCustomFrameworkModel) complianceCustomFrameworkModel { @@ -371,11 +321,9 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl) } - // Create maps to track requirements and controls by name reqMap := make(map[string]datadogV2.CustomFrameworkRequirement) ctrlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) - // Build maps of requirements and controls from API response for _, req := range data.GetData().Attributes.Requirements { reqMap[req.GetName()] = req ctrlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl) @@ -384,14 +332,11 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str } } - // Check if API response has same elements as current state if currentState != nil { - // Check if all requirements and controls match hasChanges := false stateReqMap := make(map[string]bool) stateCtrlMap := make(map[string]map[string]bool) - // Build maps of current state requirements and controls for _, req := range currentState.Requirements { reqName := req.Name.ValueString() stateReqMap[reqName] = true @@ -401,7 +346,6 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str } } - // Check if API response matches current state for reqName := range reqMap { if !stateReqMap[reqName] { hasChanges = true @@ -415,14 +359,12 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str } } - // If no changes, use current state if !hasChanges { state.Requirements = currentState.Requirements return state } } - // If there are changes or no current state, use API order state.Requirements = make([]complianceCustomFrameworkRequirementsModel, len(data.GetData().Attributes.Requirements)) for i, req := range data.GetData().Attributes.Requirements { state.Requirements[i] = complianceCustomFrameworkRequirementsModel{ From b36f3348a4ea647ed3a87edb6b498ae87e18acb2 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Wed, 21 May 2025 14:08:37 -0400 Subject: [PATCH 46/54] fix the apply issue --- ...rce_datadog_compliance_custom_framework.go | 188 ++++++++++-------- 1 file changed, 100 insertions(+), 88 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 80f99e6c6..5a154c581 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -211,6 +211,8 @@ func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request return } + state.ID = types.StringValue(state.Handle.ValueString() + "-" + state.Version.ValueString()) + _, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating compliance custom framework")) @@ -243,10 +245,6 @@ func (r *complianceCustomFrameworkResource) ModifyPlan(ctx context.Context, req return } - if len(plan.Requirements) != len(state.Requirements) { - return - } - planReqMap := make(map[string]complianceCustomFrameworkRequirementsModel) stateReqMap := make(map[string]complianceCustomFrameworkRequirementsModel) @@ -257,58 +255,58 @@ func (r *complianceCustomFrameworkResource) ModifyPlan(ctx context.Context, req stateReqMap[req.Name.ValueString()] = req } - hasChanges := false - for name, planReq := range planReqMap { - stateReq, exists := stateReqMap[name] - if !exists { - hasChanges = true - break - } + sortedRequirements := make([]complianceCustomFrameworkRequirementsModel, 0, len(plan.Requirements)) - if len(planReq.Controls) != len(stateReq.Controls) { - hasChanges = true - break - } + for _, stateReq := range state.Requirements { + stateReqName := stateReq.Name.ValueString() + if planReq, exists := planReqMap[stateReqName]; exists { + planCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) + stateCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) - planCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) - stateCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) + for _, ctrl := range planReq.Controls { + planCtrlMap[ctrl.Name.ValueString()] = ctrl + } + for _, ctrl := range stateReq.Controls { + stateCtrlMap[ctrl.Name.ValueString()] = ctrl + } - for _, ctrl := range planReq.Controls { - planCtrlMap[ctrl.Name.ValueString()] = ctrl - } - for _, ctrl := range stateReq.Controls { - stateCtrlMap[ctrl.Name.ValueString()] = ctrl - } + sortedControls := make([]complianceCustomFrameworkControlsModel, 0, len(planReq.Controls)) - for ctrlName, planCtrl := range planCtrlMap { - stateCtrl, exists := stateCtrlMap[ctrlName] - if !exists { - hasChanges = true - break + for _, stateCtrl := range stateReq.Controls { + stateCtrlName := stateCtrl.Name.ValueString() + if planCtrl, exists := planCtrlMap[stateCtrlName]; exists { + sortedControls = append(sortedControls, planCtrl) + delete(planCtrlMap, stateCtrlName) + } } - if !planCtrl.RulesID.Equal(stateCtrl.RulesID) { - hasChanges = true - break + for _, planCtrl := range planCtrlMap { + sortedControls = append(sortedControls, planCtrl) } - } - if hasChanges { - break + sortedReq := complianceCustomFrameworkRequirementsModel{ + Name: planReq.Name, + Controls: sortedControls, + } + sortedRequirements = append(sortedRequirements, sortedReq) + delete(planReqMap, stateReqName) } } - if !hasChanges { - newPlan := complianceCustomFrameworkModel{ - ID: state.ID, - Version: plan.Version, - Handle: plan.Handle, - Name: plan.Name, - IconURL: plan.IconURL, - Requirements: state.Requirements, - } - diags = resp.Plan.Set(ctx, &newPlan) - resp.Diagnostics.Append(diags...) + for _, planReq := range planReqMap { + sortedRequirements = append(sortedRequirements, planReq) + } + + newPlan := complianceCustomFrameworkModel{ + ID: state.ID, + Version: plan.Version, + Handle: plan.Handle, + Name: plan.Name, + IconURL: plan.IconURL, + Requirements: sortedRequirements, } + + diags = resp.Plan.Set(ctx, &newPlan) + resp.Diagnostics.Append(diags...) } func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string, currentState *complianceCustomFrameworkModel) complianceCustomFrameworkModel { @@ -321,70 +319,84 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl) } - reqMap := make(map[string]datadogV2.CustomFrameworkRequirement) - ctrlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) + apiReqMap := make(map[string]datadogV2.CustomFrameworkRequirement) + apiCtrlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) for _, req := range data.GetData().Attributes.Requirements { - reqMap[req.GetName()] = req - ctrlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl) + apiReqMap[req.GetName()] = req + apiCtrlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl) for _, ctrl := range req.GetControls() { - ctrlMap[req.GetName()][ctrl.GetName()] = ctrl + apiCtrlMap[req.GetName()][ctrl.GetName()] = ctrl } } + sortedRequirements := make([]complianceCustomFrameworkRequirementsModel, 0, len(data.GetData().Attributes.Requirements)) + if currentState != nil { - hasChanges := false - stateReqMap := make(map[string]bool) - stateCtrlMap := make(map[string]map[string]bool) - - for _, req := range currentState.Requirements { - reqName := req.Name.ValueString() - stateReqMap[reqName] = true - stateCtrlMap[reqName] = make(map[string]bool) - for _, ctrl := range req.Controls { - stateCtrlMap[reqName][ctrl.Name.ValueString()] = true - } - } + for _, currentReq := range currentState.Requirements { + currentReqName := currentReq.Name.ValueString() + if apiReq, exists := apiReqMap[currentReqName]; exists { + sortedControls := make([]complianceCustomFrameworkControlsModel, 0, len(apiReq.GetControls())) + + for _, currentCtrl := range currentReq.Controls { + currentCtrlName := currentCtrl.Name.ValueString() + if apiCtrl, exists := apiCtrlMap[currentReqName][currentCtrlName]; exists { + rulesID := make([]attr.Value, len(apiCtrl.GetRulesId())) + for k, v := range apiCtrl.GetRulesId() { + rulesID[k] = types.StringValue(v) + } + + sortedControls = append(sortedControls, complianceCustomFrameworkControlsModel{ + Name: types.StringValue(apiCtrl.GetName()), + RulesID: types.SetValueMust(types.StringType, rulesID), + }) + delete(apiCtrlMap[currentReqName], currentCtrlName) + } + } - for reqName := range reqMap { - if !stateReqMap[reqName] { - hasChanges = true - break - } - for ctrlName := range ctrlMap[reqName] { - if !stateCtrlMap[reqName][ctrlName] { - hasChanges = true - break + for _, apiCtrl := range apiCtrlMap[currentReqName] { + rulesID := make([]attr.Value, len(apiCtrl.GetRulesId())) + for k, v := range apiCtrl.GetRulesId() { + rulesID[k] = types.StringValue(v) + } + + sortedControls = append(sortedControls, complianceCustomFrameworkControlsModel{ + Name: types.StringValue(apiCtrl.GetName()), + RulesID: types.SetValueMust(types.StringType, rulesID), + }) } - } - } - if !hasChanges { - state.Requirements = currentState.Requirements - return state + sortedReq := complianceCustomFrameworkRequirementsModel{ + Name: types.StringValue(apiReq.GetName()), + Controls: sortedControls, + } + sortedRequirements = append(sortedRequirements, sortedReq) + delete(apiReqMap, currentReqName) + } } } - state.Requirements = make([]complianceCustomFrameworkRequirementsModel, len(data.GetData().Attributes.Requirements)) - for i, req := range data.GetData().Attributes.Requirements { - state.Requirements[i] = complianceCustomFrameworkRequirementsModel{ - Name: types.StringValue(req.GetName()), - Controls: make([]complianceCustomFrameworkControlsModel, len(req.GetControls())), - } - - for j, ctrl := range req.GetControls() { - rulesID := make([]attr.Value, len(ctrl.GetRulesId())) - for k, v := range ctrl.GetRulesId() { + for _, apiReq := range apiReqMap { + controls := make([]complianceCustomFrameworkControlsModel, len(apiReq.GetControls())) + for j, apiCtrl := range apiReq.GetControls() { + rulesID := make([]attr.Value, len(apiCtrl.GetRulesId())) + for k, v := range apiCtrl.GetRulesId() { rulesID[k] = types.StringValue(v) } - state.Requirements[i].Controls[j] = complianceCustomFrameworkControlsModel{ - Name: types.StringValue(ctrl.GetName()), + controls[j] = complianceCustomFrameworkControlsModel{ + Name: types.StringValue(apiCtrl.GetName()), RulesID: types.SetValueMust(types.StringType, rulesID), } } + + sortedRequirements = append(sortedRequirements, complianceCustomFrameworkRequirementsModel{ + Name: types.StringValue(apiReq.GetName()), + Controls: controls, + }) } + state.Requirements = sortedRequirements return state } From 827f4a9a051404c96b629f40e1df4a8eb931aeb7 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 22 May 2025 11:57:18 -0500 Subject: [PATCH 47/54] remove modify plan because read API response order is changed --- ...rce_datadog_compliance_custom_framework.go | 103 ++----- ...CreateAndUpdateMultipleRequirements.freeze | 2 +- ...k_CreateAndUpdateMultipleRequirements.yaml | 18 +- .../TestCustomFramework_CreateBasic.freeze | 2 +- .../TestCustomFramework_CreateBasic.yaml | 22 +- ...ustomFramework_CreateWithoutIconURL.freeze | 2 +- ...tCustomFramework_CreateWithoutIconURL.yaml | 8 +- ...ustomFramework_DeleteAfterAPIDelete.freeze | 2 +- ...tCustomFramework_DeleteAfterAPIDelete.yaml | 105 ++++++-- ...estCustomFramework_DuplicateRuleIds.freeze | 2 +- .../TestCustomFramework_DuplicateRuleIds.yaml | 8 +- .../TestCustomFramework_InvalidCreate.freeze | 2 +- .../TestCustomFramework_InvalidCreate.yaml | 2 +- ...tomFramework_RecreateAfterAPIDelete.freeze | 2 +- ...ustomFramework_RecreateAfterAPIDelete.yaml | 104 ++----- ...Framework_RecreateOnImmutableFields.freeze | 2 +- ...omFramework_RecreateOnImmutableFields.yaml | 18 +- ...tCustomFramework_SameConfigNoUpdate.freeze | 2 +- ...estCustomFramework_SameConfigNoUpdate.yaml | 50 ++-- ...TestCustomFramework_SameFrameworkID.freeze | 2 +- .../TestCustomFramework_SameFrameworkID.yaml | 54 ++-- ...omFramework_UpdateIfFrameworkExists.freeze | 2 +- ...stomFramework_UpdateIfFrameworkExists.yaml | 16 +- ...atadog_compliance_custom_framework_test.go | 255 ++++++++++++------ 24 files changed, 405 insertions(+), 380 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 5a154c581..279b90b0c 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -13,7 +13,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/validators" ) var _ resource.Resource = &complianceCustomFrameworkResource{} @@ -23,6 +25,8 @@ type complianceCustomFrameworkResource struct { Auth context.Context } +// to handle a larger input, requirements and controls had to be lists even though order doesn't matter +// but rules can be sets since requirements and controls are lists (the performance issue happened when all were sets) type complianceCustomFrameworkModel struct { ID types.String `tfsdk:"id"` Version types.String `tfsdk:"version"` @@ -93,6 +97,10 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Blocks: map[string]schema.Block{ "requirements": schema.ListNestedBlock{ Description: "The requirements of the framework.", + Validators: []validator.List{ + validators.RequirementNameValidator(), + listvalidator.IsRequired(), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -106,6 +114,10 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Blocks: map[string]schema.Block{ "controls": schema.ListNestedBlock{ Description: "The controls of the requirement.", + Validators: []validator.List{ + validators.ControlNameValidator(), + listvalidator.IsRequired(), + }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -222,93 +234,6 @@ func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request response.Diagnostics.Append(diags...) } -func (r *complianceCustomFrameworkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - if req.Plan.Raw.IsNull() { - return - } - - if req.State.Raw.IsNull() { - return - } - - var plan, state complianceCustomFrameworkModel - - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - diags = req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - planReqMap := make(map[string]complianceCustomFrameworkRequirementsModel) - stateReqMap := make(map[string]complianceCustomFrameworkRequirementsModel) - - for _, req := range plan.Requirements { - planReqMap[req.Name.ValueString()] = req - } - for _, req := range state.Requirements { - stateReqMap[req.Name.ValueString()] = req - } - - sortedRequirements := make([]complianceCustomFrameworkRequirementsModel, 0, len(plan.Requirements)) - - for _, stateReq := range state.Requirements { - stateReqName := stateReq.Name.ValueString() - if planReq, exists := planReqMap[stateReqName]; exists { - planCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) - stateCtrlMap := make(map[string]complianceCustomFrameworkControlsModel) - - for _, ctrl := range planReq.Controls { - planCtrlMap[ctrl.Name.ValueString()] = ctrl - } - for _, ctrl := range stateReq.Controls { - stateCtrlMap[ctrl.Name.ValueString()] = ctrl - } - - sortedControls := make([]complianceCustomFrameworkControlsModel, 0, len(planReq.Controls)) - - for _, stateCtrl := range stateReq.Controls { - stateCtrlName := stateCtrl.Name.ValueString() - if planCtrl, exists := planCtrlMap[stateCtrlName]; exists { - sortedControls = append(sortedControls, planCtrl) - delete(planCtrlMap, stateCtrlName) - } - } - - for _, planCtrl := range planCtrlMap { - sortedControls = append(sortedControls, planCtrl) - } - sortedReq := complianceCustomFrameworkRequirementsModel{ - Name: planReq.Name, - Controls: sortedControls, - } - sortedRequirements = append(sortedRequirements, sortedReq) - delete(planReqMap, stateReqName) - } - } - - for _, planReq := range planReqMap { - sortedRequirements = append(sortedRequirements, planReq) - } - - newPlan := complianceCustomFrameworkModel{ - ID: state.ID, - Version: plan.Version, - Handle: plan.Handle, - Name: plan.Name, - IconURL: plan.IconURL, - Requirements: sortedRequirements, - } - - diags = resp.Plan.Set(ctx, &newPlan) - resp.Diagnostics.Append(diags...) -} - func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string, currentState *complianceCustomFrameworkModel) complianceCustomFrameworkModel { var state complianceCustomFrameworkModel state.ID = types.StringValue(handle + "-" + version) @@ -318,7 +243,9 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str if data.GetData().Attributes.IconUrl != nil { state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl) } - + // since the requirements and controls from the API response might be in a different order than the state + // we need to sort them to match the state so terraform can detect the changes + // without taking order into account apiReqMap := make(map[string]datadogV2.CustomFrameworkRequirement) apiCtrlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze index c611e7eb0..74d749e14 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.freeze @@ -1 +1 @@ -2025-05-15T13:09:31.384764-04:00 \ No newline at end of file +2025-05-22T10:29:37.579487-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml index 33c0f9c40..94e04d8d0 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_CreateAndUpdateMultipleRequirements.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"},{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 417.707542ms + duration: 1.052087125s - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 358.391ms + duration: 911.326958ms - id: 2 request: proto: HTTP/1.1 @@ -102,7 +102,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 300.353917ms + duration: 250.824375ms - id: 3 request: proto: HTTP/1.1 @@ -115,7 +115,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"},{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"security-control","rules_id":["def-000-cea"]}],"name":"security-requirement"},{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"},{"controls":[{"name":"control-2","rules_id":["def-000-be9"]},{"name":"control-3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement-2"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -138,7 +138,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 676.958959ms + duration: 874.516417ms - id: 4 request: proto: HTTP/1.1 @@ -171,7 +171,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 668.5285ms + duration: 609.843333ms - id: 5 request: proto: HTTP/1.1 @@ -204,7 +204,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 441.340792ms + duration: 492.356333ms - id: 6 request: proto: HTTP/1.1 @@ -237,4 +237,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 43.623958ms + duration: 81.528292ms diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze index f1b3d1f4f..44a6b5c44 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.freeze @@ -1 +1 @@ -2025-05-15T13:09:47.144841-04:00 \ No newline at end of file +2025-05-22T10:28:47.103907-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml index e3a020e28..4e46283fc 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_CreateBasic.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 503.217417ms + duration: 496.214583ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 179.772625ms + duration: 138.789792ms - id: 2 request: proto: HTTP/1.1 @@ -102,7 +102,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 262.21975ms + duration: 216.784209ms - id: 3 request: proto: HTTP/1.1 @@ -115,7 +115,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"},{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -138,7 +138,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 934.496958ms + duration: 579.1545ms - id: 4 request: proto: HTTP/1.1 @@ -171,7 +171,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 357.582459ms + duration: 387.4965ms - id: 5 request: proto: HTTP/1.1 @@ -204,7 +204,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 277.229541ms + duration: 325.68ms - id: 6 request: proto: HTTP/1.1 @@ -240,7 +240,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 523.012417ms + duration: 595.280625ms - id: 7 request: proto: HTTP/1.1 @@ -273,7 +273,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 150.182542ms + duration: 191.487459ms - id: 8 request: proto: HTTP/1.1 @@ -306,7 +306,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 521.493ms + duration: 416.820542ms - id: 9 request: proto: HTTP/1.1 @@ -339,4 +339,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 74.374542ms + duration: 83.548458ms diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze index 612c8952b..bcd1668ea 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.freeze @@ -1 +1 @@ -2025-05-14T13:42:18.478133-04:00 \ No newline at end of file +2025-05-22T10:29:15.759609-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml index ea2317501..8ac651392 100644 --- a/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_CreateWithoutIconURL.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 817.065625ms + duration: 506.19625ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 298.588417ms + duration: 276.140542ms - id: 2 request: proto: HTTP/1.1 @@ -102,7 +102,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 647.031209ms + duration: 750.406292ms - id: 3 request: proto: HTTP/1.1 @@ -135,4 +135,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 61.763916ms + duration: 70.235625ms diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze index ef7ad98ce..548c78240 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.freeze @@ -1 +1 @@ -2025-05-14T13:14:53.105131-04:00 \ No newline at end of file +2025-05-22T11:41:02.727814-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml index f313cb7eb..f929e5081 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_DeleteAfterAPIDelete.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 619.970417ms + duration: 519.797292ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 239.534167ms + duration: 379.494375ms - id: 2 request: proto: HTTP/1.1 @@ -87,22 +87,22 @@ interactions: Accept: - application/json url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET + method: DELETE response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 279 + content_length: 195 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 192.272833ms + duration: 338.217417ms - id: 3 request: proto: HTTP/1.1 @@ -120,23 +120,59 @@ interactions: Accept: - application/json url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE + method: GET response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 195 + content_length: 51 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 101.837333ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 252 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 450.136667ms - - id: 4 + duration: 370.99ms + - id: 5 request: proto: HTTP/1.1 proto_major: 1 @@ -160,16 +196,49 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 51 + content_length: 279 uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 46.896708ms - - id: 5 + status: 200 OK + code: 200 + duration: 189.421584ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 195 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 656.383917ms + - id: 7 request: proto: HTTP/1.1 proto_major: 1 @@ -201,4 +270,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 59.534792ms + duration: 99.99025ms diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze index 691798890..dc47d9e49 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.freeze @@ -1 +1 @@ -2025-05-14T13:22:06.085019-04:00 \ No newline at end of file +2025-05-22T11:48:01.178393-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml index f3f425d6b..9707f7b36 100644 --- a/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateRuleIds.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 239.663166ms + duration: 646.051833ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 118.675416ms + duration: 236.522542ms - id: 2 request: proto: HTTP/1.1 @@ -102,7 +102,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 392.879417ms + duration: 479.3125ms - id: 3 request: proto: HTTP/1.1 @@ -135,4 +135,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 41.4355ms + duration: 97.182917ms diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 3e1141aaa..7e56c63a4 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-05-16T16:28:32.463436-04:00 \ No newline at end of file +2025-05-22T11:49:17.106306-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index 0d2728c5c..99b80563e 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -36,4 +36,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 357.055791ms + duration: 372.630125ms diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze index ece3bc3e3..fed2f6f15 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.freeze @@ -1 +1 @@ -2025-05-19T09:53:17.525477-04:00 \ No newline at end of file +2025-05-22T11:40:36.279056-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml index 567749fef..80c310b83 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateAfterAPIDelete.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 425.451125ms + duration: 656.768083ms - id: 1 request: proto: HTTP/1.1 @@ -69,41 +69,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 183.696666ms + duration: 180.020208ms - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 279 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 192.715042ms - - id: 3 request: proto: HTTP/1.1 proto_major: 1 @@ -135,41 +102,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 793.678167ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 58.679167ms - - id: 5 + duration: 536.796417ms + - id: 3 request: proto: HTTP/1.1 proto_major: 1 @@ -201,20 +135,20 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 53.259333ms - - id: 6 + duration: 80.27375ms + - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 385 + content_length: 252 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"compliance-requirement"},{"controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}],"name":"security-requirement"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -237,8 +171,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 369.107917ms - - id: 7 + duration: 517.871542ms + - id: 5 request: proto: HTTP/1.1 proto_major: 1 @@ -262,16 +196,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 412 + content_length: 279 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-name","requirements":[{"name":"compliance-requirement","controls":[{"name":"compliance-control","rules_id":["def-000-be9","def-000-cea"]}]},{"name":"security-requirement","controls":[{"name":"security-control","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 359.949584ms - - id: 8 + duration: 153.309084ms + - id: 6 request: proto: HTTP/1.1 proto_major: 1 @@ -295,16 +229,16 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 180 + content_length: 195 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"new-name","version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 408.013833ms - - id: 9 + duration: 346.506375ms + - id: 7 request: proto: HTTP/1.1 proto_major: 1 @@ -336,4 +270,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 57.676375ms + duration: 93.734042ms diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze index 2562c2967..fa34135a7 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.freeze @@ -1 +1 @@ -2025-05-19T10:06:22.926152-04:00 \ No newline at end of file +2025-05-22T11:43:27.403873-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml index 499a1fe16..255c5c20a 100644 --- a/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_RecreateOnImmutableFields.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 414.967541ms + duration: 417.890125ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 165.35625ms + duration: 356.255ms - id: 2 request: proto: HTTP/1.1 @@ -102,7 +102,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 365.5285ms + duration: 177.291709ms - id: 3 request: proto: HTTP/1.1 @@ -135,7 +135,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 538.317542ms + duration: 439.09975ms - id: 4 request: proto: HTTP/1.1 @@ -171,7 +171,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 424.232125ms + duration: 430.19675ms - id: 5 request: proto: HTTP/1.1 @@ -204,7 +204,7 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 41.628875ms + duration: 100.05225ms - id: 6 request: proto: HTTP/1.1 @@ -237,7 +237,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 163.765083ms + duration: 168.000083ms - id: 7 request: proto: HTTP/1.1 @@ -270,7 +270,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 384.015ms + duration: 336.826417ms - id: 8 request: proto: HTTP/1.1 @@ -303,4 +303,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 47.263041ms + duration: 84.338167ms diff --git a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze index 1fd72bb93..4f3ed9a69 100644 --- a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze @@ -1 +1 @@ -2025-05-16T14:42:25.957319-04:00 \ No newline at end of file +2025-05-22T10:30:27.088108-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml index 1cde62fb1..f679087b5 100644 --- a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml @@ -13,7 +13,7 @@ interactions: remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2.2","rules_id":["def-000-be9"]},{"name":"control2","rules_id":["def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -28,51 +28,48 @@ interactions: proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 127 + content_length: 123 uncompressed: false - body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json - status: 409 Conflict - code: 409 - duration: 102.871083ms + status: 200 OK + code: 200 + duration: 454.820917ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 483 + content_length: 0 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} + body: "" form: {} headers: Accept: - application/json - Content-Type: - - application/json url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: PUT + method: GET response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 123 + content_length: 510 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 477.118167ms + duration: 528.192208ms - id: 2 request: proto: HTTP/1.1 @@ -105,40 +102,43 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 292.398291ms + duration: 630.349083ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 0 + content_length: 483 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" - body: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"},{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: - application/json + Content-Type: + - application/json url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET + method: PUT response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 510 + content_length: 123 uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 555.781208ms + duration: 918.336084ms - id: 4 request: proto: HTTP/1.1 @@ -171,7 +171,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 639.509125ms + duration: 510.668041ms - id: 5 request: proto: HTTP/1.1 @@ -204,7 +204,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 337.378541ms + duration: 540.030417ms - id: 6 request: proto: HTTP/1.1 @@ -237,4 +237,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 57.430667ms + duration: 74.105292ms diff --git a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze index 4c5fcff9c..717d43f5b 100644 --- a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.freeze @@ -1 +1 @@ -2025-05-16T13:09:12.197143-04:00 \ No newline at end of file +2025-05-22T10:30:04.353302-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml index bebeeeffe..73889a4c7 100644 --- a/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_SameFrameworkID.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 459.425834ms + duration: 620.897666ms - id: 1 request: proto: HTTP/1.1 @@ -69,7 +69,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 245.849583ms + duration: 173.264625ms - id: 2 request: proto: HTTP/1.1 @@ -102,76 +102,76 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 175.900125ms + duration: 242.677875ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 259 + content_length: 0 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" - body: | - {"data":{"attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"terraform-framework-1.0"},"type":"custom_framework"}} + body: "" form: {} headers: Accept: - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 + method: DELETE response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 137 + content_length: 209 uncompressed: false - body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","version":"terraform-framework-1.0"}}}' + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","version":"framework-1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 425.030625ms + duration: 343.581417ms - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 0 + content_length: 259 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" - body: "" + body: | + {"data":{"attributes":{"handle":"new","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"terraform-framework-1.0"},"type":"custom_framework"}} form: {} headers: Accept: - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/new-terraform/framework-1.0 - method: DELETE + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 transfer_encoding: [] trailer: {} - content_length: 209 + content_length: 137 uncompressed: false - body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"description":"","handle":"new-terraform","icon_url":"test-url","name":"new-framework-terraform","version":"framework-1.0"}}}' + body: '{"data":{"id":"new-terraform-framework-1.0","type":"custom_framework","attributes":{"handle":"new","version":"terraform-framework-1.0"}}}' headers: Content-Type: - application/vnd.api+json status: 200 OK code: 200 - duration: 534.074291ms + duration: 525.991166ms - id: 5 request: proto: HTTP/1.1 @@ -204,7 +204,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 203.0945ms + duration: 214.634792ms - id: 6 request: proto: HTTP/1.1 @@ -237,7 +237,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 281.544583ms + duration: 200.667916ms - id: 7 request: proto: HTTP/1.1 @@ -270,7 +270,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 455.115958ms + duration: 331.03ms - id: 8 request: proto: HTTP/1.1 @@ -306,7 +306,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 494.117958ms + duration: 347.11125ms - id: 9 request: proto: HTTP/1.1 @@ -339,7 +339,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 284.34925ms + duration: 231.189833ms - id: 10 request: proto: HTTP/1.1 @@ -372,7 +372,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 316.266375ms + duration: 362.871458ms - id: 11 request: proto: HTTP/1.1 @@ -405,4 +405,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 57.598ms + duration: 69.891458ms diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze index c1fccb62d..0a62a92b4 100644 --- a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze @@ -1 +1 @@ -2025-05-16T12:30:43.514665-04:00 \ No newline at end of file +2025-05-22T11:41:25.365854-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml index c51299f3d..9d7a269cf 100644 --- a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml @@ -6,14 +6,14 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 250 + content_length: 255 transfer_encoding: [] trailer: {} host: api.datadoghq.com remote_addr: "" request_uri: "" body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control","rules_id":["def-000-be9"]}],"name":"requirement"}],"version":"1.0"},"type":"custom_framework"}} + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"","name":"existing-framework","requirements":[{"controls":[{"name":"existing-control","rules_id":["def-000-be9"]}],"name":"existing-requirement"}],"version":"1.0"},"type":"custom_framework"}} form: {} headers: Accept: @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 370.3955ms + duration: 432.001125ms - id: 1 request: proto: HTTP/1.1 @@ -72,7 +72,7 @@ interactions: - application/vnd.api+json status: 409 Conflict code: 409 - duration: 80.751916ms + duration: 95.856042ms - id: 2 request: proto: HTTP/1.1 @@ -108,7 +108,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 515.457375ms + duration: 797.741916ms - id: 3 request: proto: HTTP/1.1 @@ -141,7 +141,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 209.929417ms + duration: 462.0445ms - id: 4 request: proto: HTTP/1.1 @@ -174,7 +174,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 398.002875ms + duration: 629.974083ms - id: 5 request: proto: HTTP/1.1 @@ -207,4 +207,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 73.471375ms + duration: 85.395041ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 05c3a63a6..966105347 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -78,20 +78,21 @@ func TestCustomFramework_CreateBasic(t *testing.T) { resource.TestCheckResourceAttr(path, "icon_url", "test-url"), resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - ), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9")), }, { Config: testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version, handle), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "requirements.0.name", "compliance-requirement"), - resource.TestCheckResourceAttr(path, "requirements.1.name", "security-requirement"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "compliance-control"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "security-control"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "compliance-requirement"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "security-control"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "compliance-control"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-cea"), ), }, { @@ -151,12 +152,14 @@ func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "requirements.0.name", "compliance-requirement"), - resource.TestCheckResourceAttr(path, "requirements.1.name", "security-requirement"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "compliance-control"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "security-control"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "compliance-requirement"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "security-control"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "compliance-control"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-cea"), ), }, { @@ -165,14 +168,17 @@ func TestCustomFramework_CreateAndUpdateMultipleRequirements(t *testing.T) { resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "requirements.0.name", "security-requirement"), - resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement-2"), - resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement"), + resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement-2"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "security-control"), resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control"), resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control-2"), resource.TestCheckResourceAttr(path, "requirements.2.controls.1.name", "control-3"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.1.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.1.rules_id.*", "def-000-be9"), ), }, }, @@ -286,7 +292,7 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { name = "control1" rules_id = ["def-000-be9"] } - } + } requirements { name = "requirement2" controls { @@ -317,10 +323,14 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement2"), resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement3"), resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control2"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control2.2"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.1.name", "control2"), resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control3"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.1.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-be9"), ), }, { @@ -330,14 +340,18 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { resource.TestCheckResourceAttr(path, "version", version), resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), resource.TestCheckResourceAttr(path, "icon_url", "test url"), - resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), - resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement2"), - resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement3"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control2"), - resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control3"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.rules_id.0", "def-000-cea"), + resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement3"), + resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement1"), + resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement2"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control3"), + resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control2"), + resource.TestCheckResourceAttr(path, "requirements.2.controls.1.name", "control2.2"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-cea"), + resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.1.rules_id.*", "def-000-be9"), ), // This step should not trigger an update since only the order is different ExpectNonEmptyPlan: false, @@ -346,10 +360,7 @@ func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { }) } -// There is no way to validate the duplicate rule IDs in the config before they are removed from the state in Terraform -// because the state model converts the rules_id into sets, the duplicate rule IDs are removed -// This test validates that the duplicate rule IDs are removed from the state and only one rule ID is present -func TestCustomFramework_DuplicateRuleIds(t *testing.T) { +func TestCustomFramework_InvalidCreate(t *testing.T) { handle := "terraform-handle" version := "1.0" @@ -361,57 +372,52 @@ func TestCustomFramework_DuplicateRuleIds(t *testing.T) { CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), Steps: []resource.TestStep{ { - Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(path, "handle", handle), - resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.1.rules_id.0", "def-000-be9"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), - ), + Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, - }, - }) -} - -func TestCustomFramework_InvalidCreate(t *testing.T) { - handle := "terraform-handle" - version := "1.0" - - ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_compliance_custom_framework.sample_rules" - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), - Steps: []resource.TestStep{ { - Config: testAccCheckDatadogCreateFrameworkWithNoControls(version, handle), - ExpectError: regexp.MustCompile("controls is required"), + Config: testAccCheckDatadogCreateFrameworkRuleIdsInvalid(version, handle), + ExpectError: regexp.MustCompile("400 Bad Request"), }, { Config: testAccCheckDatadogCreateFrameworkNoRequirements(version, handle), - ExpectError: regexp.MustCompile("requirements is required"), + ExpectError: regexp.MustCompile("Invalid Block"), }, { - Config: testAccCheckDatadogCreateInvalidFrameworkName(version, handle), - ExpectError: regexp.MustCompile("name is required"), + Config: testAccCheckDatadogCreateFrameworkWithNoControls(version, handle), + ExpectError: regexp.MustCompile("Invalid Block"), }, { - Config: testAccCheckDatadogEmptyRequirementName(version, handle), - ExpectError: regexp.MustCompile("name is required"), + Config: testAccCheckDatadogCreateEmptyHandle(version), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, { - Config: testAccCheckDatadogEmptyControlName(version, handle), - ExpectError: regexp.MustCompile("name is required"), + Config: testAccCheckDatadogCreateEmptyVersion(handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, { - Config: testAccCheckDatadogCreateEmptyHandle(version), - ExpectError: regexp.MustCompile("handle is required"), + Config: testAccCheckDatadogDuplicateRequirementName(version, handle), + ExpectError: regexp.MustCompile(".*Each Requirement must have a unique name.*"), }, { - Config: testAccCheckDatadogCreateEmptyVersion(handle), - ExpectError: regexp.MustCompile("version is required"), + Config: testAccCheckDatadogDuplicateControlName(version, handle), + ExpectError: regexp.MustCompile(".*Each Control must have a unique name under the same requirement.*"), + }, + { + Config: testAccCheckDatadogDuplicateRequirement(version, handle), + ExpectError: regexp.MustCompile(".*Each Requirement must have a unique name.*"), + }, + { + Config: testAccCheckDatadogDuplicateControl(version, handle), + ExpectError: regexp.MustCompile(".*Each Control must have a unique name under the same requirement.*"), + }, + { + Config: testAccCheckDatadogEmptyRequirementName(version, handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), + }, + { + Config: testAccCheckDatadogEmptyControlName(version, handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, }, }) @@ -551,9 +557,12 @@ func TestCustomFramework_UpdateIfFrameworkExists(t *testing.T) { func TestCustomFramework_RecreateOnImmutableFields(t *testing.T) { handle := "terraform-handle" version := "1.0" + newHandle := "terraform-handle-new" + newVersion := "2.0" ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) path := "datadog_compliance_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, @@ -564,18 +573,50 @@ func TestCustomFramework_RecreateOnImmutableFields(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(path, "handle", handle), resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), ), }, { - Config: testAccCheckDatadogCreateFramework("2.0", handle), - ExpectError: regexp.MustCompile("version cannot be changed"), + Config: testAccCheckDatadogCreateFramework(newVersion, newHandle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", newHandle), + resource.TestCheckResourceAttr(path, "version", newVersion), + // Verify old resource is deleted + func(s *terraform.State) error { + _, httpResp, err := providers.frameworkProvider.DatadogApiInstances.GetSecurityMonitoringApiV2().GetCustomFramework(providers.frameworkProvider.Auth, handle, version) + if err == nil || (httpResp != nil && httpResp.StatusCode != 400) { + return fmt.Errorf("old framework with handle %s and version %s still exists", handle, version) + } + return nil + }, + ), }, + }, + }) +} + +// There is no way to validate the duplicate rule IDs in the config before they are removed from the state in Terraform +// because the state model converts the rules_id into sets, the duplicate rule IDs are removed +// This test validates that the duplicate rule IDs are removed from the state and only one rule ID is present +func TestCustomFramework_DuplicateRuleIds(t *testing.T) { + handle := "terraform-handle" + version := "1.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_compliance_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ { - Config: testAccCheckDatadogCreateFramework(version, "new-handle"), - ExpectError: regexp.MustCompile("handle cannot be changed"), + Config: testAccCheckDatadogDuplicateRulesId(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.0", "def-000-be9"), + resource.TestCheckResourceAttr(path, "requirements.0.controls.0.rules_id.#", "1"), + ), }, }, }) @@ -828,7 +869,7 @@ func testAccCheckDatadogCreateEmptyVersion(handle string) string { `, handle) } -func testAccCheckDatadogDuplicateRequirements(version string, handle string) string { +func testAccCheckDatadogDuplicateRequirementName(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" @@ -853,7 +894,32 @@ func testAccCheckDatadogDuplicateRequirements(version string, handle string) str `, version, handle) } -func testAccCheckDatadogDuplicateControls(version string, handle string) string { +func testAccCheckDatadogDuplicateRequirement(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "framework-name" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + +func testAccCheckDatadogDuplicateControlName(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { version = "%s" @@ -882,6 +948,35 @@ func testAccCheckDatadogDuplicateControls(version string, handle string) string `, version, handle) } +func testAccCheckDatadogDuplicateControl(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "framework-name" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + requirements { + name = "requirement2" + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + controls { + name = "control1" + rules_id = ["def-000-be9"] + } + } + } + `, version, handle) +} + func testAccCheckDatadogDuplicateRulesId(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { From 2ea549a88f94606cee852b4451bd170e62bf6d56 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 22 May 2025 12:17:00 -0500 Subject: [PATCH 48/54] remove import file --- examples/resources/datadog_compliance_custom_framework/import.sh | 1 - 1 file changed, 1 deletion(-) delete mode 100755 examples/resources/datadog_compliance_custom_framework/import.sh diff --git a/examples/resources/datadog_compliance_custom_framework/import.sh b/examples/resources/datadog_compliance_custom_framework/import.sh deleted file mode 100755 index 5c0d83796..000000000 --- a/examples/resources/datadog_compliance_custom_framework/import.sh +++ /dev/null @@ -1 +0,0 @@ -terraform import datadog_custom_framework.example3 "terraform-created-framework-handle-1.0.0" \ No newline at end of file From b48425768ba51dbd6cbf2d95b04968b7cef7c335 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 22 May 2025 12:38:20 -0500 Subject: [PATCH 49/54] check for rule ids length and update docs --- ...rce_datadog_compliance_custom_framework.go | 4 ++ .../TestCustomFramework_InvalidCreate.freeze | 2 +- .../TestCustomFramework_InvalidCreate.yaml | 2 +- ...atadog_compliance_custom_framework_test.go | 22 ++++++++ docs/resources/compliance_custom_framework.md | 50 +++++++++---------- .../resource.tf | 31 +++++++----- 6 files changed, 70 insertions(+), 41 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 279b90b0c..40dc298c8 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -4,6 +4,7 @@ import ( "context" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -131,6 +132,9 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource Description: "The set of rules IDs for the control.", ElementType: types.StringType, Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, }, }, }, diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze index 7e56c63a4..c80ac1347 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.freeze @@ -1 +1 @@ -2025-05-22T11:49:17.106306-05:00 \ No newline at end of file +2025-05-22T12:37:52.652765-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml index 99b80563e..92fb6030e 100644 --- a/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_InvalidCreate.yaml @@ -36,4 +36,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 372.630125ms + duration: 364.630958ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 966105347..0aaa28d76 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -419,6 +419,10 @@ func TestCustomFramework_InvalidCreate(t *testing.T) { Config: testAccCheckDatadogEmptyControlName(version, handle), ExpectError: regexp.MustCompile("Invalid Attribute Value Length"), }, + { + Config: testAccCheckDatadogCreateFrameworkWithNoRulesId(version, handle), + ExpectError: regexp.MustCompile("Invalid Attribute Value"), + }, }, }) } @@ -768,6 +772,24 @@ func testAccCheckDatadogCreateFrameworkWithNoControls(version string, handle str `, version, handle) } +func testAccCheckDatadogCreateFrameworkWithNoRulesId(version string, handle string) string { + return fmt.Sprintf(` + resource "datadog_compliance_custom_framework" "sample_rules" { + version = "%s" + handle = "%s" + name = "new-framework-terraform" + icon_url = "test url" + requirements { + name = "requirement1" + controls { + name = "control1" + rules_id = [] + } + } + } + `, version, handle) +} + func testAccCheckDatadogCreateFrameworkNoRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { diff --git a/docs/resources/compliance_custom_framework.md b/docs/resources/compliance_custom_framework.md index 916b6c049..7d5540133 100644 --- a/docs/resources/compliance_custom_framework.md +++ b/docs/resources/compliance_custom_framework.md @@ -13,27 +13,32 @@ Provides a Datadog Compliance Custom Framework resource, which is used to create ## Example Usage ```terraform -resource "datadog_compliance_custom_framework" "example" { - version = "1" - handle = "new-terraform-framework-handle" - name = "new-terraform-framework" - icon_url = "https://example.com/icon.png" +resource "datadog_compliance_custom_framework" "framework" { + name = "my-custom-framework-terraform-2" + version = "2.0.0" + handle = "my-custom-framework-terraform-2" + requirements { - name = "requirement1" + name = "requirement2" controls { - name = "control1" - rules_id = ["aaa-000-ccc", "bbb-000-ddd"] + name = "control2" + rules_id = ["def-000-h9o", "def-000-b6i", "def-000-yed", "def-000-h5a", "def-000-aw5"] } controls { - name = "control2" - rules_id = ["aaa-000-lll"] + name = "control1" + rules_id = ["def-000-j9v", "def-000-465", "def-000-vq1", "def-000-4hf", "def-000-s2d", "def-000-vnl"] } } + requirements { - name = "requirement2" + name = "requirement1" controls { - name = "control3" - rules_id = ["aaa-000-zzz"] + name = "control2" + rules_id = ["def-000-wuf", "def-000-7og"] + } + controls { + name = "control5" + rules_id = ["def-000-mdt", "def-000-zrx", "def-000-z6k"] } } } @@ -47,11 +52,12 @@ resource "datadog_compliance_custom_framework" "example" { - `handle` (String) The framework handle. String length must be at least 1. - `name` (String) The framework name. String length must be at least 1. - `version` (String) The framework version. String length must be at least 1. -- `requirements` (Block Set) The requirements of the framework. (see [below for nested schema](#nestedblock--requirements)) +- `requirements` (Block List) The requirements of the framework. Length must be at least 1. (see [below for nested schema](#nestedblock--requirements)) + -### Optional +### Optional -- `icon_url` (String) The URL of the icon representing the framework. +- `icon_url` (String) The URL of the icon representing the framework ### Read-Only @@ -63,7 +69,7 @@ resource "datadog_compliance_custom_framework" "example" { Required: - `name` (String) The name of the requirement. String length must be at least 1. -- `controls` (Block Set) The controls of the requirement. (see [below for nested schema](#nestedblock--requirements--controls)) +- `controls` (Block List) The controls of the requirement. Length must be at least 1. (see [below for nested schema](#nestedblock--requirements--controls)) ### Nested Schema for `requirements.controls` @@ -71,12 +77,4 @@ Required: Required: - `name` (String) The name of the control. String length must be at least 1. -- `rules_id` (Set of String) The list of rules IDs for the control. - -## Import - -Import is supported using the following syntax: - -```shell -terraform import datadog_custom_framework.example3 "terraform-created-framework-handle-1.0.0" -``` +- `rules_id` (Set of String) The set of rules IDs for the control. Length must be at least 1. diff --git a/examples/resources/datadog_compliance_custom_framework/resource.tf b/examples/resources/datadog_compliance_custom_framework/resource.tf index d8e63fddc..12def186b 100644 --- a/examples/resources/datadog_compliance_custom_framework/resource.tf +++ b/examples/resources/datadog_compliance_custom_framework/resource.tf @@ -1,24 +1,29 @@ -resource "datadog_compliance_custom_framework" "example" { - version = "1" - handle = "new-terraform-framework-handle" - name = "new-terraform-framework" - icon_url = "https://example.com/icon.png" +resource "datadog_compliance_custom_framework" "framework" { + name = "my-custom-framework-terraform-2" + version = "2.0.0" + handle = "my-custom-framework-terraform-2" + requirements { - name = "requirement1" + name = "requirement2" controls { - name = "control1" - rules_id = ["def-000-k6h", "def-000-u48"] + name = "control2" + rules_id = ["def-000-h9o", "def-000-b6i", "def-000-yed", "def-000-h5a", "def-000-aw5"] } controls { - name = "control2" - rules_id = ["def-000-k8u"] + name = "control1" + rules_id = ["def-000-j9v", "def-000-465", "def-000-vq1", "def-000-4hf", "def-000-s2d", "def-000-vnl"] } } + requirements { - name = "requirement2" + name = "requirement1" + controls { + name = "control2" + rules_id = ["def-000-wuf", "def-000-7og"] + } controls { - name = "control3" - rules_id = ["def-000-k6h"] + name = "control5" + rules_id = ["def-000-mdt", "def-000-zrx", "def-000-z6k"] } } } \ No newline at end of file From 014a5993db8c4a411edd6f07719856ececc901d6 Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Thu, 22 May 2025 15:51:36 -0500 Subject: [PATCH 50/54] remove same config no update test --- ...tCustomFramework_SameConfigNoUpdate.freeze | 1 - ...estCustomFramework_SameConfigNoUpdate.yaml | 240 ------------------ ...atadog_compliance_custom_framework_test.go | 131 ---------- 3 files changed, 372 deletions(-) delete mode 100644 datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze delete mode 100644 datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml diff --git a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze deleted file mode 100644 index 4f3ed9a69..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.freeze +++ /dev/null @@ -1 +0,0 @@ -2025-05-22T10:30:27.088108-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml b/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml deleted file mode 100644 index f679087b5..000000000 --- a/datadog/tests/cassettes/TestCustomFramework_SameConfigNoUpdate.yaml +++ /dev/null @@ -1,240 +0,0 @@ ---- -version: 2 -interactions: - - id: 0 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 483 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2.2","rules_id":["def-000-be9"]},{"name":"control2","rules_id":["def-000-cea"]}],"name":"requirement2"},{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks - method: POST - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 454.820917ms - - id: 1 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 510 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 528.192208ms - - id: 2 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 510 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 630.349083ms - - id: 3 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 483 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: | - {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}],"name":"requirement3"},{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"},{"controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}],"name":"requirement2"}],"version":"1.0"},"type":"custom_framework"}} - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: PUT - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 123 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 918.336084ms - - id: 4 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 510 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]},{"name":"requirement2","controls":[{"name":"control2","rules_id":["def-000-cea"]},{"name":"control2.2","rules_id":["def-000-be9"]}]},{"name":"requirement3","controls":[{"name":"control3","rules_id":["def-000-be9","def-000-cea"]}]}],"version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 510.668041ms - - id: 5 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: DELETE - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 195 - uncompressed: false - body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test url","name":"new-framework-terraform","version":"1.0"}}}' - headers: - Content-Type: - - application/vnd.api+json - status: 200 OK - code: 200 - duration: 540.030417ms - - id: 6 - request: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - content_length: 0 - transfer_encoding: [] - trailer: {} - host: api.datadoghq.com - remote_addr: "" - request_uri: "" - body: "" - form: {} - headers: - Accept: - - application/json - url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 - method: GET - response: - proto: HTTP/1.1 - proto_major: 1 - proto_minor: 1 - transfer_encoding: [] - trailer: {} - content_length: 51 - uncompressed: false - body: '{"errors":[{"status":"400","title":"Bad Request"}]}' - headers: - Content-Type: - - application/vnd.api+json - status: 400 Bad Request - code: 400 - duration: 74.105292ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index 0aaa28d76..f9ef7f264 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -229,137 +229,6 @@ func TestCustomFramework_SameFrameworkID(t *testing.T) { }) } -func TestCustomFramework_SameConfigNoUpdate(t *testing.T) { - handle := "terraform-handle" - version := "1.0" - - ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - path := "datadog_compliance_custom_framework.sample_rules" - - // First config with one order of requirements - config1 := fmt.Sprintf(` - resource "datadog_compliance_custom_framework" "sample_rules" { - version = "%s" - handle = "%s" - name = "new-framework-terraform" - icon_url = "test url" - requirements { - name = "requirement1" - controls { - name = "control1" - rules_id = ["def-000-be9"] - } - } - requirements { - name = "requirement2" - controls { - name = "control2.2" - rules_id = ["def-000-be9"] - } - controls { - name = "control2" - rules_id = ["def-000-cea"] - } - } - requirements { - name = "requirement3" - controls { - name = "control3" - rules_id = ["def-000-be9", "def-000-cea"] - } - } - } - `, version, handle) - - // Second config with different order of requirements - // test switching order of requirements, controls, and rules_id - config2 := fmt.Sprintf(` - resource "datadog_compliance_custom_framework" "sample_rules" { - version = "%s" - handle = "%s" - name = "new-framework-terraform" - icon_url = "test url" - requirements { - name = "requirement3" - controls { - name = "control3" - rules_id = ["def-000-cea", "def-000-be9"] - } - } - requirements { - name = "requirement1" - controls { - name = "control1" - rules_id = ["def-000-be9"] - } - } - requirements { - name = "requirement2" - controls { - name = "control2" - rules_id = ["def-000-cea"] - } - controls { - name = "control2.2" - rules_id = ["def-000-be9"] - } - } - } - `, version, handle) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), - Steps: []resource.TestStep{ - { - Config: config1, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(path, "handle", handle), - resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckResourceAttr(path, "icon_url", "test url"), - resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement1"), - resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement2"), - resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement3"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control2.2"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.1.name", "control2"), - resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control3"), - resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.1.rules_id.*", "def-000-cea"), - resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-cea"), - resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-be9"), - ), - }, - { - Config: config2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(path, "handle", handle), - resource.TestCheckResourceAttr(path, "version", version), - resource.TestCheckResourceAttr(path, "name", "new-framework-terraform"), - resource.TestCheckResourceAttr(path, "icon_url", "test url"), - resource.TestCheckResourceAttr(path, "requirements.0.name", "requirement3"), - resource.TestCheckResourceAttr(path, "requirements.1.name", "requirement1"), - resource.TestCheckResourceAttr(path, "requirements.2.name", "requirement2"), - resource.TestCheckResourceAttr(path, "requirements.0.controls.0.name", "control3"), - resource.TestCheckResourceAttr(path, "requirements.1.controls.0.name", "control1"), - resource.TestCheckResourceAttr(path, "requirements.2.controls.0.name", "control2"), - resource.TestCheckResourceAttr(path, "requirements.2.controls.1.name", "control2.2"), - resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-cea"), - resource.TestCheckTypeSetElemAttr(path, "requirements.0.controls.0.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.1.controls.0.rules_id.*", "def-000-be9"), - resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.0.rules_id.*", "def-000-cea"), - resource.TestCheckTypeSetElemAttr(path, "requirements.2.controls.1.rules_id.*", "def-000-be9"), - ), - // This step should not trigger an update since only the order is different - ExpectNonEmptyPlan: false, - }, - }, - }) -} - func TestCustomFramework_InvalidCreate(t *testing.T) { handle := "terraform-handle" version := "1.0" From 7291b2d2df058d8b8f27c6e67ec427e0f0b9051e Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 26 May 2025 23:35:24 -0500 Subject: [PATCH 51/54] use one validator and add test for duplicate handle --- ...rce_datadog_compliance_custom_framework.go | 100 ++++----- .../duplicate_requirement_validator.go | 35 +-- ...TestCustomFramework_DuplicateHandle.freeze | 1 + .../TestCustomFramework_DuplicateHandle.yaml | 207 ++++++++++++++++++ ...atadog_compliance_custom_framework_test.go | 26 +++ 5 files changed, 307 insertions(+), 62 deletions(-) create mode 100644 datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze create mode 100644 datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml diff --git a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go index 40dc298c8..282128dc0 100644 --- a/datadog/fwprovider/resource_datadog_compliance_custom_framework.go +++ b/datadog/fwprovider/resource_datadog_compliance_custom_framework.go @@ -99,7 +99,7 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource "requirements": schema.ListNestedBlock{ Description: "The requirements of the framework.", Validators: []validator.List{ - validators.RequirementNameValidator(), + validators.DuplicateRequirementControlValidator(), listvalidator.IsRequired(), }, NestedObject: schema.NestedBlockObject{ @@ -116,7 +116,6 @@ func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource "controls": schema.ListNestedBlock{ Description: "The controls of the requirement.", Validators: []validator.List{ - validators.ControlNameValidator(), listvalidator.IsRequired(), }, NestedObject: schema.NestedBlockObject{ @@ -164,6 +163,18 @@ func (r *complianceCustomFrameworkResource) Create(ctx context.Context, request _, httpResp, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) if err != nil { if httpResp != nil && httpResp.StatusCode == 409 { // if framework already exists, try to update it with the new state + _, httpResp, getErr := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString()) + // if the framework with the same handle and version does not exist, throw an error because + // only the handle matches which has to be unique + if httpResp != nil && httpResp.StatusCode == 400 { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "Framework with same handle already exists. Currently there is no support for two frameworks with the same handle.")) + return + } + if getErr != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(getErr, "error getting existing compliance custom framework")) + return + } + // if the framework with the same handle and version exists, update it _, _, updateErr := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) if updateErr != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(updateErr, "error updating existing compliance custom framework")) @@ -238,6 +249,28 @@ func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request response.Diagnostics.Append(diags...) } +func convertControlToModel(control datadogV2.CustomFrameworkControl) complianceCustomFrameworkControlsModel { + rulesID := make([]attr.Value, len(control.GetRulesId())) + for k, v := range control.GetRulesId() { + rulesID[k] = types.StringValue(v) + } + return complianceCustomFrameworkControlsModel{ + Name: types.StringValue(control.GetName()), + RulesID: types.SetValueMust(types.StringType, rulesID), + } +} + +func convertRequirementToModel(req datadogV2.CustomFrameworkRequirement) complianceCustomFrameworkRequirementsModel { + controls := make([]complianceCustomFrameworkControlsModel, len(req.GetControls())) + for j, control := range req.GetControls() { + controls[j] = convertControlToModel(control) + } + return complianceCustomFrameworkRequirementsModel{ + Name: types.StringValue(req.GetName()), + Controls: controls, + } +} + func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string, currentState *complianceCustomFrameworkModel) complianceCustomFrameworkModel { var state complianceCustomFrameworkModel state.ID = types.StringValue(handle + "-" + version) @@ -247,20 +280,21 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str if data.GetData().Attributes.IconUrl != nil { state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl) } - // since the requirements and controls from the API response might be in a different order than the state - // we need to sort them to match the state so terraform can detect the changes - // without taking order into account + apiReqMap := make(map[string]datadogV2.CustomFrameworkRequirement) - apiCtrlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) + apiControlMap := make(map[string]map[string]datadogV2.CustomFrameworkControl) for _, req := range data.GetData().Attributes.Requirements { apiReqMap[req.GetName()] = req - apiCtrlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl) - for _, ctrl := range req.GetControls() { - apiCtrlMap[req.GetName()][ctrl.GetName()] = ctrl + apiControlMap[req.GetName()] = make(map[string]datadogV2.CustomFrameworkControl) + for _, control := range req.GetControls() { + apiControlMap[req.GetName()][control.GetName()] = control } } + // since the requirements and controls from the API response might be in a different order than the state + // we need to sort them to match the state so terraform can detect the changes + // without taking order into account sortedRequirements := make([]complianceCustomFrameworkRequirementsModel, 0, len(data.GetData().Attributes.Requirements)) if currentState != nil { @@ -269,32 +303,16 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str if apiReq, exists := apiReqMap[currentReqName]; exists { sortedControls := make([]complianceCustomFrameworkControlsModel, 0, len(apiReq.GetControls())) - for _, currentCtrl := range currentReq.Controls { - currentCtrlName := currentCtrl.Name.ValueString() - if apiCtrl, exists := apiCtrlMap[currentReqName][currentCtrlName]; exists { - rulesID := make([]attr.Value, len(apiCtrl.GetRulesId())) - for k, v := range apiCtrl.GetRulesId() { - rulesID[k] = types.StringValue(v) - } - - sortedControls = append(sortedControls, complianceCustomFrameworkControlsModel{ - Name: types.StringValue(apiCtrl.GetName()), - RulesID: types.SetValueMust(types.StringType, rulesID), - }) - delete(apiCtrlMap[currentReqName], currentCtrlName) + for _, currentControl := range currentReq.Controls { + currentControlName := currentControl.Name.ValueString() + if apiControl, exists := apiControlMap[currentReqName][currentControlName]; exists { + sortedControls = append(sortedControls, convertControlToModel(apiControl)) + delete(apiControlMap[currentReqName], currentControlName) } } - for _, apiCtrl := range apiCtrlMap[currentReqName] { - rulesID := make([]attr.Value, len(apiCtrl.GetRulesId())) - for k, v := range apiCtrl.GetRulesId() { - rulesID[k] = types.StringValue(v) - } - - sortedControls = append(sortedControls, complianceCustomFrameworkControlsModel{ - Name: types.StringValue(apiCtrl.GetName()), - RulesID: types.SetValueMust(types.StringType, rulesID), - }) + for _, apiControl := range apiControlMap[currentReqName] { + sortedControls = append(sortedControls, convertControlToModel(apiControl)) } sortedReq := complianceCustomFrameworkRequirementsModel{ @@ -308,23 +326,7 @@ func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle str } for _, apiReq := range apiReqMap { - controls := make([]complianceCustomFrameworkControlsModel, len(apiReq.GetControls())) - for j, apiCtrl := range apiReq.GetControls() { - rulesID := make([]attr.Value, len(apiCtrl.GetRulesId())) - for k, v := range apiCtrl.GetRulesId() { - rulesID[k] = types.StringValue(v) - } - - controls[j] = complianceCustomFrameworkControlsModel{ - Name: types.StringValue(apiCtrl.GetName()), - RulesID: types.SetValueMust(types.StringType, rulesID), - } - } - - sortedRequirements = append(sortedRequirements, complianceCustomFrameworkRequirementsModel{ - Name: types.StringValue(apiReq.GetName()), - Controls: controls, - }) + sortedRequirements = append(sortedRequirements, convertRequirementToModel(apiReq)) } state.Requirements = sortedRequirements diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_validator.go index 9aa259a12..89a5183f3 100644 --- a/datadog/internal/validators/duplicate_requirement_validator.go +++ b/datadog/internal/validators/duplicate_requirement_validator.go @@ -8,30 +8,25 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -type requirementNameValidator struct{} +type duplicateRequirementControlValidator struct{} -func (v requirementNameValidator) Description(context.Context) string { - return "checks for duplicate requirement names" +func (v duplicateRequirementControlValidator) Description(context.Context) string { + return "checks for duplicate requirement and control names" } -func (v requirementNameValidator) MarkdownDescription(ctx context.Context) string { +func (v duplicateRequirementControlValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -func (v requirementNameValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { +func (v duplicateRequirementControlValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } - var requirementNames []string + seen := make(map[string]bool) for _, requirement := range req.ConfigValue.Elements() { reqObj := requirement.(types.Object) name := reqObj.Attributes()["name"].(types.String).ValueString() - requirementNames = append(requirementNames, name) - } - - seen := make(map[string]bool) - for _, name := range requirementNames { if seen[name] { resp.Diagnostics.AddError( "Each Requirement must have a unique name", @@ -40,9 +35,23 @@ func (v requirementNameValidator) ValidateList(ctx context.Context, req validato return } seen[name] = true + controls := reqObj.Attributes()["controls"].(types.List) + controlNames := make(map[string]bool) + for _, control := range controls.Elements() { + ctrlObj := control.(types.Object) + ctrlName := ctrlObj.Attributes()["name"].(types.String).ValueString() + if controlNames[ctrlName] { + resp.Diagnostics.AddError( + "Each Control must have a unique name under the same requirement", + fmt.Sprintf("Control name '%s' is used more than once under requirement '%s'", ctrlName, name), + ) + return + } + controlNames[ctrlName] = true + } } } -func RequirementNameValidator() validator.List { - return &requirementNameValidator{} +func DuplicateRequirementControlValidator() validator.List { + return &duplicateRequirementControlValidator{} } diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze new file mode 100644 index 000000000..4ce29ffaf --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.freeze @@ -0,0 +1 @@ +2025-05-26T23:32:54.142418-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml new file mode 100644 index 000000000..1f8a18a36 --- /dev/null +++ b/datadog/tests/cassettes/TestCustomFramework_DuplicateHandle.yaml @@ -0,0 +1,207 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 252 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"1.0"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 123 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 368.886833ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 171.795125ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 279 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"name":"requirement1","controls":[{"name":"control1","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 169.562916ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 266 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","requirements":[{"controls":[{"name":"control1","rules_id":["def-000-be9"]}],"name":"requirement1"}],"version":"different-version"},"type":"custom_framework"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks + method: POST + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 127 + uncompressed: false + body: '{"errors":[{"status":"409","title":"Status Conflict","detail":"already_exists(Framework ''terraform-handle'' already existed)"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 409 Conflict + code: 409 + duration: 69.033792ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: DELETE + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 195 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"description":"","handle":"terraform-handle","icon_url":"test-url","name":"new-framework-terraform","version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 372.856ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/different-version + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 51 + uncompressed: false + body: '{"errors":[{"status":"400","title":"Bad Request"}]}' + headers: + Content-Type: + - application/vnd.api+json + status: 400 Bad Request + code: 400 + duration: 306.9705ms diff --git a/datadog/tests/resource_datadog_compliance_custom_framework_test.go b/datadog/tests/resource_datadog_compliance_custom_framework_test.go index f9ef7f264..4533fe040 100644 --- a/datadog/tests/resource_datadog_compliance_custom_framework_test.go +++ b/datadog/tests/resource_datadog_compliance_custom_framework_test.go @@ -495,6 +495,32 @@ func TestCustomFramework_DuplicateRuleIds(t *testing.T) { }) } +func TestCustomFramework_DuplicateHandle(t *testing.T) { + handle := "terraform-handle" + version := "1.0" + + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + path := "datadog_compliance_custom_framework.sample_rules" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogFrameworkDestroy(ctx, providers.frameworkProvider, path, version, handle), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogCreateFramework(version, handle), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(path, "handle", handle), + resource.TestCheckResourceAttr(path, "version", version), + ), + }, + { + Config: testAccCheckDatadogCreateSecondFramework("different-version", handle), + ExpectError: regexp.MustCompile("Framework with same handle already exists"), + }, + }, + }) +} + func testAccCheckDatadogCreateFrameworkWithMultipleRequirements(version string, handle string) string { return fmt.Sprintf(` resource "datadog_compliance_custom_framework" "sample_rules" { From a43227564b6ff982bda015e65a852733c63b4ade Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 26 May 2025 23:38:23 -0500 Subject: [PATCH 52/54] edit validator file name --- .../validators/duplicate_control_validator.go | 48 ------------------- ...uplicate_requirement_control_validator.go} | 0 2 files changed, 48 deletions(-) delete mode 100644 datadog/internal/validators/duplicate_control_validator.go rename datadog/internal/validators/{duplicate_requirement_validator.go => duplicate_requirement_control_validator.go} (100%) diff --git a/datadog/internal/validators/duplicate_control_validator.go b/datadog/internal/validators/duplicate_control_validator.go deleted file mode 100644 index 04a998971..000000000 --- a/datadog/internal/validators/duplicate_control_validator.go +++ /dev/null @@ -1,48 +0,0 @@ -package validators - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -type controlNameValidator struct{} - -func (v controlNameValidator) Description(context.Context) string { - return "checks for duplicate control names" -} - -func (v controlNameValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -func (v controlNameValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - var controlNames []string - for _, control := range req.ConfigValue.Elements() { - controlObj := control.(types.Object) - name := controlObj.Attributes()["name"].(types.String).ValueString() - controlNames = append(controlNames, name) - } - - seen := make(map[string]bool) - for _, name := range controlNames { - if seen[name] { - resp.Diagnostics.AddError( - "Each Control must have a unique name under the same requirement", - fmt.Sprintf("Control name '%s' is used more than once under the same requirement", name), - ) - return - } - seen[name] = true - } -} - -func ControlNameValidator() validator.List { - return &controlNameValidator{} -} diff --git a/datadog/internal/validators/duplicate_requirement_validator.go b/datadog/internal/validators/duplicate_requirement_control_validator.go similarity index 100% rename from datadog/internal/validators/duplicate_requirement_validator.go rename to datadog/internal/validators/duplicate_requirement_control_validator.go From 7d583c8a0c4483431abd4e124c5c51ee26b3016d Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Mon, 26 May 2025 23:45:11 -0500 Subject: [PATCH 53/54] update doc --- docs/resources/compliance_custom_framework.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/compliance_custom_framework.md b/docs/resources/compliance_custom_framework.md index 7d5540133..4fa8d0d37 100644 --- a/docs/resources/compliance_custom_framework.md +++ b/docs/resources/compliance_custom_framework.md @@ -49,9 +49,9 @@ resource "datadog_compliance_custom_framework" "framework" { ### Required -- `handle` (String) The framework handle. String length must be at least 1. +- `handle` (String) The framework handle. String length must be at least 1. This field is immutable. - `name` (String) The framework name. String length must be at least 1. -- `version` (String) The framework version. String length must be at least 1. +- `version` (String) The framework version. String length must be at least 1. This field is immutable. - `requirements` (Block List) The requirements of the framework. Length must be at least 1. (see [below for nested schema](#nestedblock--requirements)) From e0c1c11cf9c42a5aafb4bd9bfa661d5dee604f0c Mon Sep 17 00:00:00 2001 From: Neha Konjeti Date: Tue, 27 May 2025 08:39:00 -0500 Subject: [PATCH 54/54] updateifframework exists casettes --- ...omFramework_UpdateIfFrameworkExists.freeze | 2 +- ...stomFramework_UpdateIfFrameworkExists.yaml | 51 +++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze index 0a62a92b4..1e94fe1ba 100644 --- a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze +++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.freeze @@ -1 +1 @@ -2025-05-22T11:41:25.365854-05:00 \ No newline at end of file +2025-05-27T08:37:45.083914-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml index 9d7a269cf..3411325ac 100644 --- a/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml +++ b/datadog/tests/cassettes/TestCustomFramework_UpdateIfFrameworkExists.yaml @@ -36,7 +36,7 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 432.001125ms + duration: 262.048625ms - id: 1 request: proto: HTTP/1.1 @@ -72,8 +72,41 @@ interactions: - application/vnd.api+json status: 409 Conflict code: 409 - duration: 95.856042ms + duration: 63.423875ms - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/cloud_security_management/custom_frameworks/terraform-handle/1.0 + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: [] + trailer: {} + content_length: 268 + uncompressed: false + body: '{"data":{"id":"terraform-handle-1.0","type":"custom_framework","attributes":{"handle":"terraform-handle","name":"existing-framework","requirements":[{"name":"existing-requirement","controls":[{"name":"existing-control","rules_id":["def-000-be9"]}]}],"version":"1.0"}}}' + headers: + Content-Type: + - application/vnd.api+json + status: 200 OK + code: 200 + duration: 165.03725ms + - id: 3 request: proto: HTTP/1.1 proto_major: 1 @@ -108,8 +141,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 797.741916ms - - id: 3 + duration: 394.019625ms + - id: 4 request: proto: HTTP/1.1 proto_major: 1 @@ -141,8 +174,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 462.0445ms - - id: 4 + duration: 171.630875ms + - id: 5 request: proto: HTTP/1.1 proto_major: 1 @@ -174,8 +207,8 @@ interactions: - application/vnd.api+json status: 200 OK code: 200 - duration: 629.974083ms - - id: 5 + duration: 377.870958ms + - id: 6 request: proto: HTTP/1.1 proto_major: 1 @@ -207,4 +240,4 @@ interactions: - application/vnd.api+json status: 400 Bad Request code: 400 - duration: 85.395041ms + duration: 61.166708ms