From 4e6263c2a2d9c3294fa0c946d00ec91835f80a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Zhou=F0=9F=92=AF?= Date: Fri, 15 Nov 2024 15:33:34 -0500 Subject: [PATCH 01/14] supporting cws multi-policy in terraform --- ...a_source_datadog_csm_threats_agent_rule.go | 22 +- .../data_source_datadog_csm_threats_policy.go | 109 ++++++++ datadog/fwprovider/framework_provider.go | 3 + ...dog_csm_threats_multi_policy_agent_rule.go | 248 ++++++++++++++++++ .../resource_datadog_csm_threats_policy.go | 245 +++++++++++++++++ ...m_threats_multi_policy_agent_rules_test.go | 125 +++++++++ ...ource_datadog_csm_threats_policies_test.go | 107 ++++++++ datadog/tests/provider_test.go | 4 + ...sm_threats_multi_policy_agent_rule_test.go | 134 ++++++++++ ...esource_datadog_csm_threats_policy_test.go | 121 +++++++++ docs/data-sources/csm_threats_agent_rules.md | 4 + docs/data-sources/csm_threats_policies.md | 33 +++ .../csm_threats_multi_policy_agent_rule.md | 31 +++ docs/resources/csm_threats_policy.md | 30 +++ 14 files changed, 1211 insertions(+), 5 deletions(-) create mode 100644 datadog/fwprovider/data_source_datadog_csm_threats_policy.go create mode 100644 datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go create mode 100644 datadog/fwprovider/resource_datadog_csm_threats_policy.go create mode 100644 datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go create mode 100644 datadog/tests/data_source_datadog_csm_threats_policies_test.go create mode 100644 datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go create mode 100644 datadog/tests/resource_datadog_csm_threats_policy_test.go create mode 100644 docs/data-sources/csm_threats_policies.md create mode 100644 docs/resources/csm_threats_multi_policy_agent_rule.md create mode 100644 docs/resources/csm_threats_policy.md diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go b/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go index d6e160a1ab..bbdc98acec 100644 --- a/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go +++ b/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go @@ -25,6 +25,7 @@ type csmThreatsAgentRulesDataSource struct { } type csmThreatsAgentRulesDataSourceModel struct { + PolicyId types.String `tfsdk:"policy_id"` Id types.String `tfsdk:"id"` AgentRulesIds types.List `tfsdk:"agent_rules_ids"` AgentRules []csmThreatsAgentRuleModel `tfsdk:"agent_rules"` @@ -51,7 +52,12 @@ func (r *csmThreatsAgentRulesDataSource) Read(ctx context.Context, request datas return } - res, _, err := r.api.ListCSMThreatsAgentRules(r.auth) + policyId := state.PolicyId.ValueStringPointer() + params := datadogV2.NewListCSMThreatsAgentRulesOptionalParameters() + if !state.PolicyId.IsNull() && !state.PolicyId.IsUnknown() { + params.WithPolicyId(*policyId) + } + res, _, err := r.api.ListCSMThreatsAgentRules(r.auth, *params) if err != nil { response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent rules")) return @@ -75,7 +81,7 @@ func (r *csmThreatsAgentRulesDataSource) Read(ctx context.Context, request datas } stateId := strings.Join(agentRuleIds, "--") - state.Id = types.StringValue(computeAgentRulesDataSourceID(&stateId)) + state.Id = types.StringValue(computeDataSourceID(&stateId)) tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, agentRuleIds) response.Diagnostics.Append(diags...) state.AgentRulesIds = tfAgentRuleIds @@ -84,11 +90,11 @@ func (r *csmThreatsAgentRulesDataSource) Read(ctx context.Context, request datas response.Diagnostics.Append(response.State.Set(ctx, &state)...) } -func computeAgentRulesDataSourceID(agentruleIds *string) string { +func computeDataSourceID(ids *string) string { // Key for hashing var b strings.Builder - if agentruleIds != nil { - b.WriteString(*agentruleIds) + if ids != nil { + b.WriteString(*ids) } keyStr := b.String() h := sha256.New() @@ -101,6 +107,12 @@ func (*csmThreatsAgentRulesDataSource) Schema(_ context.Context, _ datasource.Sc response.Schema = schema.Schema{ Description: "Use this data source to retrieve information about existing Agent rules.", Attributes: map[string]schema.Attribute{ + // Input + "policy_id": schema.StringAttribute{ + Description: "Listing only the rules in the policy with this field as the ID", + Optional: true, + }, + // Output "id": utils.ResourceIDAttribute(), "agent_rules_ids": schema.ListAttribute{ Computed: true, diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_policy.go b/datadog/fwprovider/data_source_datadog_csm_threats_policy.go new file mode 100644 index 0000000000..c8a3455b06 --- /dev/null +++ b/datadog/fwprovider/data_source_datadog_csm_threats_policy.go @@ -0,0 +1,109 @@ +package fwprovider + +import ( + "context" + "strings" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +var ( + _ datasource.DataSourceWithConfigure = &csmThreatsPoliciesDataSource{} +) + +type csmThreatsPoliciesDataSource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +type csmThreatsPoliciesDataSourceModel struct { + Id types.String `tfsdk:"id"` + PolicyIds types.List `tfsdk:"policy_ids"` + Policies []csmThreatsPolicyModel `tfsdk:"policies"` +} + +func NewCSMThreatsPoliciesDataSource() datasource.DataSource { + return &csmThreatsPoliciesDataSource{} +} + +func (r *csmThreatsPoliciesDataSource) Configure(_ context.Context, request datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (*csmThreatsPoliciesDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = "csm_threats_policies" +} + +func (r *csmThreatsPoliciesDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var state csmThreatsPoliciesDataSourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + res, _, err := r.api.ListCSMThreatsAgentPolicies(r.auth) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent rules")) + return + } + + data := res.GetData() + policyIds := make([]string, len(data)) + policies := make([]csmThreatsPolicyModel, len(data)) + + for idx, policy := range res.GetData() { + var policyModel csmThreatsPolicyModel + policyModel.Id = types.StringValue(policy.GetId()) + attributes := policy.Attributes + policyModel.Name = types.StringValue(attributes.GetName()) + policyModel.Description = types.StringValue(attributes.GetDescription()) + policyModel.Enabled = types.BoolValue(attributes.GetEnabled()) + policyModel.Tags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetHostTags()) + policyIds[idx] = policy.GetId() + policies[idx] = policyModel + } + + stateId := strings.Join(policyIds, "--") + state.Id = types.StringValue(computeDataSourceID(&stateId)) + tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, policyIds) + response.Diagnostics.Append(diags...) + state.PolicyIds = tfAgentRuleIds + state.Policies = policies + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (*csmThreatsPoliciesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Use this data source to retrieve information about existing policies.", + Attributes: map[string]schema.Attribute{ + "id": utils.ResourceIDAttribute(), + "policy_ids": schema.ListAttribute{ + Computed: true, + Description: "List of IDs for the policies.", + ElementType: types.StringType, + }, + "policies": schema.ListAttribute{ + Computed: true, + Description: "List of policies", + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "tags": types.SetType{ElemType: types.StringType}, + "name": types.StringType, + "description": types.StringType, + "enabled": types.BoolType, + }, + }, + }, + }, + } +} diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index 15da535fb1..e75490dfa6 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -80,6 +80,8 @@ var Resources = []func() resource.Resource{ NewActionConnectionResource, NewWorkflowAutomationResource, NewAppBuilderAppResource, + NewCSMThreatsPolicyResource, + NewCSMThreatsMultiPolicyAgentRuleResource, } var Datasources = []func() datasource.DataSource{ @@ -108,6 +110,7 @@ var Datasources = []func() datasource.DataSource{ NewDatadogSyntheticsGlobalVariableDataSource, NewWorkflowAutomationDataSource, NewDatadogAppBuilderAppDataSource, + NewCSMThreatsPoliciesDataSource, } // FrameworkProvider struct diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go new file mode 100644 index 0000000000..c0f06e3dac --- /dev/null +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -0,0 +1,248 @@ +package fwprovider + +import ( + "context" + "strings" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/path" + "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/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +type csmThreatsMultiPolicyAgentRuleResource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +type csmThreatsMultiPolicyAgentRuleModel struct { + Id types.String `tfsdk:"id"` + PolicyId types.String `tfsdk:"policy_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + Expression types.String `tfsdk:"expression"` +} + +func NewCSMThreatsMultiPolicyAgentRuleResource() resource.Resource { + return &csmThreatsMultiPolicyAgentRuleResource{} +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "csm_threats_multi_policy_agent_rule" +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Provides a Datadog CSM Threats Agent Rule API resource.", + Attributes: map[string]schema.Attribute{ + "id": utils.ResourceIDAttribute(), + "policy_id": schema.StringAttribute{ + Required: true, + Description: "The ID of the agent policy in which the rule is saved", + }, + "name": schema.StringAttribute{ + Required: true, + Description: "The name of the Agent rule.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Description: "A description for the Agent rule.", + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Required: true, + Description: "Indicates Whether the Agent rule is enabled.", + }, + "expression": schema.StringAttribute{ + Required: true, + Description: "The SECL expression of the Agent rule", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + result := strings.SplitN(request.ID, ":", 2) + if len(result) != 2 { + response.Diagnostics.AddError("error retrieving policy_id or rule_id from given ID", "") + return + } + + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("policy_id"), result[0])...) + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("id"), result[1])...) +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state csmThreatsMultiPolicyAgentRuleModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + agentRulePayload, err := r.buildCreateCSMThreatsAgentRulePayload(&state) + if err != nil { + response.Diagnostics.AddError("error while parsing resource", err.Error()) + } + + res, _, err := r.api.CreateCSMThreatsAgentRule(r.auth, *agentRulePayload) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state csmThreatsMultiPolicyAgentRuleModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + agentRuleId := state.Id.ValueString() + policyId := state.PolicyId.ValueString() + res, httpResponse, err := r.api.GetCSMThreatsAgentRule(r.auth, agentRuleId, *datadogV2.NewGetCSMThreatsAgentRuleOptionalParameters().WithPolicyId(policyId)) + if err != nil { + if httpResponse != nil && httpResponse.StatusCode == 404 { + response.State.RemoveResource(ctx) + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error fetching agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state csmThreatsMultiPolicyAgentRuleModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + agentRulePayload, err := r.buildUpdateCSMThreatsAgentRulePayload(&state) + if err != nil { + response.Diagnostics.AddError("error while parsing resource", err.Error()) + } + + res, _, err := r.api.UpdateCSMThreatsAgentRule(r.auth, state.Id.ValueString(), *agentRulePayload) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state csmThreatsMultiPolicyAgentRuleModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + id := state.Id.ValueString() + policyId := state.PolicyId.ValueString() + httpResp, err := r.api.DeleteCSMThreatsAgentRule(r.auth, id, *datadogV2.NewDeleteCSMThreatsAgentRuleOptionalParameters().WithPolicyId(policyId)) + if err != nil { + if httpResp != nil && httpResp.StatusCode == 404 { + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting agent rule")) + return + } +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) buildCreateCSMThreatsAgentRulePayload(state *csmThreatsMultiPolicyAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleCreateRequest, error) { + _, policyId, name, description, enabled, expression := r.extractAgentRuleAttributesFromResource(state) + + attributes := datadogV2.CloudWorkloadSecurityAgentRuleCreateAttributes{} + attributes.Expression = expression + attributes.Name = name + attributes.Description = description + attributes.Enabled = &enabled + attributes.PolicyId = &policyId + + data := datadogV2.NewCloudWorkloadSecurityAgentRuleCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) + return datadogV2.NewCloudWorkloadSecurityAgentRuleCreateRequest(*data), nil +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) buildUpdateCSMThreatsAgentRulePayload(state *csmThreatsMultiPolicyAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleUpdateRequest, error) { + agentRuleId, policyId, _, description, enabled, _ := r.extractAgentRuleAttributesFromResource(state) + + attributes := datadogV2.CloudWorkloadSecurityAgentRuleUpdateAttributes{} + attributes.Description = description + attributes.Enabled = &enabled + attributes.PolicyId = &policyId + + data := datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) + data.Id = &agentRuleId + return datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateRequest(*data), nil +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) extractAgentRuleAttributesFromResource(state *csmThreatsMultiPolicyAgentRuleModel) (string, string, string, *string, bool, string) { + // Mandatory fields + id := state.Id.ValueString() + policyId := state.PolicyId.ValueString() + name := state.Name.ValueString() + enabled := state.Enabled.ValueBool() + expression := state.Expression.ValueString() + description := state.Description.ValueStringPointer() + + return id, policyId, name, description, enabled, expression +} + +func (r *csmThreatsMultiPolicyAgentRuleResource) updateStateFromResponse(ctx context.Context, state *csmThreatsMultiPolicyAgentRuleModel, res *datadogV2.CloudWorkloadSecurityAgentRuleResponse) { + state.Id = types.StringValue(res.Data.GetId()) + + attributes := res.Data.Attributes + + state.Name = types.StringValue(attributes.GetName()) + state.Description = types.StringValue(attributes.GetDescription()) + state.Enabled = types.BoolValue(attributes.GetEnabled()) + state.Expression = types.StringValue(attributes.GetExpression()) +} diff --git a/datadog/fwprovider/resource_datadog_csm_threats_policy.go b/datadog/fwprovider/resource_datadog_csm_threats_policy.go new file mode 100644 index 0000000000..2569747259 --- /dev/null +++ b/datadog/fwprovider/resource_datadog_csm_threats_policy.go @@ -0,0 +1,245 @@ +package fwprovider + +import ( + "context" + "fmt" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +type csmThreatsPolicyModel struct { + Id types.String `tfsdk:"id"` + Tags types.Set `tfsdk:"tags"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` +} + +type csmThreatsPolicyResource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +func NewCSMThreatsPolicyResource() resource.Resource { + return &csmThreatsPolicyResource{} +} + +func (r *csmThreatsPolicyResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "csm_threats_policy" +} + +func (r *csmThreatsPolicyResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (r *csmThreatsPolicyResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Provides a Datadog CSM Threats policy API resource.", + Attributes: map[string]schema.Attribute{ + "id": utils.ResourceIDAttribute(), + "name": schema.StringAttribute{ + Required: true, + Description: "The name of the policy.", + }, + "description": schema.StringAttribute{ + Optional: true, + Description: "A description for the policy.", + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Optional: true, + Default: booldefault.StaticBool(false), + Description: "Indicates whether the policy is enabled.", + Computed: true, + }, + "tags": schema.SetAttribute{ + Optional: true, + Description: "Host tags that define where the policy is deployed.", + ElementType: types.StringType, + Computed: true, + }, + }, + } +} + +func (r *csmThreatsPolicyResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} + +func (r *csmThreatsPolicyResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state csmThreatsPolicyModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + policyPayload, err := r.buildCreateCSMThreatsPolicyPayload(&state) + if err != nil { + response.Diagnostics.AddError("error while parsing resource", err.Error()) + } + + res, _, err := r.api.CreateCSMThreatsAgentPolicy(r.auth, *policyPayload) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating policy")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsPolicyResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state csmThreatsPolicyModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + policyId := state.Id.ValueString() + res, httpResponse, err := r.api.GetCSMThreatsAgentPolicy(r.auth, policyId) + if err != nil { + if httpResponse != nil && httpResponse.StatusCode == 404 { + response.State.RemoveResource(ctx) + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error fetching agent policy")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsPolicyResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state csmThreatsPolicyModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + policyPayload, err := r.buildUpdateCSMThreatsPolicyPayload(&state) + if err != nil { + response.Diagnostics.AddError("error while parsing resource", err.Error()) + } + + res, _, err := r.api.UpdateCSMThreatsAgentPolicy(r.auth, state.Id.ValueString(), *policyPayload) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsPolicyResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state csmThreatsPolicyModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + id := state.Id.ValueString() + + httpResp, err := r.api.DeleteCSMThreatsAgentPolicy(r.auth, id) + if err != nil { + if httpResp != nil && httpResp.StatusCode == 404 { + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting agent rule")) + return + } +} + +func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyCreateRequest, error) { + _, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + if err != nil { + return nil, err + } + + attributes := datadogV2.CloudWorkloadSecurityAgentPolicyCreateAttributes{} + attributes.Name = name + attributes.Description = description + attributes.Enabled = enabled + attributes.HostTags = tags + + data := datadogV2.NewCloudWorkloadSecurityAgentPolicyCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYTYPE_POLICY) + return datadogV2.NewCloudWorkloadSecurityAgentPolicyCreateRequest(*data), nil +} + +func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyUpdateRequest, error) { + policyId, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + if err != nil { + return nil, err + } + attributes := datadogV2.CloudWorkloadSecurityAgentPolicyUpdateAttributes{} + attributes.Name = &name + attributes.Description = description + attributes.Enabled = enabled + attributes.HostTags = tags + + data := datadogV2.NewCloudWorkloadSecurityAgentPolicyUpdateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYTYPE_POLICY) + data.Id = &policyId + return datadogV2.NewCloudWorkloadSecurityAgentPolicyUpdateRequest(*data), nil +} + +func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, string, *string, *bool, []string, error) { + // Mandatory fields + id := state.Id.ValueString() + name := state.Name.ValueString() + enabled := state.Enabled.ValueBoolPointer() + description := state.Description.ValueStringPointer() + var tags []string + if !state.Tags.IsNull() && !state.Tags.IsUnknown() { + for _, tag := range state.Tags.Elements() { + tagStr, ok := tag.(types.String) + if !ok { + return "", "", nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) + } + tags = append(tags, tagStr.ValueString()) + } + } + + return id, name, description, enabled, tags, nil +} + +func (r *csmThreatsPolicyResource) updateStateFromResponse(ctx context.Context, state *csmThreatsPolicyModel, res *datadogV2.CloudWorkloadSecurityAgentPolicyResponse) { + state.Id = types.StringValue(res.Data.GetId()) + + attributes := res.Data.Attributes + + state.Name = types.StringValue(attributes.GetName()) + state.Description = types.StringValue(attributes.GetDescription()) + state.Enabled = types.BoolValue(attributes.GetEnabled()) + state.Tags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetHostTags()) +} diff --git a/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go b/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go new file mode 100644 index 0000000000..3db6f11767 --- /dev/null +++ b/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go @@ -0,0 +1,125 @@ +package test + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + + "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 TestAccCSMThreatsMultiPolicyAgentRuleDataSource(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + policyName := uniqueAgentRuleName(ctx) + policyConfig := fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_for_test" { + name = "%s" + enabled = true + description = "im a policy" + tags = ["host_name:test_host"] + } + `, policyName) + agentRuleName := uniqueAgentRuleName(ctx) + agentRuleConfig := fmt.Sprintf(` + %s + resource "datadog_csm_threats_multi_policy_agent_rule" "agent_rule_for_data_source_test" { + name = "%s" + policy_id = datadog_csm_threats_policy.policy_for_test.id + enabled = true + description = "im a rule" + expression = "open.file.name == \"etc/shadow/password\"" + } + `, policyConfig, agentRuleName) + dataSourceName := "data.datadog_csm_threats_agent_rules.my_data_source" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsAgentRuleDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + // Create an agent rule to have at least one + Config: agentRuleConfig, + Check: testAccCheckCSMThreatsMultiPolicyAgentRuleExists(providers.frameworkProvider, "datadog_csm_threats_multi_policy_agent_rule.agent_rule_for_data_source_test"), + }, + { + Config: fmt.Sprintf(` + %s + data "datadog_csm_threats_agent_rules" "my_data_source" { + policy_id = datadog_csm_threats_policy.policy_for_test.id + } + `, agentRuleConfig), + Check: checkCSMThreatsMultiPolicyAgentRulesDataSourceContent(providers.frameworkProvider, dataSourceName, agentRuleName), + }, + }, + }) +} + +func checkCSMThreatsMultiPolicyAgentRulesDataSourceContent(accProvider *fwprovider.FrameworkProvider, dataSourceName string, agentRuleName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + res, ok := state.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("resource missing from state: %s", dataSourceName) + } + + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + policyId := res.Primary.Attributes["policy_id"] + allAgentRulesResponse, _, err := apiInstances.GetCSMThreatsApiV2().ListCSMThreatsAgentRules(auth, *datadogV2.NewListCSMThreatsAgentRulesOptionalParameters().WithPolicyId(policyId)) + if err != nil { + return err + } + + // Check the agentRule we created is in the API response + agentRuleId := "" + ruleName := "" + for _, rule := range allAgentRulesResponse.GetData() { + if rule.Attributes.GetName() == agentRuleName { + agentRuleId = rule.GetId() + ruleName = rule.Attributes.GetName() + break + } + } + if agentRuleId == "" { + return fmt.Errorf("agent rule with name '%s' not found in API responses", agentRuleName) + } + + // Check that the data_source fetched is correct + resourceAttributes := res.Primary.Attributes + agentRulesIdsCount, err := strconv.Atoi(resourceAttributes["agent_rules_ids.#"]) + if err != nil { + return err + } + agentRulesCount, err := strconv.Atoi(resourceAttributes["agent_rules.#"]) + if err != nil { + return err + } + if agentRulesCount != agentRulesIdsCount { + return fmt.Errorf("the data source contains %d agent rules IDs but %d agent rules", agentRulesIdsCount, agentRulesCount) + } + + // Find in which position is the agent rule we created, and check its values + idx := 0 + for idx < agentRulesIdsCount && resourceAttributes[fmt.Sprintf("agent_rules_ids.%d", idx)] != agentRuleId { + idx++ + } + if idx == len(resourceAttributes) { + return fmt.Errorf("agent rule with ID '%s' not found in data source", agentRuleId) + } + + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.name", idx), ruleName), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.enabled", idx), "true"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.description", idx), "im a rule"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.expression", idx), "open.file.name == \"etc/shadow/password\""), + )(state) + } +} diff --git a/datadog/tests/data_source_datadog_csm_threats_policies_test.go b/datadog/tests/data_source_datadog_csm_threats_policies_test.go new file mode 100644 index 0000000000..15a9f322d4 --- /dev/null +++ b/datadog/tests/data_source_datadog_csm_threats_policies_test.go @@ -0,0 +1,107 @@ +package test + +import ( + "context" + "fmt" + "strconv" + "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 TestAccCSMThreatsPoliciesDataSource(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + policyName := uniqueAgentRuleName(ctx) + dataSourceName := "data.datadog_csm_threats_policies.my_data_source" + policyConfig := fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_for_data_source_test" { + name = "%s" + enabled = true + description = "im a policy" + tags = ["host_name:test_host"] + } + `, policyName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsPolicyDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + // Create a policy to have at least one + Config: policyConfig, + Check: testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, "datadog_csm_threats_policy.policy_for_data_source_test"), + }, + { + Config: fmt.Sprintf(` + %s + data "datadog_csm_threats_policies" "my_data_source" {} + `, policyConfig), + Check: checkCSMThreatsPoliciesDataSourceContent(providers.frameworkProvider, dataSourceName, policyName), + }, + }, + }) +} + +func checkCSMThreatsPoliciesDataSourceContent(accProvider *fwprovider.FrameworkProvider, dataSourceName string, policyName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + res, ok := state.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("resource missing from state: %s", dataSourceName) + } + + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + allPoliciesResponse, _, err := apiInstances.GetCSMThreatsApiV2().ListCSMThreatsAgentPolicies(auth) + if err != nil { + return err + } + + // Check the policy we created is in the API response + resPolicyId := "" + for _, policy := range allPoliciesResponse.GetData() { + if policy.Attributes.GetName() == policyName { + resPolicyId = policy.GetId() + break + } + } + if resPolicyId == "" { + return fmt.Errorf("policy with name '%s' not found in API responses", policyName) + } + + // Check that the data_source fetched is correct + resourceAttributes := res.Primary.Attributes + policyIdsCount, err := strconv.Atoi(resourceAttributes["policy_ids.#"]) + if err != nil { + return err + } + policiesCount, err := strconv.Atoi(resourceAttributes["policies.#"]) + if err != nil { + return err + } + if policiesCount != policyIdsCount { + return fmt.Errorf("the data source contains %d policy IDs but %d policies", policyIdsCount, policiesCount) + } + + // Find in which position is the policy we created, and check its values + idx := 0 + for idx < policyIdsCount && resourceAttributes[fmt.Sprintf("policy_ids.%d", idx)] != resPolicyId { + idx++ + } + if idx == len(resourceAttributes) { + return fmt.Errorf("policy with ID '%s' not found in data source", resPolicyId) + } + + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.name", idx), policyName), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.enabled", idx), "true"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.tags.0", idx), "host_name:test_host"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.description", idx), "im a policy"), + )(state) + } +} diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go index b334d9a20a..f68845babd 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -59,6 +59,8 @@ var testFiles2EndpointTags = map[string]string{ "tests/data_source_datadog_cloud_workload_security_agent_rules_test": "cloud-workload-security", "tests/data_source_datadog_action_connection_test": "action_connection", "tests/data_source_datadog_csm_threats_agent_rules_test": "cloud-workload-security", + "tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test": "cloud-workload-security", + "tests/data_source_datadog_csm_threats_policies_test": "cloud-workload-security", "tests/data_source_datadog_dashboard_list_test": "dashboard-lists", "tests/data_source_datadog_dashboard_test": "dashboard", "tests/data_source_datadog_hosts_test": "hosts", @@ -119,6 +121,8 @@ var testFiles2EndpointTags = map[string]string{ "tests/resource_datadog_cloud_workload_security_agent_rule_test": "cloud_workload_security", "tests/resource_datadog_action_connection_test": "action_connection", "tests/resource_datadog_csm_threats_agent_rule_test": "cloud-workload-security", + "tests/resource_datadog_csm_threats_multi_policy_agent_rule_test": "cloud-workload-security", + "tests/resource_datadog_csm_threats_policy_test": "cloud-workload-security", "tests/resource_datadog_dashboard_alert_graph_test": "dashboards", "tests/resource_datadog_dashboard_alert_value_test": "dashboards", "tests/resource_datadog_dashboard_change_test": "dashboards", diff --git a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go new file mode 100644 index 0000000000..8c5b518f01 --- /dev/null +++ b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go @@ -0,0 +1,134 @@ +package test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" +) + +// Create an agent rule and update its description +func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + agentRuleName := uniqueAgentRuleName(ctx) + resourceName := "datadog_csm_threats_multi_policy_agent_rule.agent_rule_test" + + policyName := uniqueAgentRuleName(ctx) + policyConfig := fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_for_test" { + name = "%s" + enabled = true + description = "im a policy" + tags = ["host_name:test_host"] + } + `, policyName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsMultiPolicyAgentRuleDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + // Create a policy to have at least one + Config: policyConfig, + Check: testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, "datadog_csm_threats_policy.policy_for_test"), + }, + { + Config: fmt.Sprintf(` + %s + resource "datadog_csm_threats_multi_policy_agent_rule" "agent_rule_test" { + name = "%s" + policy_id = datadog_csm_threats_policy.policy_for_test.id + enabled = true + description = "im a rule" + expression = "open.file.name == \"etc/shadow/password\"" + } + `, policyConfig, agentRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsMultiPolicyAgentRuleExists(providers.frameworkProvider, resourceName), + checkCSMThreatsAgentRuleContent( + resourceName, + agentRuleName, + "im a rule", + "open.file.name == \"etc/shadow/password\"", + ), + ), + }, + // Update description + { + Config: fmt.Sprintf(` + %s + resource "datadog_csm_threats_multi_policy_agent_rule" "agent_rule_test" { + name = "%s" + policy_id = datadog_csm_threats_policy.policy_for_test.id + enabled = true + description = "updated agent rule for terraform provider test" + expression = "open.file.name == \"etc/shadow/password\"" + } + `, policyConfig, agentRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsMultiPolicyAgentRuleExists(providers.frameworkProvider, resourceName), + checkCSMThreatsAgentRuleContent( + resourceName, + agentRuleName, + "updated agent rule for terraform provider test", + "open.file.name == \"etc/shadow/password\"", + ), + ), + }, + }, + }) +} + +func testAccCheckCSMThreatsMultiPolicyAgentRuleExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource '%s' not found in the state %s", resourceName, s.RootModule().Resources) + } + + if resource.Type != "datadog_csm_threats_multi_policy_agent_rule" { + return fmt.Errorf("resource %s is not of type datadog_csm_threats_multi_policy_agent_rule, found %s instead", resourceName, resource.Type) + } + + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + policyId := resource.Primary.Attributes["policy_id"] + _, _, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentRule(auth, resource.Primary.ID, *datadogV2.NewGetCSMThreatsAgentRuleOptionalParameters().WithPolicyId(policyId)) + if err != nil { + return fmt.Errorf("received an error retrieving agent rule: %s", err) + } + + return nil + } +} + +func testAccCheckCSMThreatsMultiPolicyAgentRuleDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { + return func(s *terraform.State) error { + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + for _, resource := range s.RootModule().Resources { + if resource.Type == "datadog_csm_threats_multi_policy_agent_rule" { + policyId := resource.Primary.Attributes["policy_id"] + _, httpResponse, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentRule(auth, resource.Primary.ID, *datadogV2.NewGetCSMThreatsAgentRuleOptionalParameters().WithPolicyId(policyId)) + if err == nil { + return errors.New("agent rule still exists") + } + if httpResponse == nil || httpResponse.StatusCode != 404 { + return fmt.Errorf("received an error while getting the agent rule: %s", err) + } + } + } + + return nil + } +} diff --git a/datadog/tests/resource_datadog_csm_threats_policy_test.go b/datadog/tests/resource_datadog_csm_threats_policy_test.go new file mode 100644 index 0000000000..4e9099c01a --- /dev/null +++ b/datadog/tests/resource_datadog_csm_threats_policy_test.go @@ -0,0 +1,121 @@ +package test + +import ( + "context" + "errors" + "fmt" + "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" +) + +// Create an agent policy and update its description +func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + policyName := uniqueAgentRuleName(ctx) + resourceName := "datadog_csm_threats_policy.policy_test" + tags := []string{"host_name:test_host"} + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsPolicyDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_test" { + name = "%s" + enabled = true + description = "im a policy" + tags = ["host_name:test_host"] + } + `, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, "datadog_csm_threats_policy.policy_test"), + checkCSMThreatsPolicyContent( + resourceName, + policyName, + "im a policy", + tags, + ), + ), + }, + // Update description + { + Config: fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_test" { + name = "%s" + enabled = true + description = "updated policy for terraform provider test" + tags = ["host_name:test_host"] + } + `, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, resourceName), + checkCSMThreatsPolicyContent( + resourceName, + policyName, + "updated policy for terraform provider test", + tags, + ), + ), + }, + }, + }) +} + +func checkCSMThreatsPolicyContent(resourceName string, name string, description string, tags []string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "tags.0", tags[0]), + ) +} + +func testAccCheckCSMThreatsPolicyExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource '%s' not found in the state %s", resourceName, s.RootModule().Resources) + } + + if resource.Type != "datadog_csm_threats_policy" { + return fmt.Errorf("resource %s is not of type datadog_csm_threats_policy, found %s instead", resourceName, resource.Type) + } + + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + _, _, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentPolicy(auth, resource.Primary.ID) + if err != nil { + return fmt.Errorf("received an error retrieving policy: %s", err) + } + + return nil + } +} + +func testAccCheckCSMThreatsPolicyDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { + return func(s *terraform.State) error { + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + for _, resource := range s.RootModule().Resources { + if resource.Type == "datadog_csm_threats_policy" { + _, httpResponse, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentPolicy(auth, resource.Primary.ID) + if err == nil { + return errors.New("policy still exists") + } + if httpResponse == nil || httpResponse.StatusCode != 404 { + return fmt.Errorf("received an error while getting the policy: %s", err) + } + } + } + + return nil + } +} diff --git a/docs/data-sources/csm_threats_agent_rules.md b/docs/data-sources/csm_threats_agent_rules.md index 6e6e7a0d19..6f5ea6e33a 100644 --- a/docs/data-sources/csm_threats_agent_rules.md +++ b/docs/data-sources/csm_threats_agent_rules.md @@ -15,6 +15,10 @@ Use this data source to retrieve information about existing Agent rules. ## Schema +### Optional + +- `policy_id` (String) Listing only the rules in the policy with this field as the ID + ### Read-Only - `agent_rules` (List of Object) List of Agent rules (see [below for nested schema](#nestedatt--agent_rules)) diff --git a/docs/data-sources/csm_threats_policies.md b/docs/data-sources/csm_threats_policies.md new file mode 100644 index 0000000000..46ae797871 --- /dev/null +++ b/docs/data-sources/csm_threats_policies.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_csm_threats_policies Data Source - terraform-provider-datadog" +subcategory: "" +description: |- + Use this data source to retrieve information about existing policies. +--- + +# datadog_csm_threats_policies (Data Source) + +Use this data source to retrieve information about existing policies. + + + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `policies` (List of Object) List of policies (see [below for nested schema](#nestedatt--policies)) +- `policy_ids` (List of String) List of IDs for the policies. + + +### Nested Schema for `policies` + +Read-Only: + +- `description` (String) +- `enabled` (Boolean) +- `id` (String) +- `name` (String) +- `tags` (Set of String) diff --git a/docs/resources/csm_threats_multi_policy_agent_rule.md b/docs/resources/csm_threats_multi_policy_agent_rule.md new file mode 100644 index 0000000000..84d0a57714 --- /dev/null +++ b/docs/resources/csm_threats_multi_policy_agent_rule.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_csm_threats_multi_policy_agent_rule Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Provides a Datadog CSM Threats Agent Rule API resource. +--- + +# datadog_csm_threats_multi_policy_agent_rule (Resource) + +Provides a Datadog CSM Threats Agent Rule API resource. + + + + +## Schema + +### Required + +- `enabled` (Boolean) Indicates Whether the Agent rule is enabled. +- `expression` (String) The SECL expression of the Agent rule +- `name` (String) The name of the Agent rule. +- `policy_id` (String) The ID of the agent policy in which the rule is saved + +### Optional + +- `description` (String) A description for the Agent rule. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/csm_threats_policy.md b/docs/resources/csm_threats_policy.md new file mode 100644 index 0000000000..ac2c43b584 --- /dev/null +++ b/docs/resources/csm_threats_policy.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_csm_threats_policy Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Provides a Datadog CSM Threats policy API resource. +--- + +# datadog_csm_threats_policy (Resource) + +Provides a Datadog CSM Threats policy API resource. + + + + +## Schema + +### Required + +- `name` (String) The name of the policy. + +### Optional + +- `description` (String) A description for the policy. +- `enabled` (Boolean) Indicates whether the policy is enabled. Defaults to `false`. +- `tags` (Set of String) Host tags that define where the policy is deployed. + +### Read-Only + +- `id` (String) The ID of this resource. From c7bf322320ef0891ff44674e7d3206504271567e Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Thu, 30 Jan 2025 14:50:03 +0100 Subject: [PATCH 02/14] supporting cws policies_list --- datadog/fwprovider/framework_provider.go | 1 + ...urce_datadog_csm_threats_multi_policies.go | 258 ++++++++++++++++++ .../resource_datadog_csm_threats_policy.go | 17 +- docs/resources/csm_threats_policies_list.md | 36 +++ docs/resources/csm_threats_policy.md | 5 +- 5 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go create mode 100644 docs/resources/csm_threats_policies_list.md diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index e75490dfa6..316197e514 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -80,6 +80,7 @@ var Resources = []func() resource.Resource{ NewActionConnectionResource, NewWorkflowAutomationResource, NewAppBuilderAppResource, + NewCSMThreatsPoliciesListResource, NewCSMThreatsPolicyResource, NewCSMThreatsMultiPolicyAgentRuleResource, } diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go new file mode 100644 index 0000000000..91622452c1 --- /dev/null +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go @@ -0,0 +1,258 @@ +package fwprovider + +import ( + "context" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "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.ResourceWithConfigure = &csmThreatsPoliciesListResource{} + _ resource.ResourceWithImportState = &csmThreatsPoliciesListResource{} +) + +type csmThreatsPoliciesListResource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +type csmThreatsPoliciesListModel struct { + ID types.String `tfsdk:"id"` + Entries []csmThreatsPoliciesListEntryModel `tfsdk:"entries"` +} + +type csmThreatsPoliciesListEntryModel struct { + PolicyID types.String `tfsdk:"policy_id"` + Name types.String `tfsdk:"name"` + Priority types.Int64 `tfsdk:"priority"` +} + +func NewCSMThreatsPoliciesListResource() resource.Resource { + return &csmThreatsPoliciesListResource{} +} + +func (r *csmThreatsPoliciesListResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "csm_threats_policies_list" +} + +func (r *csmThreatsPoliciesListResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (r *csmThreatsPoliciesListResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} + +func (r *csmThreatsPoliciesListResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Provides a Datadog CSM Threats policies API resource.", + Attributes: map[string]schema.Attribute{ + "id": utils.ResourceIDAttribute(), + }, + Blocks: map[string]schema.Block{ + "entries": schema.SetNestedBlock{ + Description: "A set of policies that belong to this list/batch. All non-listed policies get deleted.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "policy_id": schema.StringAttribute{ + Description: "The ID of the policy to manage (from `csm_threats_policy`).", + Required: true, + }, + "priority": schema.Int64Attribute{ + Description: "The priority of the policy in this list.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "Optional name. If omitted, fallback to the policy_id as name.", + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (r *csmThreatsPoliciesListResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var plan csmThreatsPoliciesListModel + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + if response.Diagnostics.HasError() { + return + } + + plan.ID = types.StringValue("policies_list") + + updatedEntries, err := r.applyBatchPolicies(ctx, plan.Entries, &response.Diagnostics) + if err != nil { + return + } + + plan.Entries = updatedEntries + response.Diagnostics.Append(response.State.Set(ctx, &plan)...) +} + +func (r *csmThreatsPoliciesListResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state csmThreatsPoliciesListModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + if state.ID.IsUnknown() || state.ID.IsNull() || state.ID.ValueString() == "" { + response.State.RemoveResource(ctx) + return + } + + listResponse, httpResp, err := r.api.ListCSMThreatsAgentPolicies(r.auth) + if err != nil { + if httpResp != nil && httpResp.StatusCode == 404 { + response.State.RemoveResource(ctx) + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent policies")) + return + } + + newEntries := make([]csmThreatsPoliciesListEntryModel, 0) + for _, policyData := range listResponse.GetData() { + policyID := policyData.GetId() + if policyID == "CWS_DD" { + continue + } + attributes := policyData.Attributes + + name := attributes.GetName() + priorirty := attributes.GetPriority() + + entry := csmThreatsPoliciesListEntryModel{ + PolicyID: types.StringValue(policyID), + Name: types.StringValue(name), + Priority: types.Int64Value(int64(priorirty)), + } + newEntries = append(newEntries, entry) + } + + state.Entries = newEntries + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsPoliciesListResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var plan csmThreatsPoliciesListModel + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + if response.Diagnostics.HasError() { + return + } + + updatedEntries, err := r.applyBatchPolicies(ctx, plan.Entries, &response.Diagnostics) + if err != nil { + return + } + + plan.Entries = updatedEntries + response.Diagnostics.Append(response.State.Set(ctx, &plan)...) +} + +func (r *csmThreatsPoliciesListResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + _, err := r.applyBatchPolicies(ctx, []csmThreatsPoliciesListEntryModel{}, &response.Diagnostics) + if err != nil { + return + } + response.State.RemoveResource(ctx) +} + +func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, entries []csmThreatsPoliciesListEntryModel, diags *diag.Diagnostics) ([]csmThreatsPoliciesListEntryModel, error) { + listResp, httpResp, err := r.api.ListCSMThreatsAgentPolicies(r.auth) + if err != nil { + if httpResp != nil && httpResp.StatusCode == 404 { + diags.Append(utils.FrameworkErrorDiag(err, "error while fetching agent policies")) + return nil, err + } + } + + existingPolicies := make(map[string]struct{}) + for _, policy := range listResp.GetData() { + if policy.GetId() == "CWS_DD" { + continue + } + existingPolicies[policy.GetId()] = struct{}{} + } + + var batchItems []datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems + + for i := range entries { + policyID := entries[i].PolicyID.ValueString() + name := entries[i].Name.ValueString() + + if name == "" { + name = policyID + entries[i].Name = types.StringValue(name) + } + priority := entries[i].Priority.ValueInt64() + + item := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ + Id: &policyID, + Name: &name, + Priority: &priority, + } + + batchItems = append(batchItems, item) + delete(existingPolicies, policyID) + } + + for policyID := range existingPolicies { + DeleteTrue := true + item := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ + Id: &policyID, + Delete: &DeleteTrue, + } + batchItems = append(batchItems, item) + } + + patchID := "batch_update_req" + typ := datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYBATCHUPDATEDATATYPE_POLICIES + attributes := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateAttributes() + attributes.SetPolicies(batchItems) + data := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateData(*attributes, patchID, typ) + batchReq := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateRequest(*data) + + response, _, err := r.api.BatchUpdateCSMThreatsAgentPolicy( + r.auth, + *batchReq, + ) + if err != nil { + diags.Append(utils.FrameworkErrorDiag(err, "error while applying batch policies")) + return nil, err + } + + finalEntries := make([]csmThreatsPoliciesListEntryModel, 0) + for _, policy := range response.GetData() { + policyID := policy.GetId() + attributes := policy.Attributes + + name := "" + if attributes.GetName() == "" { + name = policyID + } + name = attributes.GetName() + priority := attributes.GetPriority() + + entry := csmThreatsPoliciesListEntryModel{ + PolicyID: types.StringValue(policyID), + Name: types.StringValue(name), + Priority: types.Int64Value(int64(priority)), + } + finalEntries = append(finalEntries, entry) + } + + return finalEntries, nil +} diff --git a/datadog/fwprovider/resource_datadog_csm_threats_policy.go b/datadog/fwprovider/resource_datadog_csm_threats_policy.go index 2569747259..a1f960770f 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_policy.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_policy.go @@ -3,6 +3,7 @@ package fwprovider import ( "context" "fmt" + mathrand "math/rand" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/path" @@ -47,7 +48,7 @@ func (r *csmThreatsPolicyResource) Schema(_ context.Context, _ resource.SchemaRe Attributes: map[string]schema.Attribute{ "id": utils.ResourceIDAttribute(), "name": schema.StringAttribute{ - Required: true, + Computed: true, Description: "The name of the policy.", }, "description": schema.StringAttribute{ @@ -182,13 +183,13 @@ func (r *csmThreatsPolicyResource) Delete(ctx context.Context, request resource. } func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyCreateRequest, error) { - _, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + _, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) if err != nil { return nil, err } attributes := datadogV2.CloudWorkloadSecurityAgentPolicyCreateAttributes{} - attributes.Name = name + attributes.Name = fmt.Sprintf("policy-%d", mathrand.Intn(1000)) attributes.Description = description attributes.Enabled = enabled attributes.HostTags = tags @@ -198,12 +199,11 @@ func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csm } func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyUpdateRequest, error) { - policyId, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + policyId, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) if err != nil { return nil, err } attributes := datadogV2.CloudWorkloadSecurityAgentPolicyUpdateAttributes{} - attributes.Name = &name attributes.Description = description attributes.Enabled = enabled attributes.HostTags = tags @@ -213,10 +213,9 @@ func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csm return datadogV2.NewCloudWorkloadSecurityAgentPolicyUpdateRequest(*data), nil } -func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, string, *string, *bool, []string, error) { +func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, *string, *bool, []string, error) { // Mandatory fields id := state.Id.ValueString() - name := state.Name.ValueString() enabled := state.Enabled.ValueBoolPointer() description := state.Description.ValueStringPointer() var tags []string @@ -224,13 +223,13 @@ func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *cs for _, tag := range state.Tags.Elements() { tagStr, ok := tag.(types.String) if !ok { - return "", "", nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) + return "", nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) } tags = append(tags, tagStr.ValueString()) } } - return id, name, description, enabled, tags, nil + return id, description, enabled, tags, nil } func (r *csmThreatsPolicyResource) updateStateFromResponse(ctx context.Context, state *csmThreatsPolicyModel, res *datadogV2.CloudWorkloadSecurityAgentPolicyResponse) { diff --git a/docs/resources/csm_threats_policies_list.md b/docs/resources/csm_threats_policies_list.md new file mode 100644 index 0000000000..1145484806 --- /dev/null +++ b/docs/resources/csm_threats_policies_list.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_csm_threats_policies_list Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Provides a Datadog CSM Threats policies API resource. +--- + +# datadog_csm_threats_policies_list (Resource) + +Provides a Datadog CSM Threats policies API resource. + + + + +## Schema + +### Optional + +- `entries` (Block Set) A set of policies that belong to this list/batch. All non-listed policies get deleted. (see [below for nested schema](#nestedblock--entries)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `entries` + +Required: + +- `policy_id` (String) The ID of the policy to manage (from `csm_threats_policy`). +- `priority` (Number) The priority of the policy in this list. + +Optional: + +- `name` (String) Optional name. If omitted, fallback to the policy_id as name. diff --git a/docs/resources/csm_threats_policy.md b/docs/resources/csm_threats_policy.md index ac2c43b584..85b84a448c 100644 --- a/docs/resources/csm_threats_policy.md +++ b/docs/resources/csm_threats_policy.md @@ -15,10 +15,6 @@ Provides a Datadog CSM Threats policy API resource. ## Schema -### Required - -- `name` (String) The name of the policy. - ### Optional - `description` (String) A description for the policy. @@ -28,3 +24,4 @@ Provides a Datadog CSM Threats policy API resource. ### Read-Only - `id` (String) The ID of this resource. +- `name` (String) The name of the policy. From 1bec8ef75753bae081a52723b032bca3f1842f0e Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Fri, 14 Feb 2025 14:03:03 +0100 Subject: [PATCH 03/14] add test file for policies_list --- ...urce_datadog_csm_threats_multi_policies.go | 4 +- datadog/tests/provider_test.go | 1 + ..._datadog_csm_threats_policies_list_test.go | 155 ++++++++++++++++++ ...esource_datadog_csm_threats_policy_test.go | 19 +-- docs/resources/csm_threats_policies_list.md | 4 +- 5 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 datadog/tests/resource_datadog_csm_threats_policies_list_test.go diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go index 91622452c1..ac4491be63 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go @@ -60,11 +60,11 @@ func (r *csmThreatsPoliciesListResource) Schema(_ context.Context, _ resource.Sc }, Blocks: map[string]schema.Block{ "entries": schema.SetNestedBlock{ - Description: "A set of policies that belong to this list/batch. All non-listed policies get deleted.", + Description: "A set of policies that belong to this list. Only one policies_list resource can be defined in Terraform, containing all unique policies. All non-listed policies get deleted.", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "policy_id": schema.StringAttribute{ - Description: "The ID of the policy to manage (from `csm_threats_policy`).", + Description: "The ID of the policy to manage (from csm_threats_policy).", Required: true, }, "priority": schema.Int64Attribute{ diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go index f68845babd..0b81a14232 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -123,6 +123,7 @@ var testFiles2EndpointTags = map[string]string{ "tests/resource_datadog_csm_threats_agent_rule_test": "cloud-workload-security", "tests/resource_datadog_csm_threats_multi_policy_agent_rule_test": "cloud-workload-security", "tests/resource_datadog_csm_threats_policy_test": "cloud-workload-security", + "tests/resource_datadog_csm_threats_policies_list_test": "cloud-workload-security", "tests/resource_datadog_dashboard_alert_graph_test": "dashboards", "tests/resource_datadog_dashboard_alert_value_test": "dashboards", "tests/resource_datadog_dashboard_change_test": "dashboards", diff --git a/datadog/tests/resource_datadog_csm_threats_policies_list_test.go b/datadog/tests/resource_datadog_csm_threats_policies_list_test.go new file mode 100644 index 0000000000..bef4c484b9 --- /dev/null +++ b/datadog/tests/resource_datadog_csm_threats_policies_list_test.go @@ -0,0 +1,155 @@ +package test + +import ( + "context" + "fmt" + "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" +) + +// Create a policies_list and update the name and priority of its policy +func TestAccCSMThreatsPoliciesList_CreateAndUpdate(t *testing.T) { + _, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + resourceName := "datadog_csm_threats_policies_list.all" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsPoliciesListDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + Config: testAccCSMThreatsPoliciesListConfigBasic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsPoliciesListExists(providers.frameworkProvider, resourceName), + resource.TestCheckResourceAttr(resourceName, "entries.#", "2"), + resource.TestCheckResourceAttr(resourceName, "entries.0.name", "TERRAFORM_POLICY1"), + resource.TestCheckResourceAttr(resourceName, "entries.0.priority", "2"), + resource.TestCheckResourceAttr(resourceName, "entries.1.name", "TERRAFORM_POLICY2"), + resource.TestCheckResourceAttr(resourceName, "entries.1.priority", "3"), + ), + }, + { + Config: testAccCSMThreatsPoliciesListConfigUpdate(), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsPoliciesListExists(providers.frameworkProvider, resourceName), + resource.TestCheckResourceAttr(resourceName, "entries.#", "2"), + resource.TestCheckResourceAttr(resourceName, "entries.0.name", "TERRAFORM_POLICY1"), + resource.TestCheckResourceAttr(resourceName, "entries.0.priority", "2"), + resource.TestCheckResourceAttr(resourceName, "entries.1.name", "TERRAFORM_POLICY2 UPDATED"), + resource.TestCheckResourceAttr(resourceName, "entries.1.priority", "5"), + ), + }, + }, + }) +} + +func testAccCheckCSMThreatsPoliciesListExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource '%s' not found in state", resourceName) + } + if rs.Type != "datadog_csm_threats_policies_list" { + return fmt.Errorf( + "resource %s is not a datadog_csm_threats_policies_list, got: %s", + resourceName, + rs.Type, + ) + } + + if rs.Primary.ID != "policies_list" { + return fmt.Errorf("expected resource ID to be 'policies_list', got %s", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckCSMThreatsPoliciesListDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { + return func(s *terraform.State) error { + apiInstances := accProvider.DatadogApiInstances + auth := accProvider.Auth + + for _, r := range s.RootModule().Resources { + if r.Type != "datadog_csm_threats_policies_list" { + continue + } + + resp, httpResponse, err := apiInstances.GetCSMThreatsApiV2().ListCSMThreatsAgentPolicies(auth) + if err != nil { + if httpResponse != nil && httpResponse.StatusCode == 404 { + return nil + } + return fmt.Errorf("Received an error while listing the policies: %s", err) + } + + if len(resp.GetData()) > 1 { // CWS_DD is always present + return fmt.Errorf("Policies list not empty, some policies are still present") + } + } + return nil + } +} + +func testAccCSMThreatsPoliciesListConfigBasic() string { + return ` + resource "datadog_csm_threats_policy" "policy1" { + description = "created with terraform" + enabled = false + tags = [] + } + + resource "datadog_csm_threats_policy" "policy2" { + description = "created with terraform 2" + enabled = true + tags = ["env:staging"] + } + + resource "datadog_csm_threats_policies_list" "all" { + entries { + policy_id = datadog_csm_threats_policy.policy1.id + name = "TERRAFORM_POLICY1" + priority = 2 + } + entries { + policy_id = datadog_csm_threats_policy.policy2.id + name = "TERRAFORM_POLICY2" + priority = 3 + } + } + ` +} + +func testAccCSMThreatsPoliciesListConfigUpdate() string { + return ` + resource "datadog_csm_threats_policy" "policy1" { + description = "created with terraform" + enabled = false + tags = [] + } + + resource "datadog_csm_threats_policy" "policy2" { + description = "created with terraform 2" + enabled = true + tags = ["env:staging"] + } + + resource "datadog_csm_threats_policies_list" "all" { + entries { + policy_id = datadog_csm_threats_policy.policy1.id + name = "TERRAFORM_POLICY1" + priority = 2 + } + entries { + policy_id = datadog_csm_threats_policy.policy2.id + name = "TERRAFORM_POLICY2 UPDATED" + priority = 5 + } + } + ` +} diff --git a/datadog/tests/resource_datadog_csm_threats_policy_test.go b/datadog/tests/resource_datadog_csm_threats_policy_test.go index 4e9099c01a..87483539d2 100644 --- a/datadog/tests/resource_datadog_csm_threats_policy_test.go +++ b/datadog/tests/resource_datadog_csm_threats_policy_test.go @@ -14,9 +14,8 @@ import ( // Create an agent policy and update its description func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { - ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + _, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - policyName := uniqueAgentRuleName(ctx) resourceName := "datadog_csm_threats_policy.policy_test" tags := []string{"host_name:test_host"} resource.Test(t, resource.TestCase{ @@ -25,19 +24,17 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { CheckDestroy: testAccCheckCSMThreatsPolicyDestroy(providers.frameworkProvider), Steps: []resource.TestStep{ { - Config: fmt.Sprintf(` + Config: ` resource "datadog_csm_threats_policy" "policy_test" { - name = "%s" enabled = true description = "im a policy" tags = ["host_name:test_host"] } - `, policyName), + `, Check: resource.ComposeTestCheckFunc( testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, "datadog_csm_threats_policy.policy_test"), checkCSMThreatsPolicyContent( resourceName, - policyName, "im a policy", tags, ), @@ -45,19 +42,17 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { }, // Update description { - Config: fmt.Sprintf(` + Config: ` resource "datadog_csm_threats_policy" "policy_test" { - name = "%s" enabled = true description = "updated policy for terraform provider test" tags = ["host_name:test_host"] } - `, policyName), + `, Check: resource.ComposeTestCheckFunc( testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, resourceName), checkCSMThreatsPolicyContent( resourceName, - policyName, "updated policy for terraform provider test", tags, ), @@ -67,9 +62,9 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { }) } -func checkCSMThreatsPolicyContent(resourceName string, name string, description string, tags []string) resource.TestCheckFunc { +func checkCSMThreatsPolicyContent(resourceName string, description string, tags []string) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttrSet(resourceName, "name"), resource.TestCheckResourceAttr(resourceName, "description", description), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), resource.TestCheckResourceAttr(resourceName, "tags.0", tags[0]), diff --git a/docs/resources/csm_threats_policies_list.md b/docs/resources/csm_threats_policies_list.md index 1145484806..6a32cd8280 100644 --- a/docs/resources/csm_threats_policies_list.md +++ b/docs/resources/csm_threats_policies_list.md @@ -17,7 +17,7 @@ Provides a Datadog CSM Threats policies API resource. ### Optional -- `entries` (Block Set) A set of policies that belong to this list/batch. All non-listed policies get deleted. (see [below for nested schema](#nestedblock--entries)) +- `entries` (Block Set) A set of policies that belong to this list. Only one policies_list resource can be defined in Terraform, containing all unique policies. All non-listed policies get deleted. (see [below for nested schema](#nestedblock--entries)) ### Read-Only @@ -28,7 +28,7 @@ Provides a Datadog CSM Threats policies API resource. Required: -- `policy_id` (String) The ID of the policy to manage (from `csm_threats_policy`). +- `policy_id` (String) The ID of the policy to manage (from csm_threats_policy). - `priority` (Number) The priority of the policy in this list. Optional: From 4e653f6be2bf7e8902224a92064a61601e8b632e Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Thu, 27 Feb 2025 13:58:13 +0100 Subject: [PATCH 04/14] remove priority --- ...urce_datadog_csm_threats_multi_policies.go | 305 ++++++++++++------ 1 file changed, 211 insertions(+), 94 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go index ac4491be63..8dce554355 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go @@ -2,12 +2,14 @@ package fwprovider import ( "context" + "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" @@ -24,14 +26,17 @@ type csmThreatsPoliciesListResource struct { } type csmThreatsPoliciesListModel struct { - ID types.String `tfsdk:"id"` - Entries []csmThreatsPoliciesListEntryModel `tfsdk:"entries"` + ID types.String `tfsdk:"id"` + Policies []csmThreatsPolicyEntryModel `tfsdk:"policies"` } -type csmThreatsPoliciesListEntryModel struct { - PolicyID types.String `tfsdk:"policy_id"` - Name types.String `tfsdk:"name"` - Priority types.Int64 `tfsdk:"priority"` +type csmThreatsPolicyEntryModel struct { + PolicyLabel types.String `tfsdk:"policy_label"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + Tags types.Set `tfsdk:"tags"` } func NewCSMThreatsPoliciesListResource() resource.Resource { @@ -39,7 +44,7 @@ func NewCSMThreatsPoliciesListResource() resource.Resource { } func (r *csmThreatsPoliciesListResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = "csm_threats_policies_list" + response.TypeName = "csm_threats_policies" } func (r *csmThreatsPoliciesListResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { @@ -54,28 +59,42 @@ func (r *csmThreatsPoliciesListResource) ImportState(ctx context.Context, reques func (r *csmThreatsPoliciesListResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = schema.Schema{ - Description: "Provides a Datadog CSM Threats policies API resource.", + Description: "Manages multiple Datadog CSM Threats policies in a single resource.", Attributes: map[string]schema.Attribute{ "id": utils.ResourceIDAttribute(), }, Blocks: map[string]schema.Block{ - "entries": schema.SetNestedBlock{ - Description: "A set of policies that belong to this list. Only one policies_list resource can be defined in Terraform, containing all unique policies. All non-listed policies get deleted.", + "policies": schema.SetNestedBlock{ + Description: "Set of policy blocks. Each block requires a unique policy_label.", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "policy_id": schema.StringAttribute{ + "policy_label": schema.StringAttribute{ Description: "The ID of the policy to manage (from csm_threats_policy).", Required: true, }, - "priority": schema.Int64Attribute{ - Description: "The priority of the policy in this list.", - Required: true, + "id": schema.StringAttribute{ + Description: "The Datadog-assigned policy ID.", + Computed: true, }, "name": schema.StringAttribute{ - Description: "Optional name. If omitted, fallback to the policy_id as name.", + Description: "Name of the policy.", + Optional: true, + }, + "description": schema.StringAttribute{ + Description: "A description for the policy.", Optional: true, + }, + "enabled": schema.BoolAttribute{ + Description: "Indicates whether the policy is enabled.", + Optional: true, + Default: booldefault.StaticBool(false), Computed: true, }, + "tags": schema.SetAttribute{ + Description: "Host tags that define where the policy is deployed.", + Optional: true, + ElementType: types.StringType, + }, }, }, }, @@ -92,12 +111,12 @@ func (r *csmThreatsPoliciesListResource) Create(ctx context.Context, request res plan.ID = types.StringValue("policies_list") - updatedEntries, err := r.applyBatchPolicies(ctx, plan.Entries, &response.Diagnostics) + updatedPolicies, err := r.applyBatchPolicies(ctx, []csmThreatsPolicyEntryModel{}, plan.Policies, &response.Diagnostics) if err != nil { return } - plan.Entries = updatedEntries + plan.Policies = updatedPolicies response.Diagnostics.Append(response.State.Set(ctx, &plan)...) } @@ -108,11 +127,6 @@ func (r *csmThreatsPoliciesListResource) Read(ctx context.Context, request resou return } - if state.ID.IsUnknown() || state.ID.IsNull() || state.ID.ValueString() == "" { - response.State.RemoveResource(ctx) - return - } - listResponse, httpResp, err := r.api.ListCSMThreatsAgentPolicies(r.auth) if err != nil { if httpResp != nil && httpResp.StatusCode == 404 { @@ -123,93 +137,112 @@ func (r *csmThreatsPoliciesListResource) Read(ctx context.Context, request resou return } - newEntries := make([]csmThreatsPoliciesListEntryModel, 0) - for _, policyData := range listResponse.GetData() { - policyID := policyData.GetId() - if policyID == "CWS_DD" { - continue + apiMap := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) + for _, policy := range listResponse.GetData() { + policyID := policy.GetId() + if policy.Attributes != nil { + apiMap[policyID] = *policy.Attributes } - attributes := policyData.Attributes + } - name := attributes.GetName() - priorirty := attributes.GetPriority() + newPolicies := make([]csmThreatsPolicyEntryModel, 0, len(state.Policies)) - entry := csmThreatsPoliciesListEntryModel{ - PolicyID: types.StringValue(policyID), - Name: types.StringValue(name), - Priority: types.Int64Value(int64(priorirty)), + // update the state with the latest data from the API, but only for the policies that are already present in the state + for _, policy := range state.Policies { + policyID := policy.ID.ValueString() + attr, found := apiMap[policyID] + if !found { + // policy was deleted outside of Terraform + continue } - newEntries = append(newEntries, entry) + + tags, _ := types.SetValueFrom(ctx, types.StringType, attr.GetHostTags()) + newPolicies = append(newPolicies, csmThreatsPolicyEntryModel{ + PolicyLabel: policy.PolicyLabel, + ID: types.StringValue(policyID), + Name: types.StringValue(attr.GetName()), + Description: types.StringValue(attr.GetDescription()), + Enabled: types.BoolValue(attr.GetEnabled()), + Tags: tags, + }) } - state.Entries = newEntries + state.Policies = newPolicies response.Diagnostics.Append(response.State.Set(ctx, &state)...) } func (r *csmThreatsPoliciesListResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - var plan csmThreatsPoliciesListModel + var plan, old csmThreatsPoliciesListModel + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) if response.Diagnostics.HasError() { return } + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } - updatedEntries, err := r.applyBatchPolicies(ctx, plan.Entries, &response.Diagnostics) + updatedPolicies, err := r.applyBatchPolicies(ctx, old.Policies, plan.Policies, &response.Diagnostics) if err != nil { return } - plan.Entries = updatedEntries + plan.Policies = updatedPolicies response.Diagnostics.Append(response.State.Set(ctx, &plan)...) } func (r *csmThreatsPoliciesListResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - _, err := r.applyBatchPolicies(ctx, []csmThreatsPoliciesListEntryModel{}, &response.Diagnostics) + var state csmThreatsPoliciesListModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + _, err := r.applyBatchPolicies(ctx, state.Policies, []csmThreatsPolicyEntryModel{}, &response.Diagnostics) if err != nil { return } response.State.RemoveResource(ctx) } -func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, entries []csmThreatsPoliciesListEntryModel, diags *diag.Diagnostics) ([]csmThreatsPoliciesListEntryModel, error) { - listResp, httpResp, err := r.api.ListCSMThreatsAgentPolicies(r.auth) - if err != nil { - if httpResp != nil && httpResp.StatusCode == 404 { - diags.Append(utils.FrameworkErrorDiag(err, "error while fetching agent policies")) - return nil, err - } +func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, oldPolicies []csmThreatsPolicyEntryModel, newPolicies []csmThreatsPolicyEntryModel, diags *diag.Diagnostics) ([]csmThreatsPolicyEntryModel, error) { + oldPoliciesMap := make(map[string]csmThreatsPolicyEntryModel) + for _, policy := range oldPolicies { + oldPoliciesMap[policy.PolicyLabel.ValueString()] = policy } - existingPolicies := make(map[string]struct{}) - for _, policy := range listResp.GetData() { - if policy.GetId() == "CWS_DD" { - continue - } - existingPolicies[policy.GetId()] = struct{}{} + newPoliciesMap := make(map[string]csmThreatsPolicyEntryModel) + for _, policy := range newPolicies { + newPoliciesMap[policy.PolicyLabel.ValueString()] = policy } - var batchItems []datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems - - for i := range entries { - policyID := entries[i].PolicyID.ValueString() - name := entries[i].Name.ValueString() + // check policies that should be deleted (present in old but not in new) + var toDelete []csmThreatsPolicyEntryModel - if name == "" { - name = policyID - entries[i].Name = types.StringValue(name) + for policyLabel, oldPolicy := range oldPoliciesMap { + if _, found := newPoliciesMap[policyLabel]; !found { + toDelete = append(toDelete, oldPolicy) } - priority := entries[i].Priority.ValueInt64() + } - item := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ - Id: &policyID, - Name: &name, - Priority: &priority, - } + // add policies that should be created or updated (even if they are not modified, we send all policies in the batch request) + var toUpsert []csmThreatsPolicyEntryModel - batchItems = append(batchItems, item) - delete(existingPolicies, policyID) + // get IDs of existing policies + for _, policy := range newPolicies { + policyLabel := policy.PolicyLabel.ValueString() + if oldPolicy, found := oldPoliciesMap[policyLabel]; found { + policy.ID = oldPolicy.ID + } + toUpsert = append(toUpsert, policy) } - for policyID := range existingPolicies { + var batchItems []datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems + + // add deleted policies to the batch request + for _, policy := range toDelete { + policyID := policy.PolicyLabel.ValueString() DeleteTrue := true item := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ Id: &policyID, @@ -218,41 +251,125 @@ func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, batchItems = append(batchItems, item) } - patchID := "batch_update_req" + // add updated or new policies to the batch request + for _, policy := range toUpsert { + policyID := policy.ID.ValueString() + name := policy.Name.ValueString() + description := policy.Description.ValueString() + enabled := policy.Enabled.ValueBool() + var tags []string + if !policy.Tags.IsNull() && !policy.Tags.IsUnknown() { + for _, tag := range policy.Tags.Elements() { + tagStr, ok := tag.(types.String) + if !ok { + return nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) + } + tags = append(tags, tagStr.ValueString()) + } + } + + items := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ + Name: &name, + Description: &description, + Enabled: &enabled, + HostTags: tags, + } + // if policyID is not empty, it means it's not a new policy: we add the id parameter to the request + if policyID != "" { + items.Id = &policyID + } + batchItems = append(batchItems, items) + } + + if len(batchItems) == 0 { + return newPolicies, nil + } + + patchID := "batch_req" typ := datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYBATCHUPDATEDATATYPE_POLICIES - attributes := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateAttributes() - attributes.SetPolicies(batchItems) - data := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateData(*attributes, patchID, typ) + attrs := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateAttributes() + attrs.SetPolicies(batchItems) + data := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateData(*attrs, patchID, typ) batchReq := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateRequest(*data) - response, _, err := r.api.BatchUpdateCSMThreatsAgentPolicy( - r.auth, - *batchReq, - ) + batchResp, _, err := r.api.BatchUpdateCSMThreatsAgentPolicy(r.auth, *batchReq) if err != nil { - diags.Append(utils.FrameworkErrorDiag(err, "error while applying batch policies")) + *diags = append(*diags, utils.FrameworkErrorDiag(err, "error applying batch policy changes")) return nil, err } - finalEntries := make([]csmThreatsPoliciesListEntryModel, 0) - for _, policy := range response.GetData() { - policyID := policy.GetId() - attributes := policy.Attributes + for _, policy := range toDelete { + delete(newPoliciesMap, policy.PolicyLabel.ValueString()) + } + + // get the policies from the response using the ID for modified policies and the name for new policies (because new policies don't have an ID yet) + respMapByID := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) + respMapByName := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) + + for _, policy := range batchResp.GetData() { + respID := policy.GetId() + respAttr := policy.Attributes + if respAttr == nil { + continue + } + respMapByID[respID] = *respAttr + respMapByName[respAttr.GetName()] = *respAttr + + } + + // final state of the policies updated with the response from the API + finalMap := make(map[string]csmThreatsPolicyEntryModel, len(newPoliciesMap)) + + for label, policy := range newPoliciesMap { + oldID := policy.ID.ValueString() + oldName := policy.Name.ValueString() + + // if the ID is not empty, it means the policy was either modified or left unchanged + if oldID != "" { + if attr, found := respMapByID[oldID]; found { + tags, _ := types.SetValueFrom(ctx, types.StringType, attr.GetHostTags()) + finalMap[label] = csmThreatsPolicyEntryModel{ + PolicyLabel: policy.PolicyLabel, + ID: types.StringValue(oldID), + Name: types.StringValue(attr.GetName()), + Description: types.StringValue(attr.GetDescription()), + Enabled: types.BoolValue(attr.GetEnabled()), + Tags: tags, + } + continue + } + } - name := "" - if attributes.GetName() == "" { - name = policyID + // if the ID is empty, it means the policy was created + if attr, found := respMapByName[oldName]; found { + finalID := findIDByName(oldName, batchResp.GetData()) + tags, _ := types.SetValueFrom(ctx, types.StringType, attr.GetHostTags()) + finalMap[label] = csmThreatsPolicyEntryModel{ + PolicyLabel: policy.PolicyLabel, + ID: types.StringValue(finalID), + Name: types.StringValue(attr.GetName()), + Description: types.StringValue(attr.GetDescription()), + Enabled: types.BoolValue(attr.GetEnabled()), + Tags: tags, + } } - name = attributes.GetName() - priority := attributes.GetPriority() + } - entry := csmThreatsPoliciesListEntryModel{ - PolicyID: types.StringValue(policyID), - Name: types.StringValue(name), - Priority: types.Int64Value(int64(priority)), + finalSlice := make([]csmThreatsPolicyEntryModel, 0, len(finalMap)) + for _, policy := range newPolicies { + if updated, ok := finalMap[policy.PolicyLabel.ValueString()]; ok { + finalSlice = append(finalSlice, updated) } - finalEntries = append(finalEntries, entry) } - return finalEntries, nil + return finalSlice, nil +} + +func findIDByName(name string, items []datadogV2.CloudWorkloadSecurityAgentPolicyData) string { + for _, it := range items { + if it.Attributes != nil && it.Attributes.GetName() == name { + return it.GetId() + } + } + return "" } From 01017fd13dc72913b24f1298a9db2f09078e07c6 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Fri, 28 Feb 2025 16:34:02 +0100 Subject: [PATCH 05/14] generate docs + modify test file --- ...urce_datadog_csm_threats_multi_policies.go | 11 +- ..._datadog_csm_threats_policies_list_test.go | 119 ++++++------------ docs/resources/csm_threats_policies.md | 42 +++++++ docs/resources/csm_threats_policies_list.md | 36 ------ 4 files changed, 86 insertions(+), 122 deletions(-) create mode 100644 docs/resources/csm_threats_policies.md delete mode 100644 docs/resources/csm_threats_policies_list.md diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go index 8dce554355..bdee54a318 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" @@ -78,22 +77,23 @@ func (r *csmThreatsPoliciesListResource) Schema(_ context.Context, _ resource.Sc }, "name": schema.StringAttribute{ Description: "Name of the policy.", - Optional: true, + Required: true, }, "description": schema.StringAttribute{ Description: "A description for the policy.", Optional: true, + Computed: true, }, "enabled": schema.BoolAttribute{ Description: "Indicates whether the policy is enabled.", Optional: true, - Default: booldefault.StaticBool(false), Computed: true, }, "tags": schema.SetAttribute{ Description: "Host tags that define where the policy is deployed.", Optional: true, ElementType: types.StringType, + Computed: true, }, }, }, @@ -242,7 +242,7 @@ func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, // add deleted policies to the batch request for _, policy := range toDelete { - policyID := policy.PolicyLabel.ValueString() + policyID := policy.ID.ValueString() DeleteTrue := true item := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ Id: &policyID, @@ -257,7 +257,7 @@ func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, name := policy.Name.ValueString() description := policy.Description.ValueString() enabled := policy.Enabled.ValueBool() - var tags []string + tags := []string{} if !policy.Tags.IsNull() && !policy.Tags.IsUnknown() { for _, tag := range policy.Tags.Elements() { tagStr, ok := tag.(types.String) @@ -307,6 +307,7 @@ func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, respMapByName := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) for _, policy := range batchResp.GetData() { + respID := policy.GetId() respAttr := policy.Attributes if respAttr == nil { diff --git a/datadog/tests/resource_datadog_csm_threats_policies_list_test.go b/datadog/tests/resource_datadog_csm_threats_policies_list_test.go index bef4c484b9..06f2cee752 100644 --- a/datadog/tests/resource_datadog_csm_threats_policies_list_test.go +++ b/datadog/tests/resource_datadog_csm_threats_policies_list_test.go @@ -12,51 +12,49 @@ import ( ) // Create a policies_list and update the name and priority of its policy -func TestAccCSMThreatsPoliciesList_CreateAndUpdate(t *testing.T) { +func TestAccCSMThreatsPolicies_CreateAndUpdate(t *testing.T) { _, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - resourceName := "datadog_csm_threats_policies_list.all" + resourceName := "datadog_csm_threats_policies.all_policies" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckCSMThreatsPoliciesListDestroy(providers.frameworkProvider), + CheckDestroy: testAccCheckCSMThreatsPoliciesDestroy(providers.frameworkProvider), Steps: []resource.TestStep{ { - Config: testAccCSMThreatsPoliciesListConfigBasic(), + Config: testAccCSMThreatsPoliciesConfig(), Check: resource.ComposeTestCheckFunc( - testAccCheckCSMThreatsPoliciesListExists(providers.frameworkProvider, resourceName), - resource.TestCheckResourceAttr(resourceName, "entries.#", "2"), - resource.TestCheckResourceAttr(resourceName, "entries.0.name", "TERRAFORM_POLICY1"), - resource.TestCheckResourceAttr(resourceName, "entries.0.priority", "2"), - resource.TestCheckResourceAttr(resourceName, "entries.1.name", "TERRAFORM_POLICY2"), - resource.TestCheckResourceAttr(resourceName, "entries.1.priority", "3"), + testAccCheckCSMThreatsPoliciesExists(providers.frameworkProvider, resourceName), + resource.TestCheckResourceAttr(resourceName, "policies.0.name", "terraform_policy"), + resource.TestCheckResourceAttr(resourceName, "policies.0.description", "description"), + resource.TestCheckResourceAttr(resourceName, "policies.0.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "policies.0.tags.0", "env:staging"), ), }, { - Config: testAccCSMThreatsPoliciesListConfigUpdate(), + Config: testAccCSMThreatsPoliciesConfigUpdate(), Check: resource.ComposeTestCheckFunc( - testAccCheckCSMThreatsPoliciesListExists(providers.frameworkProvider, resourceName), - resource.TestCheckResourceAttr(resourceName, "entries.#", "2"), - resource.TestCheckResourceAttr(resourceName, "entries.0.name", "TERRAFORM_POLICY1"), - resource.TestCheckResourceAttr(resourceName, "entries.0.priority", "2"), - resource.TestCheckResourceAttr(resourceName, "entries.1.name", "TERRAFORM_POLICY2 UPDATED"), - resource.TestCheckResourceAttr(resourceName, "entries.1.priority", "5"), + testAccCheckCSMThreatsPoliciesExists(providers.frameworkProvider, resourceName), + resource.TestCheckResourceAttr(resourceName, "policies.0.name", "terraform_policy updated"), + resource.TestCheckResourceAttr(resourceName, "policies.0.description", "new description"), + resource.TestCheckResourceAttr(resourceName, "policies.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "policies.0.tags.0", "foo:bar"), ), }, }, }) } -func testAccCheckCSMThreatsPoliciesListExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { +func testAccCheckCSMThreatsPoliciesExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { return fmt.Errorf("resource '%s' not found in state", resourceName) } - if rs.Type != "datadog_csm_threats_policies_list" { + if rs.Type != "datadog_csm_threats_policies" { return fmt.Errorf( - "resource %s is not a datadog_csm_threats_policies_list, got: %s", + "resource %s is not a datadog_csm_threats_policies, got: %s", resourceName, rs.Type, ) @@ -70,85 +68,44 @@ func testAccCheckCSMThreatsPoliciesListExists(accProvider *fwprovider.FrameworkP } } -func testAccCheckCSMThreatsPoliciesListDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { +func testAccCheckCSMThreatsPoliciesDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { return func(s *terraform.State) error { - apiInstances := accProvider.DatadogApiInstances - auth := accProvider.Auth - for _, r := range s.RootModule().Resources { - if r.Type != "datadog_csm_threats_policies_list" { + if r.Type != "datadog_csm_threats_policies" { continue } - resp, httpResponse, err := apiInstances.GetCSMThreatsApiV2().ListCSMThreatsAgentPolicies(auth) - if err != nil { - if httpResponse != nil && httpResponse.StatusCode == 404 { - return nil - } - return fmt.Errorf("Received an error while listing the policies: %s", err) - } - - if len(resp.GetData()) > 1 { // CWS_DD is always present - return fmt.Errorf("Policies list not empty, some policies are still present") + if _, ok := s.RootModule().Resources[r.Primary.ID]; ok { + return fmt.Errorf("Resource %s still exists in state", r.Primary.ID) } } return nil } } -func testAccCSMThreatsPoliciesListConfigBasic() string { +func testAccCSMThreatsPoliciesConfig() string { return ` - resource "datadog_csm_threats_policy" "policy1" { - description = "created with terraform" - enabled = false - tags = [] - } - - resource "datadog_csm_threats_policy" "policy2" { - description = "created with terraform 2" - enabled = true - tags = ["env:staging"] - } - - resource "datadog_csm_threats_policies_list" "all" { - entries { - policy_id = datadog_csm_threats_policy.policy1.id - name = "TERRAFORM_POLICY1" - priority = 2 - } - entries { - policy_id = datadog_csm_threats_policy.policy2.id - name = "TERRAFORM_POLICY2" - priority = 3 + resource "datadog_csm_threats_policies" "all_policies" { + policies { + policy_label = "policy" + name = "terraform_policy" + description = "description" + enabled = false + tags = ["env:staging"] } } ` } -func testAccCSMThreatsPoliciesListConfigUpdate() string { +func testAccCSMThreatsPoliciesConfigUpdate() string { return ` - resource "datadog_csm_threats_policy" "policy1" { - description = "created with terraform" - enabled = false - tags = [] - } - - resource "datadog_csm_threats_policy" "policy2" { - description = "created with terraform 2" - enabled = true - tags = ["env:staging"] - } - - resource "datadog_csm_threats_policies_list" "all" { - entries { - policy_id = datadog_csm_threats_policy.policy1.id - name = "TERRAFORM_POLICY1" - priority = 2 - } - entries { - policy_id = datadog_csm_threats_policy.policy2.id - name = "TERRAFORM_POLICY2 UPDATED" - priority = 5 + resource "datadog_csm_threats_policies" "all_policies" { + policies { + policy_label = "policy" + name = "terraform_policy updated" + description = "new description" + enabled = true + tags = ["foo:bar"] } } ` diff --git a/docs/resources/csm_threats_policies.md b/docs/resources/csm_threats_policies.md new file mode 100644 index 0000000000..3143eb1d21 --- /dev/null +++ b/docs/resources/csm_threats_policies.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_csm_threats_policies Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Manages multiple Datadog CSM Threats policies in a single resource. +--- + +# datadog_csm_threats_policies (Resource) + +Manages multiple Datadog CSM Threats policies in a single resource. + + + + +## Schema + +### Optional + +- `policies` (Block Set) Set of policy blocks. Each block requires a unique policy_label. (see [below for nested schema](#nestedblock--policies)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `policies` + +Required: + +- `name` (String) Name of the policy. +- `policy_label` (String) The ID of the policy to manage (from csm_threats_policy). + +Optional: + +- `description` (String) A description for the policy. +- `enabled` (Boolean) Indicates whether the policy is enabled. +- `tags` (Set of String) Host tags that define where the policy is deployed. + +Read-Only: + +- `id` (String) The Datadog-assigned policy ID. diff --git a/docs/resources/csm_threats_policies_list.md b/docs/resources/csm_threats_policies_list.md deleted file mode 100644 index 6a32cd8280..0000000000 --- a/docs/resources/csm_threats_policies_list.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "datadog_csm_threats_policies_list Resource - terraform-provider-datadog" -subcategory: "" -description: |- - Provides a Datadog CSM Threats policies API resource. ---- - -# datadog_csm_threats_policies_list (Resource) - -Provides a Datadog CSM Threats policies API resource. - - - - -## Schema - -### Optional - -- `entries` (Block Set) A set of policies that belong to this list. Only one policies_list resource can be defined in Terraform, containing all unique policies. All non-listed policies get deleted. (see [below for nested schema](#nestedblock--entries)) - -### Read-Only - -- `id` (String) The ID of this resource. - - -### Nested Schema for `entries` - -Required: - -- `policy_id` (String) The ID of the policy to manage (from csm_threats_policy). -- `priority` (Number) The priority of the policy in this list. - -Optional: - -- `name` (String) Optional name. If omitted, fallback to the policy_id as name. From 6477be3dbb94a95cce0ed01381406130de6958f7 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Tue, 25 Feb 2025 10:59:30 +0100 Subject: [PATCH 06/14] add product_tags --- ...dog_csm_threats_multi_policy_agent_rule.go | 27 ++++++++++++++++--- .../resource_datadog_csm_threats_policy.go | 17 ++++++------ ...rce_datadog_csm_threats_agent_rule_test.go | 8 +++++- ...sm_threats_multi_policy_agent_rule_test.go | 4 +++ ...esource_datadog_csm_threats_policy_test.go | 19 ++++++++----- .../csm_threats_multi_policy_agent_rule.md | 1 + go.mod | 1 + 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go index c0f06e3dac..93ba69bc95 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -27,6 +27,7 @@ type csmThreatsMultiPolicyAgentRuleModel struct { Description types.String `tfsdk:"description"` Enabled types.Bool `tfsdk:"enabled"` Expression types.String `tfsdk:"expression"` + ProductTags types.Set `tfsdk:"product_tags"` } func NewCSMThreatsMultiPolicyAgentRuleResource() resource.Resource { @@ -75,6 +76,11 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Schema(_ context.Context, _ res stringplanmodifier.RequiresReplace(), }, }, + "product_tags": schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "The list of product tags associated with the rule", + }, }, } } @@ -198,7 +204,7 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Delete(ctx context.Context, req } func (r *csmThreatsMultiPolicyAgentRuleResource) buildCreateCSMThreatsAgentRulePayload(state *csmThreatsMultiPolicyAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleCreateRequest, error) { - _, policyId, name, description, enabled, expression := r.extractAgentRuleAttributesFromResource(state) + _, policyId, name, description, enabled, expression, productTags := r.extractAgentRuleAttributesFromResource(state) attributes := datadogV2.CloudWorkloadSecurityAgentRuleCreateAttributes{} attributes.Expression = expression @@ -206,25 +212,27 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) buildCreateCSMThreatsAgentRuleP attributes.Description = description attributes.Enabled = &enabled attributes.PolicyId = &policyId + attributes.ProductTags = productTags data := datadogV2.NewCloudWorkloadSecurityAgentRuleCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) return datadogV2.NewCloudWorkloadSecurityAgentRuleCreateRequest(*data), nil } func (r *csmThreatsMultiPolicyAgentRuleResource) buildUpdateCSMThreatsAgentRulePayload(state *csmThreatsMultiPolicyAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleUpdateRequest, error) { - agentRuleId, policyId, _, description, enabled, _ := r.extractAgentRuleAttributesFromResource(state) + agentRuleId, policyId, _, description, enabled, _, productTags := r.extractAgentRuleAttributesFromResource(state) attributes := datadogV2.CloudWorkloadSecurityAgentRuleUpdateAttributes{} attributes.Description = description attributes.Enabled = &enabled attributes.PolicyId = &policyId + attributes.ProductTags = productTags data := datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) data.Id = &agentRuleId return datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateRequest(*data), nil } -func (r *csmThreatsMultiPolicyAgentRuleResource) extractAgentRuleAttributesFromResource(state *csmThreatsMultiPolicyAgentRuleModel) (string, string, string, *string, bool, string) { +func (r *csmThreatsMultiPolicyAgentRuleResource) extractAgentRuleAttributesFromResource(state *csmThreatsMultiPolicyAgentRuleModel) (string, string, string, *string, bool, string, []string) { // Mandatory fields id := state.Id.ValueString() policyId := state.PolicyId.ValueString() @@ -232,8 +240,18 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) extractAgentRuleAttributesFromR enabled := state.Enabled.ValueBool() expression := state.Expression.ValueString() description := state.Description.ValueStringPointer() + var productTags []string + if !state.ProductTags.IsNull() && !state.ProductTags.IsUnknown() { + for _, tag := range state.ProductTags.Elements() { + tagStr, ok := tag.(types.String) + if !ok { + return "", "", "", nil, false, "", nil + } + productTags = append(productTags, tagStr.ValueString()) + } + } - return id, policyId, name, description, enabled, expression + return id, policyId, name, description, enabled, expression, productTags } func (r *csmThreatsMultiPolicyAgentRuleResource) updateStateFromResponse(ctx context.Context, state *csmThreatsMultiPolicyAgentRuleModel, res *datadogV2.CloudWorkloadSecurityAgentRuleResponse) { @@ -245,4 +263,5 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) updateStateFromResponse(ctx con state.Description = types.StringValue(attributes.GetDescription()) state.Enabled = types.BoolValue(attributes.GetEnabled()) state.Expression = types.StringValue(attributes.GetExpression()) + state.ProductTags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetProductTags()) } diff --git a/datadog/fwprovider/resource_datadog_csm_threats_policy.go b/datadog/fwprovider/resource_datadog_csm_threats_policy.go index a1f960770f..2569747259 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_policy.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_policy.go @@ -3,7 +3,6 @@ package fwprovider import ( "context" "fmt" - mathrand "math/rand" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/path" @@ -48,7 +47,7 @@ func (r *csmThreatsPolicyResource) Schema(_ context.Context, _ resource.SchemaRe Attributes: map[string]schema.Attribute{ "id": utils.ResourceIDAttribute(), "name": schema.StringAttribute{ - Computed: true, + Required: true, Description: "The name of the policy.", }, "description": schema.StringAttribute{ @@ -183,13 +182,13 @@ func (r *csmThreatsPolicyResource) Delete(ctx context.Context, request resource. } func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyCreateRequest, error) { - _, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + _, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) if err != nil { return nil, err } attributes := datadogV2.CloudWorkloadSecurityAgentPolicyCreateAttributes{} - attributes.Name = fmt.Sprintf("policy-%d", mathrand.Intn(1000)) + attributes.Name = name attributes.Description = description attributes.Enabled = enabled attributes.HostTags = tags @@ -199,11 +198,12 @@ func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csm } func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyUpdateRequest, error) { - policyId, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + policyId, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) if err != nil { return nil, err } attributes := datadogV2.CloudWorkloadSecurityAgentPolicyUpdateAttributes{} + attributes.Name = &name attributes.Description = description attributes.Enabled = enabled attributes.HostTags = tags @@ -213,9 +213,10 @@ func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csm return datadogV2.NewCloudWorkloadSecurityAgentPolicyUpdateRequest(*data), nil } -func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, *string, *bool, []string, error) { +func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, string, *string, *bool, []string, error) { // Mandatory fields id := state.Id.ValueString() + name := state.Name.ValueString() enabled := state.Enabled.ValueBoolPointer() description := state.Description.ValueStringPointer() var tags []string @@ -223,13 +224,13 @@ func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *cs for _, tag := range state.Tags.Elements() { tagStr, ok := tag.(types.String) if !ok { - return "", nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) + return "", "", nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) } tags = append(tags, tagStr.ValueString()) } } - return id, description, enabled, tags, nil + return id, name, description, enabled, tags, nil } func (r *csmThreatsPolicyResource) updateStateFromResponse(ctx context.Context, state *csmThreatsPolicyModel, res *datadogV2.CloudWorkloadSecurityAgentPolicyResponse) { diff --git a/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go index a99e0c7420..224c190f82 100644 --- a/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go +++ b/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go @@ -30,6 +30,7 @@ func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { enabled = true description = "im a rule" expression = "open.file.name == \"etc/shadow/password\"" + product_tags = ["compliance_framework:PCI-DSS"] } `, agentRuleName), Check: resource.ComposeTestCheckFunc( @@ -39,6 +40,7 @@ func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { agentRuleName, "im a rule", "open.file.name == \"etc/shadow/password\"", + "compliance_framework:PCI-DSS\"]", ), ), }, @@ -50,6 +52,7 @@ func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { enabled = true description = "updated agent rule for terraform provider test" expression = "open.file.name == \"etc/shadow/password\"" + product_tags = ["compliance_framework:ISO-27799"] } `, agentRuleName), Check: resource.ComposeTestCheckFunc( @@ -59,6 +62,7 @@ func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { agentRuleName, "updated agent rule for terraform provider test", "open.file.name == \"etc/shadow/password\"", + "compliance_framework:ISO-27799", ), ), }, @@ -66,12 +70,14 @@ func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { }) } -func checkCSMThreatsAgentRuleContent(resourceName string, name string, description string, expression string) resource.TestCheckFunc { +func checkCSMThreatsAgentRuleContent(resourceName string, name string, description string, expression string, product_tags string) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "description", description), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), resource.TestCheckResourceAttr(resourceName, "expression", expression), + resource.TestCheckResourceAttr(resourceName, "product_tags.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "product_tags.*", product_tags), ) } diff --git a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go index 8c5b518f01..c202af3f9d 100644 --- a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go +++ b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go @@ -49,6 +49,7 @@ func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { enabled = true description = "im a rule" expression = "open.file.name == \"etc/shadow/password\"" + product_tags = ["compliance_framework:PCI-DSS"] } `, policyConfig, agentRuleName), Check: resource.ComposeTestCheckFunc( @@ -58,6 +59,7 @@ func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { agentRuleName, "im a rule", "open.file.name == \"etc/shadow/password\"", + "compliance_framework:PCI-DSS", ), ), }, @@ -71,6 +73,7 @@ func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { enabled = true description = "updated agent rule for terraform provider test" expression = "open.file.name == \"etc/shadow/password\"" + product_tags = ["compliance_framework:ISO-27799"] } `, policyConfig, agentRuleName), Check: resource.ComposeTestCheckFunc( @@ -80,6 +83,7 @@ func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { agentRuleName, "updated agent rule for terraform provider test", "open.file.name == \"etc/shadow/password\"", + "compliance_framework:ISO-27799", ), ), }, diff --git a/datadog/tests/resource_datadog_csm_threats_policy_test.go b/datadog/tests/resource_datadog_csm_threats_policy_test.go index 87483539d2..4e9099c01a 100644 --- a/datadog/tests/resource_datadog_csm_threats_policy_test.go +++ b/datadog/tests/resource_datadog_csm_threats_policy_test.go @@ -14,8 +14,9 @@ import ( // Create an agent policy and update its description func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { - _, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + policyName := uniqueAgentRuleName(ctx) resourceName := "datadog_csm_threats_policy.policy_test" tags := []string{"host_name:test_host"} resource.Test(t, resource.TestCase{ @@ -24,17 +25,19 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { CheckDestroy: testAccCheckCSMThreatsPolicyDestroy(providers.frameworkProvider), Steps: []resource.TestStep{ { - Config: ` + Config: fmt.Sprintf(` resource "datadog_csm_threats_policy" "policy_test" { + name = "%s" enabled = true description = "im a policy" tags = ["host_name:test_host"] } - `, + `, policyName), Check: resource.ComposeTestCheckFunc( testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, "datadog_csm_threats_policy.policy_test"), checkCSMThreatsPolicyContent( resourceName, + policyName, "im a policy", tags, ), @@ -42,17 +45,19 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { }, // Update description { - Config: ` + Config: fmt.Sprintf(` resource "datadog_csm_threats_policy" "policy_test" { + name = "%s" enabled = true description = "updated policy for terraform provider test" tags = ["host_name:test_host"] } - `, + `, policyName), Check: resource.ComposeTestCheckFunc( testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, resourceName), checkCSMThreatsPolicyContent( resourceName, + policyName, "updated policy for terraform provider test", tags, ), @@ -62,9 +67,9 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { }) } -func checkCSMThreatsPolicyContent(resourceName string, description string, tags []string) resource.TestCheckFunc { +func checkCSMThreatsPolicyContent(resourceName string, name string, description string, tags []string) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "description", description), resource.TestCheckResourceAttr(resourceName, "enabled", "true"), resource.TestCheckResourceAttr(resourceName, "tags.0", tags[0]), diff --git a/docs/resources/csm_threats_multi_policy_agent_rule.md b/docs/resources/csm_threats_multi_policy_agent_rule.md index 84d0a57714..67f4e6c52b 100644 --- a/docs/resources/csm_threats_multi_policy_agent_rule.md +++ b/docs/resources/csm_threats_multi_policy_agent_rule.md @@ -25,6 +25,7 @@ Provides a Datadog CSM Threats Agent Rule API resource. ### Optional - `description` (String) A description for the Agent rule. +- `product_tags` (Set of String) The list of product tags associated with the rule ### Read-Only diff --git a/go.mod b/go.mod index a02074fa79..b6da3fa13b 100644 --- a/go.mod +++ b/go.mod @@ -102,3 +102,4 @@ require ( ) go 1.23.0 +replace github.com/DataDog/datadog-api-client-go/v2 v2.35.0 => ../datadog-api-spec/generated/datadog-api-client-go \ No newline at end of file From 6e6bd84da1373b3bafcde3bd0985be7f295840ad Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Fri, 28 Mar 2025 10:54:48 +0100 Subject: [PATCH 07/14] remove unused resources + modify test files --- ...dog_cloud_workload_security_agent_rules.go | 100 -------- ...a_source_datadog_csm_threats_agent_rule.go | 137 ---------- ...og_csm_threats_multi_policy_agent_rules.go | 188 ++++++++++++++ datadog/fwprovider/framework_provider.go | 3 +- ...resource_datadog_csm_threats_agent_rule.go | 240 ------------------ ...dog_csm_threats_multi_policy_agent_rule.go | 7 + datadog/provider.go | 1 - ...loud_workload_security_agent_rules_test.go | 68 ----- ...ce_datadog_csm_threats_agent_rules_test.go | 109 -------- ...m_threats_multi_policy_agent_rules_test.go | 40 ++- ...cloud_workload_security_agent_rule_test.go | 130 ---------- ...rce_datadog_csm_threats_agent_rule_test.go | 126 --------- ...sm_threats_multi_policy_agent_rule_test.go | 11 + 13 files changed, 241 insertions(+), 919 deletions(-) delete mode 100644 datadog/data_source_datadog_cloud_workload_security_agent_rules.go delete mode 100644 datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go create mode 100644 datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go delete mode 100644 datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go delete mode 100644 datadog/tests/data_source_datadog_cloud_workload_security_agent_rules_test.go delete mode 100644 datadog/tests/data_source_datadog_csm_threats_agent_rules_test.go delete mode 100644 datadog/tests/resource_datadog_cloud_workload_security_agent_rule_test.go delete mode 100644 datadog/tests/resource_datadog_csm_threats_agent_rule_test.go diff --git a/datadog/data_source_datadog_cloud_workload_security_agent_rules.go b/datadog/data_source_datadog_cloud_workload_security_agent_rules.go deleted file mode 100644 index 45f942b0f3..0000000000 --- a/datadog/data_source_datadog_cloud_workload_security_agent_rules.go +++ /dev/null @@ -1,100 +0,0 @@ -package datadog - -import ( - "context" - "fmt" - - "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func dataSourceDatadogCloudWorkloadSecurityAgentRules() *schema.Resource { - return &schema.Resource{ - Description: "Use this data source to retrieve information about existing Cloud Workload Security Agent Rules for use in other resources. Deprecated, use datadog_csm_threats_agent_rules data source instead: https://registry.terraform.io/providers/DataDog/datadog/latest/docs/data-sources/csm_threats_agent_rules", - DeprecationMessage: "This data source is deprecated — use the datadog_csm_threats_agent_rules data source instead: https://registry.terraform.io/providers/DataDog/datadog/latest/docs/data-sources/csm_threats_agent_rules", - ReadContext: dataSourceDatadogCloudWorkloadSecurityAgentRulesRead, - - SchemaFunc: func() map[string]*schema.Schema { - return map[string]*schema.Schema{ - // Computed - "agent_rules": { - Description: "List of Agent rules.", - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - Description: "The id of the Agent rule.", - }, - "description": { - Type: schema.TypeString, - Computed: true, - Description: "The description of the Agent rule.", - }, - "enabled": { - Type: schema.TypeBool, - Computed: true, - Description: "Whether the Agent rule is enabled.", - }, - "expression": { - Type: schema.TypeString, - Computed: true, - Description: "The SECL expression of the Agent rule.", - }, - "name": { - Type: schema.TypeString, - Computed: true, - Description: "The name of the Agent rule.", - }, - }, - }, - }, - } - }, - } -} - -func dataSourceDatadogCloudWorkloadSecurityAgentRulesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - providerConf := meta.(*ProviderConfiguration) - apiInstances := providerConf.DatadogApiInstances - auth := providerConf.Auth - - agentRules := make([]map[string]interface{}, 0) - response, httpresp, err := apiInstances.GetCSMThreatsApiV2().ListCloudWorkloadSecurityAgentRules(auth) - if err != nil { - return utils.TranslateClientErrorDiag(err, httpresp, "error listing agent rules") - } - - diags := diag.Diagnostics{} - for _, agentRule := range response.GetData() { - if err := utils.CheckForUnparsed(agentRule); err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("skipping agent rule with id: %s", agentRule.GetId()), - Detail: fmt.Sprintf("rule contains unparsed object: %v", err), - }) - continue - } - - // extract agent rule - agentRuleTF := make(map[string]interface{}) - attributes := agentRule.GetAttributes() - - agentRuleTF["id"] = agentRule.GetId() - agentRuleTF["name"] = attributes.GetName() - agentRuleTF["description"] = attributes.GetDescription() - agentRuleTF["expression"] = attributes.GetExpression() - agentRuleTF["enabled"] = attributes.GetEnabled() - - agentRules = append(agentRules, agentRuleTF) - } - - d.SetId("cloud-workload-security-agent-rules") - d.Set("agent_rules", agentRules) - - return diags -} diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go b/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go deleted file mode 100644 index bbdc98acec..0000000000 --- a/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go +++ /dev/null @@ -1,137 +0,0 @@ -package fwprovider - -import ( - "context" - "crypto/sha256" - "fmt" - "strings" - - "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" -) - -var ( - _ datasource.DataSourceWithConfigure = &csmThreatsAgentRulesDataSource{} -) - -type csmThreatsAgentRulesDataSource struct { - api *datadogV2.CSMThreatsApi - auth context.Context -} - -type csmThreatsAgentRulesDataSourceModel struct { - PolicyId types.String `tfsdk:"policy_id"` - Id types.String `tfsdk:"id"` - AgentRulesIds types.List `tfsdk:"agent_rules_ids"` - AgentRules []csmThreatsAgentRuleModel `tfsdk:"agent_rules"` -} - -func NewCSMThreatsAgentRulesDataSource() datasource.DataSource { - return &csmThreatsAgentRulesDataSource{} -} - -func (r *csmThreatsAgentRulesDataSource) Configure(_ context.Context, request datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { - providerData := request.ProviderData.(*FrameworkProvider) - r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() - r.auth = providerData.Auth -} - -func (*csmThreatsAgentRulesDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, response *datasource.MetadataResponse) { - response.TypeName = "csm_threats_agent_rules" -} - -func (r *csmThreatsAgentRulesDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { - var state csmThreatsAgentRulesDataSourceModel - response.Diagnostics.Append(request.Config.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - policyId := state.PolicyId.ValueStringPointer() - params := datadogV2.NewListCSMThreatsAgentRulesOptionalParameters() - if !state.PolicyId.IsNull() && !state.PolicyId.IsUnknown() { - params.WithPolicyId(*policyId) - } - res, _, err := r.api.ListCSMThreatsAgentRules(r.auth, *params) - if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent rules")) - return - } - - data := res.GetData() - agentRuleIds := make([]string, len(data)) - agentRules := make([]csmThreatsAgentRuleModel, len(data)) - - for idx, agentRule := range res.GetData() { - var agentRuleModel csmThreatsAgentRuleModel - agentRuleModel.Id = types.StringValue(agentRule.GetId()) - attributes := agentRule.Attributes - agentRuleModel.Name = types.StringValue(attributes.GetName()) - agentRuleModel.Description = types.StringValue(attributes.GetDescription()) - agentRuleModel.Enabled = types.BoolValue(attributes.GetEnabled()) - agentRuleModel.Expression = types.StringValue(*attributes.Expression) - - agentRuleIds[idx] = agentRule.GetId() - agentRules[idx] = agentRuleModel - } - - stateId := strings.Join(agentRuleIds, "--") - state.Id = types.StringValue(computeDataSourceID(&stateId)) - tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, agentRuleIds) - response.Diagnostics.Append(diags...) - state.AgentRulesIds = tfAgentRuleIds - state.AgentRules = agentRules - - response.Diagnostics.Append(response.State.Set(ctx, &state)...) -} - -func computeDataSourceID(ids *string) string { - // Key for hashing - var b strings.Builder - if ids != nil { - b.WriteString(*ids) - } - keyStr := b.String() - h := sha256.New() - h.Write([]byte(keyStr)) - - return fmt.Sprintf("%x", h.Sum(nil)) -} - -func (*csmThreatsAgentRulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: "Use this data source to retrieve information about existing Agent rules.", - Attributes: map[string]schema.Attribute{ - // Input - "policy_id": schema.StringAttribute{ - Description: "Listing only the rules in the policy with this field as the ID", - Optional: true, - }, - // Output - "id": utils.ResourceIDAttribute(), - "agent_rules_ids": schema.ListAttribute{ - Computed: true, - Description: "List of IDs for the Agent rules.", - ElementType: types.StringType, - }, - "agent_rules": schema.ListAttribute{ - Computed: true, - Description: "List of Agent rules", - ElementType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - "name": types.StringType, - "description": types.StringType, - "enabled": types.BoolType, - "expression": types.StringType, - }, - }, - }, - }, - } -} diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go b/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go new file mode 100644 index 0000000000..478276de5c --- /dev/null +++ b/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go @@ -0,0 +1,188 @@ +package fwprovider + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +var ( + _ datasource.DataSourceWithConfigure = &csmThreatsMultiPolicyAgentRulesDataSource{} +) + +type csmThreatsMultiPolicyAgentRulesDataSource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +type csmThreatsMultiPolicyAgentRulesDataSourceModel struct { + PolicyId types.String `tfsdk:"policy_id"` + Id types.String `tfsdk:"id"` + AgentRulesIds types.List `tfsdk:"agent_rules_ids"` + AgentRules []csmThreatsMultiPolicyAgentRuleDataSourceModel `tfsdk:"agent_rules"` +} + +type csmThreatsMultiPolicyAgentRuleDataSourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + Expression types.String `tfsdk:"expression"` + ProductTags types.Set `tfsdk:"product_tags"` +} + +func NewCSMThreatsMultiPolicyAgentRulesDataSource() datasource.DataSource { + return &csmThreatsMultiPolicyAgentRulesDataSource{} +} + +func (r *csmThreatsMultiPolicyAgentRulesDataSource) Configure(_ context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { + if request.ProviderData == nil { + return + } + + providerData, ok := request.ProviderData.(*FrameworkProvider) + if !ok { + response.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *FrameworkProvider, got: %T. Please report this issue to the provider developers.", request.ProviderData), + ) + return + } + + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (r *csmThreatsMultiPolicyAgentRulesDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = "csm_threats_multi_policy_agent_rules" +} + +func (r *csmThreatsMultiPolicyAgentRulesDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var state csmThreatsMultiPolicyAgentRulesDataSourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + params := datadogV2.NewListCSMThreatsAgentRulesOptionalParameters() + if !state.PolicyId.IsNull() && !state.PolicyId.IsUnknown() { + policyId := state.PolicyId.ValueString() + params.WithPolicyId(policyId) + } + + res, _, err := r.api.ListCSMThreatsAgentRules(r.auth, *params) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent rules")) + return + } + + data := res.GetData() + agentRuleIds := make([]string, len(data)) + agentRules := make([]csmThreatsMultiPolicyAgentRuleDataSourceModel, len(data)) + + for idx, agentRule := range res.GetData() { + var agentRuleModel csmThreatsMultiPolicyAgentRuleDataSourceModel + agentRuleModel.Id = types.StringValue(agentRule.GetId()) + attributes := agentRule.Attributes + agentRuleModel.Name = types.StringValue(attributes.GetName()) + agentRuleModel.Description = types.StringValue(attributes.GetDescription()) + agentRuleModel.Enabled = types.BoolValue(attributes.GetEnabled()) + agentRuleModel.Expression = types.StringValue(*attributes.Expression) + + // Handle product tags if they exist + if attributes.ProductTags != nil && len(attributes.GetProductTags()) > 0 { + // Remove duplicates from product tags + uniqueTags := make(map[string]struct{}) + for _, tag := range attributes.GetProductTags() { + uniqueTags[tag] = struct{}{} + } + uniqueTagsSlice := make([]string, 0, len(uniqueTags)) + for tag := range uniqueTags { + uniqueTagsSlice = append(uniqueTagsSlice, tag) + } + + productTags, diags := types.SetValueFrom(ctx, types.StringType, uniqueTagsSlice) + if diags.HasError() { + response.Diagnostics.Append(diags...) + continue + } + agentRuleModel.ProductTags = productTags + } else { + // Create an empty set if no product tags + emptySet, diags := types.SetValueFrom(ctx, types.StringType, []string{}) + if diags.HasError() { + response.Diagnostics.Append(diags...) + continue + } + agentRuleModel.ProductTags = emptySet + } + + agentRuleIds[idx] = agentRule.GetId() + agentRules[idx] = agentRuleModel + } + + stateId := strings.Join(agentRuleIds, "--") + state.Id = types.StringValue(computeDataSourceID(&stateId)) + tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, agentRuleIds) + response.Diagnostics.Append(diags...) + state.AgentRulesIds = tfAgentRuleIds + state.AgentRules = agentRules + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (*csmThreatsMultiPolicyAgentRulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Use this data source to retrieve information about existing Agent rules.", + Attributes: map[string]schema.Attribute{ + // Input + "policy_id": schema.StringAttribute{ + Description: "Listing only the rules in the policy with this field as the ID", + Optional: true, + }, + // Output + "id": utils.ResourceIDAttribute(), + "agent_rules_ids": schema.ListAttribute{ + Computed: true, + Description: "List of IDs for the Agent rules.", + ElementType: types.StringType, + }, + "agent_rules": schema.ListAttribute{ + Computed: true, + Description: "List of Agent rules", + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "description": types.StringType, + "enabled": types.BoolType, + "expression": types.StringType, + "product_tags": types.SetType{ElemType: types.StringType}, + }, + }, + }, + }, + } +} + +func computeDataSourceID(ids *string) string { + // Key for hashing + var b strings.Builder + if ids != nil { + b.WriteString(*ids) + } + keyStr := b.String() + h := sha256.New() + h.Write([]byte(keyStr)) + + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index 316197e514..c23dde6ed2 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -68,7 +68,6 @@ var Resources = []func() resource.Resource{ NewTeamResource, NewUserRoleResource, NewSecurityMonitoringSuppressionResource, - NewCSMThreatsAgentRuleResource, NewServiceAccountResource, NewWebhookResource, NewWebhookCustomVariableResource, @@ -104,7 +103,7 @@ var Datasources = []func() datasource.DataSource{ NewDatadogUsersDataSource, NewDatadogRoleUsersDataSource, NewSecurityMonitoringSuppressionDataSource, - NewCSMThreatsAgentRulesDataSource, + NewCSMThreatsMultiPolicyAgentRulesDataSource, NewLogsPipelinesOrderDataSource, NewDatadogTeamsDataSource, NewDatadogActionConnectionDataSource, diff --git a/datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go deleted file mode 100644 index f04ada2d7f..0000000000 --- a/datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go +++ /dev/null @@ -1,240 +0,0 @@ -package fwprovider - -import ( - "context" - "sync" - - "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/hashicorp/terraform-plugin-framework/path" - "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/stringdefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" -) - -var ( - csmThreatsMutex sync.Mutex - _ resource.ResourceWithConfigure = &csmThreatsAgentRuleResource{} - _ resource.ResourceWithImportState = &csmThreatsAgentRuleResource{} -) - -type csmThreatsAgentRuleModel struct { - Id types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Enabled types.Bool `tfsdk:"enabled"` - Expression types.String `tfsdk:"expression"` -} - -type csmThreatsAgentRuleResource struct { - api *datadogV2.CSMThreatsApi - auth context.Context -} - -func NewCSMThreatsAgentRuleResource() resource.Resource { - return &csmThreatsAgentRuleResource{} -} - -func (r *csmThreatsAgentRuleResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = "csm_threats_agent_rule" -} - -func (r *csmThreatsAgentRuleResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { - providerData := request.ProviderData.(*FrameworkProvider) - r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() - r.auth = providerData.Auth -} - -func (r *csmThreatsAgentRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: "Provides a Datadog CSM Threats Agent Rule API resource.", - Attributes: map[string]schema.Attribute{ - "id": utils.ResourceIDAttribute(), - "name": schema.StringAttribute{ - Required: true, - Description: "The name of the Agent rule.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "description": schema.StringAttribute{ - Optional: true, - Description: "A description for the Agent rule.", - Default: stringdefault.StaticString(""), - Computed: true, - }, - "enabled": schema.BoolAttribute{ - Required: true, - Description: "Indicates Whether the Agent rule is enabled.", - }, - "expression": schema.StringAttribute{ - Required: true, - Description: "The SECL expression of the Agent rule", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -func (r *csmThreatsAgentRuleResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) -} - -func (r *csmThreatsAgentRuleResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - var state csmThreatsAgentRuleModel - response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - csmThreatsMutex.Lock() - defer csmThreatsMutex.Unlock() - - agentRulePayload, err := r.buildCreateCSMThreatsAgentRulePayload(&state) - if err != nil { - response.Diagnostics.AddError("error while parsing resource", err.Error()) - } - - res, _, err := r.api.CreateCSMThreatsAgentRule(r.auth, *agentRulePayload) - if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating agent rule")) - return - } - if err := utils.CheckForUnparsed(response); err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) - return - } - - r.updateStateFromResponse(ctx, &state, &res) - response.Diagnostics.Append(response.State.Set(ctx, &state)...) -} - -func (r *csmThreatsAgentRuleResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - var state csmThreatsAgentRuleModel - response.Diagnostics.Append(request.State.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - agentRuleId := state.Id.ValueString() - res, httpResponse, err := r.api.GetCSMThreatsAgentRule(r.auth, agentRuleId) - if err != nil { - if httpResponse != nil && httpResponse.StatusCode == 404 { - response.State.RemoveResource(ctx) - return - } - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error fetching agent rule")) - return - } - if err := utils.CheckForUnparsed(response); err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) - return - } - - r.updateStateFromResponse(ctx, &state, &res) - response.Diagnostics.Append(response.State.Set(ctx, &state)...) -} - -func (r *csmThreatsAgentRuleResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - var state csmThreatsAgentRuleModel - response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - csmThreatsMutex.Lock() - defer csmThreatsMutex.Unlock() - - agentRulePayload, err := r.buildUpdateCSMThreatsAgentRulePayload(&state) - if err != nil { - response.Diagnostics.AddError("error while parsing resource", err.Error()) - } - - res, _, err := r.api.UpdateCSMThreatsAgentRule(r.auth, state.Id.ValueString(), *agentRulePayload) - if err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating agent rule")) - return - } - if err := utils.CheckForUnparsed(response); err != nil { - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) - return - } - - r.updateStateFromResponse(ctx, &state, &res) - response.Diagnostics.Append(response.State.Set(ctx, &state)...) -} - -func (r *csmThreatsAgentRuleResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - var state csmThreatsAgentRuleModel - response.Diagnostics.Append(request.State.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - csmThreatsMutex.Lock() - defer csmThreatsMutex.Unlock() - - id := state.Id.ValueString() - - httpResp, err := r.api.DeleteCSMThreatsAgentRule(r.auth, id) - if err != nil { - if httpResp != nil && httpResp.StatusCode == 404 { - return - } - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting agent rule")) - return - } -} - -func (r *csmThreatsAgentRuleResource) buildCreateCSMThreatsAgentRulePayload(state *csmThreatsAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleCreateRequest, error) { - _, name, description, enabled, expression := r.extractAgentRuleAttributesFromResource(state) - - attributes := datadogV2.CloudWorkloadSecurityAgentRuleCreateAttributes{} - attributes.Expression = expression - attributes.Name = name - attributes.Description = description - attributes.Enabled = &enabled - - data := datadogV2.NewCloudWorkloadSecurityAgentRuleCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) - return datadogV2.NewCloudWorkloadSecurityAgentRuleCreateRequest(*data), nil -} - -func (r *csmThreatsAgentRuleResource) buildUpdateCSMThreatsAgentRulePayload(state *csmThreatsAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleUpdateRequest, error) { - agentRuleId, _, description, enabled, _ := r.extractAgentRuleAttributesFromResource(state) - - attributes := datadogV2.CloudWorkloadSecurityAgentRuleUpdateAttributes{} - attributes.Description = description - attributes.Enabled = &enabled - - data := datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) - data.Id = &agentRuleId - return datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateRequest(*data), nil -} - -func (r *csmThreatsAgentRuleResource) extractAgentRuleAttributesFromResource(state *csmThreatsAgentRuleModel) (string, string, *string, bool, string) { - // Mandatory fields - id := state.Id.ValueString() - name := state.Name.ValueString() - enabled := state.Enabled.ValueBool() - expression := state.Expression.ValueString() - description := state.Description.ValueStringPointer() - - return id, name, description, enabled, expression -} - -func (r *csmThreatsAgentRuleResource) updateStateFromResponse(ctx context.Context, state *csmThreatsAgentRuleModel, res *datadogV2.CloudWorkloadSecurityAgentRuleResponse) { - state.Id = types.StringValue(res.Data.GetId()) - - attributes := res.Data.Attributes - - state.Name = types.StringValue(attributes.GetName()) - state.Description = types.StringValue(attributes.GetDescription()) - state.Enabled = types.BoolValue(attributes.GetEnabled()) - state.Expression = types.StringValue(attributes.GetExpression()) -} diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go index 93ba69bc95..5910d7d89d 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -3,6 +3,7 @@ package fwprovider import ( "context" "strings" + "sync" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/path" @@ -15,6 +16,12 @@ import ( "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" ) +var ( + csmThreatsMutex sync.Mutex + _ resource.ResourceWithConfigure = &csmThreatsMultiPolicyAgentRuleResource{} + _ resource.ResourceWithImportState = &csmThreatsMultiPolicyAgentRuleResource{} +) + type csmThreatsMultiPolicyAgentRuleResource struct { api *datadogV2.CSMThreatsApi auth context.Context diff --git a/datadog/provider.go b/datadog/provider.go index 37839ef108..e0d30cfa02 100644 --- a/datadog/provider.go +++ b/datadog/provider.go @@ -222,7 +222,6 @@ func Provider() *schema.Provider { // NEW DATA SOURCES ARE NOT ALLOWED TO BE ADDED HERE // New data sources must be implemented using the `terraform-plugin-framework`, and added to the `datadog/fwprovider` directory. DataSourcesMap: map[string]*schema.Resource{ - "datadog_cloud_workload_security_agent_rules": dataSourceDatadogCloudWorkloadSecurityAgentRules(), "datadog_dashboard": dataSourceDatadogDashboard(), "datadog_integration_aws_logs_services": dataSourceDatadogIntegrationAWSLogsServices(), "datadog_logs_archives_order": dataSourceDatadogLogsArchivesOrder(), diff --git a/datadog/tests/data_source_datadog_cloud_workload_security_agent_rules_test.go b/datadog/tests/data_source_datadog_cloud_workload_security_agent_rules_test.go deleted file mode 100644 index 1e0a6a1fe0..0000000000 --- a/datadog/tests/data_source_datadog_cloud_workload_security_agent_rules_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package test - -import ( - "context" - "fmt" - "strconv" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - - "github.com/terraform-providers/terraform-provider-datadog/datadog" -) - -const tfAgentRulesSource = "data.datadog_cloud_workload_security_agent_rules.acceptance_test" - -func TestAccDatadogCloudWorkloadSecurityAgentRulesDatasource(t *testing.T) { - t.Parallel() - _, accProviders := testAccProviders(context.Background(), t) - accProvider := testAccProvider(t, accProviders) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: accProviders, - Steps: []resource.TestStep{ - { - Config: testAccDataSourceCloudWorkloadSecurityAgentRules(), - Check: resource.ComposeTestCheckFunc( - cloudWorkloadSecurityCheckAgentRulesCount(accProvider), - ), - }, - }, - }) -} - -func cloudWorkloadSecurityCheckAgentRulesCount(accProvider func() (*schema.Provider, error)) func(state *terraform.State) error { - return func(state *terraform.State) error { - provider, _ := accProvider() - providerConf := provider.Meta().(*datadog.ProviderConfiguration) - auth := providerConf.Auth - apiInstances := providerConf.DatadogApiInstances - - agentRulesResponse, _, err := apiInstances.GetCSMThreatsApiV2().ListCloudWorkloadSecurityAgentRules(auth) - if err != nil { - return err - } - return cloudWorkloadSecurityAgentRulesCount(state, len(agentRulesResponse.Data)) - } -} - -func cloudWorkloadSecurityAgentRulesCount(state *terraform.State, responseCount int) error { - resourceAttributes := state.RootModule().Resources[tfAgentRulesSource].Primary.Attributes - agentRulesCount, _ := strconv.Atoi(resourceAttributes["agent_rules.#"]) - - if agentRulesCount != responseCount { - return fmt.Errorf("expected %d agent rules got %d agent rules", - responseCount, agentRulesCount) - } - return nil -} - -func testAccDataSourceCloudWorkloadSecurityAgentRules() string { - return ` -data "datadog_cloud_workload_security_agent_rules" "acceptance_test" { -} -` -} diff --git a/datadog/tests/data_source_datadog_csm_threats_agent_rules_test.go b/datadog/tests/data_source_datadog_csm_threats_agent_rules_test.go deleted file mode 100644 index f0be8a911c..0000000000 --- a/datadog/tests/data_source_datadog_csm_threats_agent_rules_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package test - -import ( - "context" - "fmt" - "strconv" - "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 TestAccCSMThreatsAgentRuleDataSource(t *testing.T) { - ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - - agentRuleName := uniqueAgentRuleName(ctx) - dataSourceName := "data.datadog_csm_threats_agent_rules.my_data_source" - agentRuleConfig := fmt.Sprintf(` - resource "datadog_csm_threats_agent_rule" "agent_rule_for_data_source_test" { - name = "%s" - enabled = false - description = "im a rule" - expression = "open.file.name == \"etc/shadow/password\"" - } - `, agentRuleName) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckCSMThreatsAgentRuleDestroy(providers.frameworkProvider), - Steps: []resource.TestStep{ - { - // Create an agent rule to have at least one - Config: agentRuleConfig, - Check: testAccCheckCSMThreatsAgentRuleExists(providers.frameworkProvider, "datadog_csm_threats_agent_rule.agent_rule_for_data_source_test"), - }, - { - Config: fmt.Sprintf(` - %s - data "datadog_csm_threats_agent_rules" "my_data_source" {} - `, agentRuleConfig), - Check: checkCSMThreatsAgentRulesDataSourceContent(providers.frameworkProvider, dataSourceName, agentRuleName), - }, - }, - }) -} - -func checkCSMThreatsAgentRulesDataSourceContent(accProvider *fwprovider.FrameworkProvider, dataSourceName string, agentRuleName string) resource.TestCheckFunc { - return func(state *terraform.State) error { - res, ok := state.RootModule().Resources[dataSourceName] - if !ok { - return fmt.Errorf("resource missing from state: %s", dataSourceName) - } - - auth := accProvider.Auth - apiInstances := accProvider.DatadogApiInstances - - allAgentRulesResponse, _, err := apiInstances.GetCSMThreatsApiV2().ListCSMThreatsAgentRules(auth) - if err != nil { - return err - } - - // Check the agentRule we created is in the API response - agentRuleId := "" - ruleName := "" - for _, rule := range allAgentRulesResponse.GetData() { - if rule.Attributes.GetName() == agentRuleName { - agentRuleId = rule.GetId() - ruleName = rule.Attributes.GetName() - break - } - } - if agentRuleId == "" { - return fmt.Errorf("agent rule with name '%s' not found in API responses", agentRuleName) - } - - // Check that the data_source fetched is correct - resourceAttributes := res.Primary.Attributes - agentRulesIdsCount, err := strconv.Atoi(resourceAttributes["agent_rules_ids.#"]) - if err != nil { - return err - } - agentRulesCount, err := strconv.Atoi(resourceAttributes["agent_rules.#"]) - if err != nil { - return err - } - if agentRulesCount != agentRulesIdsCount { - return fmt.Errorf("the data source contains %d agent rules IDs but %d agent rules", agentRulesIdsCount, agentRulesCount) - } - - // Find in which position is the agent rule we created, and check its values - idx := 0 - for idx < agentRulesIdsCount && resourceAttributes[fmt.Sprintf("agent_rules_ids.%d", idx)] != agentRuleId { - idx++ - } - if idx == len(resourceAttributes) { - return fmt.Errorf("agent rule with ID '%s' not found in data source", agentRuleId) - } - - return resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.name", idx), ruleName), - resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.enabled", idx), "false"), - resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.description", idx), "im a rule"), - resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.expression", idx), "open.file.name == \"etc/shadow/password\""), - )(state) - } -} diff --git a/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go b/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go index 3db6f11767..16cc6d08f3 100644 --- a/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go +++ b/datadog/tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test.go @@ -7,14 +7,14 @@ import ( "testing" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "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 TestAccCSMThreatsMultiPolicyAgentRuleDataSource(t *testing.T) { +func TestAccCSMThreatsMultiPolicyAgentRulesDataSource(t *testing.T) { + t.Parallel() ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) policyName := uniqueAgentRuleName(ctx) @@ -26,23 +26,25 @@ func TestAccCSMThreatsMultiPolicyAgentRuleDataSource(t *testing.T) { tags = ["host_name:test_host"] } `, policyName) + agentRuleName := uniqueAgentRuleName(ctx) agentRuleConfig := fmt.Sprintf(` %s resource "datadog_csm_threats_multi_policy_agent_rule" "agent_rule_for_data_source_test" { name = "%s" - policy_id = datadog_csm_threats_policy.policy_for_test.id + policy_id = datadog_csm_threats_policy.policy_for_test.id enabled = true description = "im a rule" expression = "open.file.name == \"etc/shadow/password\"" + product_tags = ["compliance_framework:PCI-DSS"] } `, policyConfig, agentRuleName) - dataSourceName := "data.datadog_csm_threats_agent_rules.my_data_source" + dataSourceName := "data.datadog_csm_threats_multi_policy_agent_rules.my_data_source" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckCSMThreatsAgentRuleDestroy(providers.frameworkProvider), + CheckDestroy: testAccCheckCSMThreatsMultiPolicyAgentRuleDestroy(providers.frameworkProvider), Steps: []resource.TestStep{ { // Create an agent rule to have at least one @@ -52,7 +54,7 @@ func TestAccCSMThreatsMultiPolicyAgentRuleDataSource(t *testing.T) { { Config: fmt.Sprintf(` %s - data "datadog_csm_threats_agent_rules" "my_data_source" { + data "datadog_csm_threats_multi_policy_agent_rules" "my_data_source" { policy_id = datadog_csm_threats_policy.policy_for_test.id } `, agentRuleConfig), @@ -62,6 +64,30 @@ func TestAccCSMThreatsMultiPolicyAgentRuleDataSource(t *testing.T) { }) } +func testAccCheckDatadogCSMThreatsMultiPolicyAgentRulesDataSourceConfig(policyName, agentRuleName string) string { + return fmt.Sprintf(` +data "datadog_csm_threats_multi_policy_agent_rules" "my_data_source" { + policy_id = datadog_csm_threats_policy.policy.id +} + +resource "datadog_csm_threats_policy" "policy" { + name = "%s" + enabled = true + description = "Test description" + tags = ["host_name:test_host"] +} + +resource "datadog_csm_threats_multi_policy_agent_rule" "agent_rule" { + name = "%s" + description = "Test description" + enabled = true + expression = "open.file.name == \"etc/shadow/password\"" + policy_id = datadog_csm_threats_policy.policy.id + product_tags = ["compliance_framework:PCI-DSS"] +} +`, policyName, agentRuleName) +} + func checkCSMThreatsMultiPolicyAgentRulesDataSourceContent(accProvider *fwprovider.FrameworkProvider, dataSourceName string, agentRuleName string) resource.TestCheckFunc { return func(state *terraform.State) error { res, ok := state.RootModule().Resources[dataSourceName] @@ -120,6 +146,8 @@ func checkCSMThreatsMultiPolicyAgentRulesDataSourceContent(accProvider *fwprovid resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.enabled", idx), "true"), resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.description", idx), "im a rule"), resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.expression", idx), "open.file.name == \"etc/shadow/password\""), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.product_tags.#", idx), "1"), + resource.TestCheckTypeSetElemAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.product_tags.*", idx), "compliance_framework:PCI-DSS"), )(state) } } diff --git a/datadog/tests/resource_datadog_cloud_workload_security_agent_rule_test.go b/datadog/tests/resource_datadog_cloud_workload_security_agent_rule_test.go deleted file mode 100644 index 9ac3ce0b7c..0000000000 --- a/datadog/tests/resource_datadog_cloud_workload_security_agent_rule_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - - "github.com/terraform-providers/terraform-provider-datadog/datadog" -) - -const tfAgentRuleName = "datadog_cloud_workload_security_agent_rule.acceptance_test" - -func TestAccDatadogCloudWorkloadSecurityAgentRule(t *testing.T) { - t.Parallel() - ctx, accProviders := testAccProviders(context.Background(), t) - agentRuleName := strings.Replace(uniqueEntityName(ctx, t), "-", "_", -1) - accProvider := testAccProvider(t, accProviders) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: accProviders, - CheckDestroy: testAccCheckDatadogCloudWorkloadSecurityAgentRuleDestroy(accProvider), - Steps: []resource.TestStep{ - { - Config: testAccCheckDatadogCloudWorkloadSecurityAgentRuleCreated(agentRuleName), - Check: testAccCheckDatadogCloudWorkloadSecurityAgentRuleCreatedCheck(accProvider, agentRuleName), - }, - { - Config: testAccCheckDatadogCloudWorkloadSecurityAgentRuleUpdated(agentRuleName), - Check: testAccCheckDatadogCloudWorkloadSecurityAgentRuleUpdatedCheck(accProvider, agentRuleName), - }, - }, - }) -} - -func testAccCheckDatadogCloudWorkloadSecurityAgentRuleCreated(name string) string { - return fmt.Sprintf(` -resource "datadog_cloud_workload_security_agent_rule" "acceptance_test" { - name = "%s" - description = "an agent rule" - enabled = "true" - expression = "exec.file.name == \"java\"" -} -`, name) -} - -func testAccCheckDatadogCloudWorkloadSecurityAgentRuleCreatedCheck(accProvider func() (*schema.Provider, error), agentRuleName string) resource.TestCheckFunc { - return resource.ComposeTestCheckFunc( - testAccCheckDatadogCloudWorkloadSecurityAgentRuleExists(accProvider), - resource.TestCheckResourceAttr( - tfAgentRuleName, "name", agentRuleName), - resource.TestCheckResourceAttr( - tfAgentRuleName, "description", "an agent rule"), - resource.TestCheckResourceAttr( - tfAgentRuleName, "enabled", "true"), - resource.TestCheckResourceAttr( - tfAgentRuleName, "expression", "exec.file.name == \"java\""), - ) -} - -func testAccCheckDatadogCloudWorkloadSecurityAgentRuleUpdated(name string) string { - return fmt.Sprintf(` -resource "datadog_cloud_workload_security_agent_rule" "acceptance_test" { - name = "%s" - description = "a new agent rule" - enabled = "false" - expression = "exec.file.name == \"go\"" -} -`, name) -} - -func testAccCheckDatadogCloudWorkloadSecurityAgentRuleUpdatedCheck(accProvider func() (*schema.Provider, error), agentRuleName string) resource.TestCheckFunc { - return resource.ComposeTestCheckFunc( - testAccCheckDatadogCloudWorkloadSecurityAgentRuleExists(accProvider), - resource.TestCheckResourceAttr( - tfAgentRuleName, "name", agentRuleName), - resource.TestCheckResourceAttr( - tfAgentRuleName, "description", "a new agent rule"), - resource.TestCheckResourceAttr( - tfAgentRuleName, "enabled", "false"), - resource.TestCheckResourceAttr( - tfAgentRuleName, "expression", "exec.file.name == \"go\""), - ) -} - -func testAccCheckDatadogCloudWorkloadSecurityAgentRuleExists(accProvider func() (*schema.Provider, error)) resource.TestCheckFunc { - return func(s *terraform.State) error { - provider, _ := accProvider() - providerConf := provider.Meta().(*datadog.ProviderConfiguration) - auth := providerConf.Auth - apiInstances := providerConf.DatadogApiInstances - - for _, agentRule := range s.RootModule().Resources { - _, _, err := apiInstances.GetCSMThreatsApiV2().GetCloudWorkloadSecurityAgentRule(auth, agentRule.Primary.ID) - if err != nil { - return fmt.Errorf("received an error retrieving cloud workload security agent rule: %s", err) - } - } - return nil - } -} - -func testAccCheckDatadogCloudWorkloadSecurityAgentRuleDestroy(accProvider func() (*schema.Provider, error)) resource.TestCheckFunc { - return func(s *terraform.State) error { - provider, _ := accProvider() - providerConf := provider.Meta().(*datadog.ProviderConfiguration) - auth := providerConf.Auth - apiInstances := providerConf.DatadogApiInstances - - for _, resource := range s.RootModule().Resources { - if resource.Type == "datadog_cloud_workload_security_agent_rule" { - _, httpResponse, err := apiInstances.GetCSMThreatsApiV2().GetCloudWorkloadSecurityAgentRule(auth, resource.Primary.ID) - if err != nil { - if httpResponse != nil && httpResponse.StatusCode == 404 { - continue - } - return fmt.Errorf("received an error deleting cloud workload security agent rule: %s", err) - } - return fmt.Errorf("cloud workload security agent rule still exists") - } - } - return nil - } - -} diff --git a/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go deleted file mode 100644 index 224c190f82..0000000000 --- a/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package test - -import ( - "context" - "errors" - "fmt" - "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" -) - -// Create an agent rule and update its description -func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { - ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - - agentRuleName := uniqueAgentRuleName(ctx) - resourceName := "datadog_csm_threats_agent_rule.agent_rule_test" - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckCSMThreatsAgentRuleDestroy(providers.frameworkProvider), - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(` - resource "datadog_csm_threats_agent_rule" "agent_rule_test" { - name = "%s" - enabled = true - description = "im a rule" - expression = "open.file.name == \"etc/shadow/password\"" - product_tags = ["compliance_framework:PCI-DSS"] - } - `, agentRuleName), - Check: resource.ComposeTestCheckFunc( - testAccCheckCSMThreatsAgentRuleExists(providers.frameworkProvider, "datadog_csm_threats_agent_rule.agent_rule_test"), - checkCSMThreatsAgentRuleContent( - resourceName, - agentRuleName, - "im a rule", - "open.file.name == \"etc/shadow/password\"", - "compliance_framework:PCI-DSS\"]", - ), - ), - }, - // Update description - { - Config: fmt.Sprintf(` - resource "datadog_csm_threats_agent_rule" "agent_rule_test" { - name = "%s" - enabled = true - description = "updated agent rule for terraform provider test" - expression = "open.file.name == \"etc/shadow/password\"" - product_tags = ["compliance_framework:ISO-27799"] - } - `, agentRuleName), - Check: resource.ComposeTestCheckFunc( - testAccCheckCSMThreatsAgentRuleExists(providers.frameworkProvider, resourceName), - checkCSMThreatsAgentRuleContent( - resourceName, - agentRuleName, - "updated agent rule for terraform provider test", - "open.file.name == \"etc/shadow/password\"", - "compliance_framework:ISO-27799", - ), - ), - }, - }, - }) -} - -func checkCSMThreatsAgentRuleContent(resourceName string, name string, description string, expression string, product_tags string) resource.TestCheckFunc { - return resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", name), - resource.TestCheckResourceAttr(resourceName, "description", description), - resource.TestCheckResourceAttr(resourceName, "enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "expression", expression), - resource.TestCheckResourceAttr(resourceName, "product_tags.#", "1"), - resource.TestCheckTypeSetElemAttr(resourceName, "product_tags.*", product_tags), - ) -} - -func testAccCheckCSMThreatsAgentRuleExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - resource, ok := s.RootModule().Resources[resourceName] - if !ok { - return fmt.Errorf("resource '%s' not found in the state %s", resourceName, s.RootModule().Resources) - } - - if resource.Type != "datadog_csm_threats_agent_rule" { - return fmt.Errorf("resource %s is not of type datadog_csm_threats_agent_rule, found %s instead", resourceName, resource.Type) - } - - auth := accProvider.Auth - apiInstances := accProvider.DatadogApiInstances - - _, _, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentRule(auth, resource.Primary.ID) - if err != nil { - return fmt.Errorf("received an error retrieving agent rule: %s", err) - } - - return nil - } -} - -func testAccCheckCSMThreatsAgentRuleDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { - return func(s *terraform.State) error { - auth := accProvider.Auth - apiInstances := accProvider.DatadogApiInstances - - for _, resource := range s.RootModule().Resources { - if resource.Type == "datadog_csm_threats_agent_rule" { - _, httpResponse, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentRule(auth, resource.Primary.ID) - if err == nil { - return errors.New("agent rule still exists") - } - if httpResponse == nil || httpResponse.StatusCode != 404 { - return fmt.Errorf("received an error while getting the agent rule: %s", err) - } - } - } - - return nil - } -} diff --git a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go index c202af3f9d..ff6ae35cba 100644 --- a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go +++ b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go @@ -136,3 +136,14 @@ func testAccCheckCSMThreatsMultiPolicyAgentRuleDestroy(accProvider *fwprovider.F return nil } } + +func checkCSMThreatsAgentRuleContent(resourceName string, name string, description string, expression string, product_tags string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "expression", expression), + resource.TestCheckResourceAttr(resourceName, "product_tags.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "product_tags.*", product_tags), + ) +} From 1d306ae8d7d8246732ded6fd837d9bf8b66cca39 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Fri, 28 Mar 2025 11:54:03 +0100 Subject: [PATCH 08/14] fix bugs --- ...og_csm_threats_multi_policy_agent_rules.go | 58 +++++-------------- .../data_source_datadog_csm_threats_policy.go | 2 +- ...urce_datadog_csm_threats_multi_policies.go | 6 +- ...dog_csm_threats_multi_policy_agent_rule.go | 15 +---- go.mod | 1 - 5 files changed, 22 insertions(+), 60 deletions(-) diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go b/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go index 478276de5c..23dc98bd02 100644 --- a/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go +++ b/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go @@ -97,41 +97,13 @@ func (r *csmThreatsMultiPolicyAgentRulesDataSource) Read(ctx context.Context, re agentRuleModel.Description = types.StringValue(attributes.GetDescription()) agentRuleModel.Enabled = types.BoolValue(attributes.GetEnabled()) agentRuleModel.Expression = types.StringValue(*attributes.Expression) - - // Handle product tags if they exist - if attributes.ProductTags != nil && len(attributes.GetProductTags()) > 0 { - // Remove duplicates from product tags - uniqueTags := make(map[string]struct{}) - for _, tag := range attributes.GetProductTags() { - uniqueTags[tag] = struct{}{} - } - uniqueTagsSlice := make([]string, 0, len(uniqueTags)) - for tag := range uniqueTags { - uniqueTagsSlice = append(uniqueTagsSlice, tag) - } - - productTags, diags := types.SetValueFrom(ctx, types.StringType, uniqueTagsSlice) - if diags.HasError() { - response.Diagnostics.Append(diags...) - continue - } - agentRuleModel.ProductTags = productTags - } else { - // Create an empty set if no product tags - emptySet, diags := types.SetValueFrom(ctx, types.StringType, []string{}) - if diags.HasError() { - response.Diagnostics.Append(diags...) - continue - } - agentRuleModel.ProductTags = emptySet - } - + agentRuleModel.ProductTags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetProductTags()) agentRuleIds[idx] = agentRule.GetId() agentRules[idx] = agentRuleModel } stateId := strings.Join(agentRuleIds, "--") - state.Id = types.StringValue(computeDataSourceID(&stateId)) + state.Id = types.StringValue(computeMultiPolicyAgentRulesID(&stateId)) tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, agentRuleIds) response.Diagnostics.Append(diags...) state.AgentRulesIds = tfAgentRuleIds @@ -140,6 +112,19 @@ func (r *csmThreatsMultiPolicyAgentRulesDataSource) Read(ctx context.Context, re response.Diagnostics.Append(response.State.Set(ctx, &state)...) } +func computeMultiPolicyAgentRulesID(ids *string) string { + // Key for hashing + var b strings.Builder + if ids != nil { + b.WriteString(*ids) + } + keyStr := b.String() + h := sha256.New() + h.Write([]byte(keyStr)) + + return fmt.Sprintf("%x", h.Sum(nil)) +} + func (*csmThreatsMultiPolicyAgentRulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { response.Schema = schema.Schema{ Description: "Use this data source to retrieve information about existing Agent rules.", @@ -173,16 +158,3 @@ func (*csmThreatsMultiPolicyAgentRulesDataSource) Schema(_ context.Context, _ da }, } } - -func computeDataSourceID(ids *string) string { - // Key for hashing - var b strings.Builder - if ids != nil { - b.WriteString(*ids) - } - keyStr := b.String() - h := sha256.New() - h.Write([]byte(keyStr)) - - return fmt.Sprintf("%x", h.Sum(nil)) -} diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_policy.go b/datadog/fwprovider/data_source_datadog_csm_threats_policy.go index c8a3455b06..1817529ef9 100644 --- a/datadog/fwprovider/data_source_datadog_csm_threats_policy.go +++ b/datadog/fwprovider/data_source_datadog_csm_threats_policy.go @@ -72,7 +72,7 @@ func (r *csmThreatsPoliciesDataSource) Read(ctx context.Context, request datasou } stateId := strings.Join(policyIds, "--") - state.Id = types.StringValue(computeDataSourceID(&stateId)) + state.Id = types.StringValue(computeMultiPolicyAgentRulesID(&stateId)) tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, policyIds) response.Diagnostics.Append(diags...) state.PolicyIds = tfAgentRuleIds diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go index bdee54a318..f8d0cce5de 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go @@ -3,6 +3,7 @@ package fwprovider import ( "context" "fmt" + "sync" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -15,8 +16,9 @@ import ( ) var ( - _ resource.ResourceWithConfigure = &csmThreatsPoliciesListResource{} - _ resource.ResourceWithImportState = &csmThreatsPoliciesListResource{} + csmThreatsMutex sync.Mutex + _ resource.ResourceWithConfigure = &csmThreatsPoliciesListResource{} + _ resource.ResourceWithImportState = &csmThreatsPoliciesListResource{} ) type csmThreatsPoliciesListResource struct { diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go index 5910d7d89d..1f5b588c20 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -3,7 +3,6 @@ package fwprovider import ( "context" "strings" - "sync" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/path" @@ -17,9 +16,8 @@ import ( ) var ( - csmThreatsMutex sync.Mutex - _ resource.ResourceWithConfigure = &csmThreatsMultiPolicyAgentRuleResource{} - _ resource.ResourceWithImportState = &csmThreatsMultiPolicyAgentRuleResource{} + _ resource.ResourceWithConfigure = &csmThreatsMultiPolicyAgentRuleResource{} + _ resource.ResourceWithImportState = &csmThreatsMultiPolicyAgentRuleResource{} ) type csmThreatsMultiPolicyAgentRuleResource struct { @@ -110,9 +108,6 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Create(ctx context.Context, req return } - csmThreatsMutex.Lock() - defer csmThreatsMutex.Unlock() - agentRulePayload, err := r.buildCreateCSMThreatsAgentRulePayload(&state) if err != nil { response.Diagnostics.AddError("error while parsing resource", err.Error()) @@ -166,9 +161,6 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Update(ctx context.Context, req return } - csmThreatsMutex.Lock() - defer csmThreatsMutex.Unlock() - agentRulePayload, err := r.buildUpdateCSMThreatsAgentRulePayload(&state) if err != nil { response.Diagnostics.AddError("error while parsing resource", err.Error()) @@ -195,9 +187,6 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Delete(ctx context.Context, req return } - csmThreatsMutex.Lock() - defer csmThreatsMutex.Unlock() - id := state.Id.ValueString() policyId := state.PolicyId.ValueString() httpResp, err := r.api.DeleteCSMThreatsAgentRule(r.auth, id, *datadogV2.NewDeleteCSMThreatsAgentRuleOptionalParameters().WithPolicyId(policyId)) diff --git a/go.mod b/go.mod index b6da3fa13b..a02074fa79 100644 --- a/go.mod +++ b/go.mod @@ -102,4 +102,3 @@ require ( ) go 1.23.0 -replace github.com/DataDog/datadog-api-client-go/v2 v2.35.0 => ../datadog-api-spec/generated/datadog-api-client-go \ No newline at end of file From 0d839f7eb9d9d37071635fcae105a57dad407556 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Wed, 2 Apr 2025 14:42:20 +0200 Subject: [PATCH 09/14] restore legacy anget_rule resource, fix tests --- ...a_source_datadog_csm_threats_agent_rule.go | 125 ++++++ ...og_csm_threats_multi_policy_agent_rules.go | 50 ++- .../data_source_datadog_csm_threats_policy.go | 2 +- datadog/fwprovider/framework_provider.go | 3 +- ...resource_datadog_csm_threats_agent_rule.go | 238 +++++++++++ ...urce_datadog_csm_threats_multi_policies.go | 378 ------------------ ...dog_csm_threats_multi_policy_agent_rule.go | 15 +- ...rce_datadog_csm_threats_agent_rule_test.go | 109 +++++ datadog/tests/provider_test.go | 1 + ...rce_datadog_csm_threats_agent_rule_test.go | 120 ++++++ ...sm_threats_multi_policy_agent_rule_test.go | 6 +- ..._datadog_csm_threats_policies_list_test.go | 112 ------ go.mod | 1 + 13 files changed, 646 insertions(+), 514 deletions(-) create mode 100644 datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go create mode 100644 datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go delete mode 100644 datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go create mode 100644 datadog/tests/data_source_datadog_csm_threats_agent_rule_test.go create mode 100644 datadog/tests/resource_datadog_csm_threats_agent_rule_test.go delete mode 100644 datadog/tests/resource_datadog_csm_threats_policies_list_test.go diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go b/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go new file mode 100644 index 0000000000..d6e160a1ab --- /dev/null +++ b/datadog/fwprovider/data_source_datadog_csm_threats_agent_rule.go @@ -0,0 +1,125 @@ +package fwprovider + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +var ( + _ datasource.DataSourceWithConfigure = &csmThreatsAgentRulesDataSource{} +) + +type csmThreatsAgentRulesDataSource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +type csmThreatsAgentRulesDataSourceModel struct { + Id types.String `tfsdk:"id"` + AgentRulesIds types.List `tfsdk:"agent_rules_ids"` + AgentRules []csmThreatsAgentRuleModel `tfsdk:"agent_rules"` +} + +func NewCSMThreatsAgentRulesDataSource() datasource.DataSource { + return &csmThreatsAgentRulesDataSource{} +} + +func (r *csmThreatsAgentRulesDataSource) Configure(_ context.Context, request datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (*csmThreatsAgentRulesDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = "csm_threats_agent_rules" +} + +func (r *csmThreatsAgentRulesDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var state csmThreatsAgentRulesDataSourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + res, _, err := r.api.ListCSMThreatsAgentRules(r.auth) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent rules")) + return + } + + data := res.GetData() + agentRuleIds := make([]string, len(data)) + agentRules := make([]csmThreatsAgentRuleModel, len(data)) + + for idx, agentRule := range res.GetData() { + var agentRuleModel csmThreatsAgentRuleModel + agentRuleModel.Id = types.StringValue(agentRule.GetId()) + attributes := agentRule.Attributes + agentRuleModel.Name = types.StringValue(attributes.GetName()) + agentRuleModel.Description = types.StringValue(attributes.GetDescription()) + agentRuleModel.Enabled = types.BoolValue(attributes.GetEnabled()) + agentRuleModel.Expression = types.StringValue(*attributes.Expression) + + agentRuleIds[idx] = agentRule.GetId() + agentRules[idx] = agentRuleModel + } + + stateId := strings.Join(agentRuleIds, "--") + state.Id = types.StringValue(computeAgentRulesDataSourceID(&stateId)) + tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, agentRuleIds) + response.Diagnostics.Append(diags...) + state.AgentRulesIds = tfAgentRuleIds + state.AgentRules = agentRules + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func computeAgentRulesDataSourceID(agentruleIds *string) string { + // Key for hashing + var b strings.Builder + if agentruleIds != nil { + b.WriteString(*agentruleIds) + } + keyStr := b.String() + h := sha256.New() + h.Write([]byte(keyStr)) + + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func (*csmThreatsAgentRulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Use this data source to retrieve information about existing Agent rules.", + Attributes: map[string]schema.Attribute{ + "id": utils.ResourceIDAttribute(), + "agent_rules_ids": schema.ListAttribute{ + Computed: true, + Description: "List of IDs for the Agent rules.", + ElementType: types.StringType, + }, + "agent_rules": schema.ListAttribute{ + Computed: true, + Description: "List of Agent rules", + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "description": types.StringType, + "enabled": types.BoolType, + "expression": types.StringType, + }, + }, + }, + }, + } +} diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go b/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go index 23dc98bd02..1f8d11b101 100644 --- a/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go +++ b/datadog/fwprovider/data_source_datadog_csm_threats_multi_policy_agent_rules.go @@ -97,13 +97,28 @@ func (r *csmThreatsMultiPolicyAgentRulesDataSource) Read(ctx context.Context, re agentRuleModel.Description = types.StringValue(attributes.GetDescription()) agentRuleModel.Enabled = types.BoolValue(attributes.GetEnabled()) agentRuleModel.Expression = types.StringValue(*attributes.Expression) - agentRuleModel.ProductTags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetProductTags()) + tags := attributes.GetProductTags() + tagSet := make(map[string]struct{}) + for _, tag := range tags { + tagSet[tag] = struct{}{} + } + uniqueTags := make([]string, 0, len(tagSet)) + for tag := range tagSet { + uniqueTags = append(uniqueTags, tag) + } + + productTags, diags := types.SetValueFrom(ctx, types.StringType, uniqueTags) + if diags.HasError() { + response.Diagnostics.Append(diags...) + continue + } + agentRuleModel.ProductTags = productTags agentRuleIds[idx] = agentRule.GetId() agentRules[idx] = agentRuleModel } stateId := strings.Join(agentRuleIds, "--") - state.Id = types.StringValue(computeMultiPolicyAgentRulesID(&stateId)) + state.Id = types.StringValue(computeDataSourceID(&stateId)) tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, agentRuleIds) response.Diagnostics.Append(diags...) state.AgentRulesIds = tfAgentRuleIds @@ -112,19 +127,6 @@ func (r *csmThreatsMultiPolicyAgentRulesDataSource) Read(ctx context.Context, re response.Diagnostics.Append(response.State.Set(ctx, &state)...) } -func computeMultiPolicyAgentRulesID(ids *string) string { - // Key for hashing - var b strings.Builder - if ids != nil { - b.WriteString(*ids) - } - keyStr := b.String() - h := sha256.New() - h.Write([]byte(keyStr)) - - return fmt.Sprintf("%x", h.Sum(nil)) -} - func (*csmThreatsMultiPolicyAgentRulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { response.Schema = schema.Schema{ Description: "Use this data source to retrieve information about existing Agent rules.", @@ -135,7 +137,10 @@ func (*csmThreatsMultiPolicyAgentRulesDataSource) Schema(_ context.Context, _ da Optional: true, }, // Output - "id": utils.ResourceIDAttribute(), + "id": schema.StringAttribute{ + Description: "The ID of the data source", + Computed: true, + }, "agent_rules_ids": schema.ListAttribute{ Computed: true, Description: "List of IDs for the Agent rules.", @@ -158,3 +163,16 @@ func (*csmThreatsMultiPolicyAgentRulesDataSource) Schema(_ context.Context, _ da }, } } + +func computeDataSourceID(ids *string) string { + // Key for hashing + var b strings.Builder + if ids != nil { + b.WriteString(*ids) + } + keyStr := b.String() + h := sha256.New() + h.Write([]byte(keyStr)) + + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/datadog/fwprovider/data_source_datadog_csm_threats_policy.go b/datadog/fwprovider/data_source_datadog_csm_threats_policy.go index 1817529ef9..c8a3455b06 100644 --- a/datadog/fwprovider/data_source_datadog_csm_threats_policy.go +++ b/datadog/fwprovider/data_source_datadog_csm_threats_policy.go @@ -72,7 +72,7 @@ func (r *csmThreatsPoliciesDataSource) Read(ctx context.Context, request datasou } stateId := strings.Join(policyIds, "--") - state.Id = types.StringValue(computeMultiPolicyAgentRulesID(&stateId)) + state.Id = types.StringValue(computeDataSourceID(&stateId)) tfAgentRuleIds, diags := types.ListValueFrom(ctx, types.StringType, policyIds) response.Diagnostics.Append(diags...) state.PolicyIds = tfAgentRuleIds diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index c23dde6ed2..9fd49eaf68 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -79,7 +79,7 @@ var Resources = []func() resource.Resource{ NewActionConnectionResource, NewWorkflowAutomationResource, NewAppBuilderAppResource, - NewCSMThreatsPoliciesListResource, + NewCSMThreatsAgentRuleResource, NewCSMThreatsPolicyResource, NewCSMThreatsMultiPolicyAgentRuleResource, } @@ -110,6 +110,7 @@ var Datasources = []func() datasource.DataSource{ NewDatadogSyntheticsGlobalVariableDataSource, NewWorkflowAutomationDataSource, NewDatadogAppBuilderAppDataSource, + NewCSMThreatsAgentRulesDataSource, NewCSMThreatsPoliciesDataSource, } diff --git a/datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go new file mode 100644 index 0000000000..73274922a0 --- /dev/null +++ b/datadog/fwprovider/resource_datadog_csm_threats_agent_rule.go @@ -0,0 +1,238 @@ +package fwprovider + +import ( + "context" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + "github.com/hashicorp/terraform-plugin-framework/path" + "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +var ( + _ resource.ResourceWithConfigure = &csmThreatsAgentRuleResource{} + _ resource.ResourceWithImportState = &csmThreatsAgentRuleResource{} +) + +type csmThreatsAgentRuleModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + Expression types.String `tfsdk:"expression"` +} + +type csmThreatsAgentRuleResource struct { + api *datadogV2.CSMThreatsApi + auth context.Context +} + +func NewCSMThreatsAgentRuleResource() resource.Resource { + return &csmThreatsAgentRuleResource{} +} + +func (r *csmThreatsAgentRuleResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "csm_threats_agent_rule" +} + +func (r *csmThreatsAgentRuleResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() + r.auth = providerData.Auth +} + +func (r *csmThreatsAgentRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Provides a Datadog CSM Threats Agent Rule API resource.", + Attributes: map[string]schema.Attribute{ + "id": utils.ResourceIDAttribute(), + "name": schema.StringAttribute{ + Required: true, + Description: "The name of the Agent rule.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Description: "A description for the Agent rule.", + Default: stringdefault.StaticString(""), + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Required: true, + Description: "Indicates Whether the Agent rule is enabled.", + }, + "expression": schema.StringAttribute{ + Required: true, + Description: "The SECL expression of the Agent rule", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *csmThreatsAgentRuleResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} + +func (r *csmThreatsAgentRuleResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state csmThreatsAgentRuleModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + agentRulePayload, err := r.buildCreateCSMThreatsAgentRulePayload(&state) + if err != nil { + response.Diagnostics.AddError("error while parsing resource", err.Error()) + } + + res, _, err := r.api.CreateCSMThreatsAgentRule(r.auth, *agentRulePayload) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsAgentRuleResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state csmThreatsAgentRuleModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + agentRuleId := state.Id.ValueString() + res, httpResponse, err := r.api.GetCSMThreatsAgentRule(r.auth, agentRuleId) + if err != nil { + if httpResponse != nil && httpResponse.StatusCode == 404 { + response.State.RemoveResource(ctx) + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error fetching agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsAgentRuleResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state csmThreatsAgentRuleModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + agentRulePayload, err := r.buildUpdateCSMThreatsAgentRulePayload(&state) + if err != nil { + response.Diagnostics.AddError("error while parsing resource", err.Error()) + } + + res, _, err := r.api.UpdateCSMThreatsAgentRule(r.auth, state.Id.ValueString(), *agentRulePayload) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating agent rule")) + return + } + if err := utils.CheckForUnparsed(response); err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "response contains unparsed object")) + return + } + + r.updateStateFromResponse(ctx, &state, &res) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *csmThreatsAgentRuleResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state csmThreatsAgentRuleModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + csmThreatsMutex.Lock() + defer csmThreatsMutex.Unlock() + + id := state.Id.ValueString() + + httpResp, err := r.api.DeleteCSMThreatsAgentRule(r.auth, id) + if err != nil { + if httpResp != nil && httpResp.StatusCode == 404 { + return + } + response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting agent rule")) + return + } +} + +func (r *csmThreatsAgentRuleResource) buildCreateCSMThreatsAgentRulePayload(state *csmThreatsAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleCreateRequest, error) { + _, name, description, enabled, expression := r.extractAgentRuleAttributesFromResource(state) + + attributes := datadogV2.CloudWorkloadSecurityAgentRuleCreateAttributes{} + attributes.Expression = expression + attributes.Name = name + attributes.Description = description + attributes.Enabled = &enabled + + data := datadogV2.NewCloudWorkloadSecurityAgentRuleCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) + return datadogV2.NewCloudWorkloadSecurityAgentRuleCreateRequest(*data), nil +} + +func (r *csmThreatsAgentRuleResource) buildUpdateCSMThreatsAgentRulePayload(state *csmThreatsAgentRuleModel) (*datadogV2.CloudWorkloadSecurityAgentRuleUpdateRequest, error) { + agentRuleId, _, description, enabled, _ := r.extractAgentRuleAttributesFromResource(state) + + attributes := datadogV2.CloudWorkloadSecurityAgentRuleUpdateAttributes{} + attributes.Description = description + attributes.Enabled = &enabled + + data := datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) + data.Id = &agentRuleId + return datadogV2.NewCloudWorkloadSecurityAgentRuleUpdateRequest(*data), nil +} + +func (r *csmThreatsAgentRuleResource) extractAgentRuleAttributesFromResource(state *csmThreatsAgentRuleModel) (string, string, *string, bool, string) { + // Mandatory fields + id := state.Id.ValueString() + name := state.Name.ValueString() + enabled := state.Enabled.ValueBool() + expression := state.Expression.ValueString() + description := state.Description.ValueStringPointer() + + return id, name, description, enabled, expression +} + +func (r *csmThreatsAgentRuleResource) updateStateFromResponse(ctx context.Context, state *csmThreatsAgentRuleModel, res *datadogV2.CloudWorkloadSecurityAgentRuleResponse) { + state.Id = types.StringValue(res.Data.GetId()) + + attributes := res.Data.Attributes + + state.Name = types.StringValue(attributes.GetName()) + state.Description = types.StringValue(attributes.GetDescription()) + state.Enabled = types.BoolValue(attributes.GetEnabled()) + state.Expression = types.StringValue(attributes.GetExpression()) +} diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go deleted file mode 100644 index f8d0cce5de..0000000000 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policies.go +++ /dev/null @@ -1,378 +0,0 @@ -package fwprovider - -import ( - "context" - "fmt" - "sync" - - "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "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 ( - csmThreatsMutex sync.Mutex - _ resource.ResourceWithConfigure = &csmThreatsPoliciesListResource{} - _ resource.ResourceWithImportState = &csmThreatsPoliciesListResource{} -) - -type csmThreatsPoliciesListResource struct { - api *datadogV2.CSMThreatsApi - auth context.Context -} - -type csmThreatsPoliciesListModel struct { - ID types.String `tfsdk:"id"` - Policies []csmThreatsPolicyEntryModel `tfsdk:"policies"` -} - -type csmThreatsPolicyEntryModel struct { - PolicyLabel types.String `tfsdk:"policy_label"` - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Enabled types.Bool `tfsdk:"enabled"` - Tags types.Set `tfsdk:"tags"` -} - -func NewCSMThreatsPoliciesListResource() resource.Resource { - return &csmThreatsPoliciesListResource{} -} - -func (r *csmThreatsPoliciesListResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = "csm_threats_policies" -} - -func (r *csmThreatsPoliciesListResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { - providerData := request.ProviderData.(*FrameworkProvider) - r.api = providerData.DatadogApiInstances.GetCSMThreatsApiV2() - r.auth = providerData.Auth -} - -func (r *csmThreatsPoliciesListResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) -} - -func (r *csmThreatsPoliciesListResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: "Manages multiple Datadog CSM Threats policies in a single resource.", - Attributes: map[string]schema.Attribute{ - "id": utils.ResourceIDAttribute(), - }, - Blocks: map[string]schema.Block{ - "policies": schema.SetNestedBlock{ - Description: "Set of policy blocks. Each block requires a unique policy_label.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "policy_label": schema.StringAttribute{ - Description: "The ID of the policy to manage (from csm_threats_policy).", - Required: true, - }, - "id": schema.StringAttribute{ - Description: "The Datadog-assigned policy ID.", - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "Name of the policy.", - Required: true, - }, - "description": schema.StringAttribute{ - Description: "A description for the policy.", - Optional: true, - Computed: true, - }, - "enabled": schema.BoolAttribute{ - Description: "Indicates whether the policy is enabled.", - Optional: true, - Computed: true, - }, - "tags": schema.SetAttribute{ - Description: "Host tags that define where the policy is deployed.", - Optional: true, - ElementType: types.StringType, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func (r *csmThreatsPoliciesListResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - var plan csmThreatsPoliciesListModel - response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) - if response.Diagnostics.HasError() { - return - } - - plan.ID = types.StringValue("policies_list") - - updatedPolicies, err := r.applyBatchPolicies(ctx, []csmThreatsPolicyEntryModel{}, plan.Policies, &response.Diagnostics) - if err != nil { - return - } - - plan.Policies = updatedPolicies - response.Diagnostics.Append(response.State.Set(ctx, &plan)...) -} - -func (r *csmThreatsPoliciesListResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - var state csmThreatsPoliciesListModel - response.Diagnostics.Append(request.State.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - listResponse, httpResp, err := r.api.ListCSMThreatsAgentPolicies(r.auth) - if err != nil { - if httpResp != nil && httpResp.StatusCode == 404 { - response.State.RemoveResource(ctx) - return - } - response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error while fetching agent policies")) - return - } - - apiMap := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) - for _, policy := range listResponse.GetData() { - policyID := policy.GetId() - if policy.Attributes != nil { - apiMap[policyID] = *policy.Attributes - } - } - - newPolicies := make([]csmThreatsPolicyEntryModel, 0, len(state.Policies)) - - // update the state with the latest data from the API, but only for the policies that are already present in the state - for _, policy := range state.Policies { - policyID := policy.ID.ValueString() - attr, found := apiMap[policyID] - if !found { - // policy was deleted outside of Terraform - continue - } - - tags, _ := types.SetValueFrom(ctx, types.StringType, attr.GetHostTags()) - newPolicies = append(newPolicies, csmThreatsPolicyEntryModel{ - PolicyLabel: policy.PolicyLabel, - ID: types.StringValue(policyID), - Name: types.StringValue(attr.GetName()), - Description: types.StringValue(attr.GetDescription()), - Enabled: types.BoolValue(attr.GetEnabled()), - Tags: tags, - }) - } - - state.Policies = newPolicies - response.Diagnostics.Append(response.State.Set(ctx, &state)...) -} - -func (r *csmThreatsPoliciesListResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - var plan, old csmThreatsPoliciesListModel - - response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) - if response.Diagnostics.HasError() { - return - } - response.Diagnostics.Append(request.State.Get(ctx, &old)...) - if response.Diagnostics.HasError() { - return - } - - updatedPolicies, err := r.applyBatchPolicies(ctx, old.Policies, plan.Policies, &response.Diagnostics) - if err != nil { - return - } - - plan.Policies = updatedPolicies - response.Diagnostics.Append(response.State.Set(ctx, &plan)...) -} - -func (r *csmThreatsPoliciesListResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - var state csmThreatsPoliciesListModel - response.Diagnostics.Append(request.State.Get(ctx, &state)...) - if response.Diagnostics.HasError() { - return - } - - _, err := r.applyBatchPolicies(ctx, state.Policies, []csmThreatsPolicyEntryModel{}, &response.Diagnostics) - if err != nil { - return - } - response.State.RemoveResource(ctx) -} - -func (r *csmThreatsPoliciesListResource) applyBatchPolicies(ctx context.Context, oldPolicies []csmThreatsPolicyEntryModel, newPolicies []csmThreatsPolicyEntryModel, diags *diag.Diagnostics) ([]csmThreatsPolicyEntryModel, error) { - oldPoliciesMap := make(map[string]csmThreatsPolicyEntryModel) - for _, policy := range oldPolicies { - oldPoliciesMap[policy.PolicyLabel.ValueString()] = policy - } - - newPoliciesMap := make(map[string]csmThreatsPolicyEntryModel) - for _, policy := range newPolicies { - newPoliciesMap[policy.PolicyLabel.ValueString()] = policy - } - - // check policies that should be deleted (present in old but not in new) - var toDelete []csmThreatsPolicyEntryModel - - for policyLabel, oldPolicy := range oldPoliciesMap { - if _, found := newPoliciesMap[policyLabel]; !found { - toDelete = append(toDelete, oldPolicy) - } - } - - // add policies that should be created or updated (even if they are not modified, we send all policies in the batch request) - var toUpsert []csmThreatsPolicyEntryModel - - // get IDs of existing policies - for _, policy := range newPolicies { - policyLabel := policy.PolicyLabel.ValueString() - if oldPolicy, found := oldPoliciesMap[policyLabel]; found { - policy.ID = oldPolicy.ID - } - toUpsert = append(toUpsert, policy) - } - - var batchItems []datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems - - // add deleted policies to the batch request - for _, policy := range toDelete { - policyID := policy.ID.ValueString() - DeleteTrue := true - item := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ - Id: &policyID, - Delete: &DeleteTrue, - } - batchItems = append(batchItems, item) - } - - // add updated or new policies to the batch request - for _, policy := range toUpsert { - policyID := policy.ID.ValueString() - name := policy.Name.ValueString() - description := policy.Description.ValueString() - enabled := policy.Enabled.ValueBool() - tags := []string{} - if !policy.Tags.IsNull() && !policy.Tags.IsUnknown() { - for _, tag := range policy.Tags.Elements() { - tagStr, ok := tag.(types.String) - if !ok { - return nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) - } - tags = append(tags, tagStr.ValueString()) - } - } - - items := datadogV2.CloudWorkloadSecurityAgentPolicyBatchUpdateAttributesPoliciesItems{ - Name: &name, - Description: &description, - Enabled: &enabled, - HostTags: tags, - } - // if policyID is not empty, it means it's not a new policy: we add the id parameter to the request - if policyID != "" { - items.Id = &policyID - } - batchItems = append(batchItems, items) - } - - if len(batchItems) == 0 { - return newPolicies, nil - } - - patchID := "batch_req" - typ := datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYBATCHUPDATEDATATYPE_POLICIES - attrs := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateAttributes() - attrs.SetPolicies(batchItems) - data := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateData(*attrs, patchID, typ) - batchReq := datadogV2.NewCloudWorkloadSecurityAgentPolicyBatchUpdateRequest(*data) - - batchResp, _, err := r.api.BatchUpdateCSMThreatsAgentPolicy(r.auth, *batchReq) - if err != nil { - *diags = append(*diags, utils.FrameworkErrorDiag(err, "error applying batch policy changes")) - return nil, err - } - - for _, policy := range toDelete { - delete(newPoliciesMap, policy.PolicyLabel.ValueString()) - } - - // get the policies from the response using the ID for modified policies and the name for new policies (because new policies don't have an ID yet) - respMapByID := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) - respMapByName := make(map[string]datadogV2.CloudWorkloadSecurityAgentPolicyAttributes) - - for _, policy := range batchResp.GetData() { - - respID := policy.GetId() - respAttr := policy.Attributes - if respAttr == nil { - continue - } - respMapByID[respID] = *respAttr - respMapByName[respAttr.GetName()] = *respAttr - - } - - // final state of the policies updated with the response from the API - finalMap := make(map[string]csmThreatsPolicyEntryModel, len(newPoliciesMap)) - - for label, policy := range newPoliciesMap { - oldID := policy.ID.ValueString() - oldName := policy.Name.ValueString() - - // if the ID is not empty, it means the policy was either modified or left unchanged - if oldID != "" { - if attr, found := respMapByID[oldID]; found { - tags, _ := types.SetValueFrom(ctx, types.StringType, attr.GetHostTags()) - finalMap[label] = csmThreatsPolicyEntryModel{ - PolicyLabel: policy.PolicyLabel, - ID: types.StringValue(oldID), - Name: types.StringValue(attr.GetName()), - Description: types.StringValue(attr.GetDescription()), - Enabled: types.BoolValue(attr.GetEnabled()), - Tags: tags, - } - continue - } - } - - // if the ID is empty, it means the policy was created - if attr, found := respMapByName[oldName]; found { - finalID := findIDByName(oldName, batchResp.GetData()) - tags, _ := types.SetValueFrom(ctx, types.StringType, attr.GetHostTags()) - finalMap[label] = csmThreatsPolicyEntryModel{ - PolicyLabel: policy.PolicyLabel, - ID: types.StringValue(finalID), - Name: types.StringValue(attr.GetName()), - Description: types.StringValue(attr.GetDescription()), - Enabled: types.BoolValue(attr.GetEnabled()), - Tags: tags, - } - } - } - - finalSlice := make([]csmThreatsPolicyEntryModel, 0, len(finalMap)) - for _, policy := range newPolicies { - if updated, ok := finalMap[policy.PolicyLabel.ValueString()]; ok { - finalSlice = append(finalSlice, updated) - } - } - - return finalSlice, nil -} - -func findIDByName(name string, items []datadogV2.CloudWorkloadSecurityAgentPolicyData) string { - for _, it := range items { - if it.Attributes != nil && it.Attributes.GetName() == name { - return it.GetId() - } - } - return "" -} diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go index 1f5b588c20..4e724823d9 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -3,6 +3,7 @@ package fwprovider import ( "context" "strings" + "sync" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/hashicorp/terraform-plugin-framework/path" @@ -16,8 +17,9 @@ import ( ) var ( - _ resource.ResourceWithConfigure = &csmThreatsMultiPolicyAgentRuleResource{} - _ resource.ResourceWithImportState = &csmThreatsMultiPolicyAgentRuleResource{} + csmThreatsMutex sync.Mutex + _ resource.ResourceWithConfigure = &csmThreatsMultiPolicyAgentRuleResource{} + _ resource.ResourceWithImportState = &csmThreatsMultiPolicyAgentRuleResource{} ) type csmThreatsMultiPolicyAgentRuleResource struct { @@ -85,6 +87,7 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Schema(_ context.Context, _ res Optional: true, ElementType: types.StringType, Description: "The list of product tags associated with the rule", + Computed: true, }, }, } @@ -259,5 +262,11 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) updateStateFromResponse(ctx con state.Description = types.StringValue(attributes.GetDescription()) state.Enabled = types.BoolValue(attributes.GetEnabled()) state.Expression = types.StringValue(attributes.GetExpression()) - state.ProductTags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetProductTags()) + tags := attributes.GetProductTags() + if len(tags) == 0 && state.ProductTags.IsNull() { + state.ProductTags = types.SetNull(types.StringType) + } else { + state.ProductTags, _ = types.SetValueFrom(ctx, types.StringType, tags) + } + } diff --git a/datadog/tests/data_source_datadog_csm_threats_agent_rule_test.go b/datadog/tests/data_source_datadog_csm_threats_agent_rule_test.go new file mode 100644 index 0000000000..f0be8a911c --- /dev/null +++ b/datadog/tests/data_source_datadog_csm_threats_agent_rule_test.go @@ -0,0 +1,109 @@ +package test + +import ( + "context" + "fmt" + "strconv" + "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 TestAccCSMThreatsAgentRuleDataSource(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + agentRuleName := uniqueAgentRuleName(ctx) + dataSourceName := "data.datadog_csm_threats_agent_rules.my_data_source" + agentRuleConfig := fmt.Sprintf(` + resource "datadog_csm_threats_agent_rule" "agent_rule_for_data_source_test" { + name = "%s" + enabled = false + description = "im a rule" + expression = "open.file.name == \"etc/shadow/password\"" + } + `, agentRuleName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsAgentRuleDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + // Create an agent rule to have at least one + Config: agentRuleConfig, + Check: testAccCheckCSMThreatsAgentRuleExists(providers.frameworkProvider, "datadog_csm_threats_agent_rule.agent_rule_for_data_source_test"), + }, + { + Config: fmt.Sprintf(` + %s + data "datadog_csm_threats_agent_rules" "my_data_source" {} + `, agentRuleConfig), + Check: checkCSMThreatsAgentRulesDataSourceContent(providers.frameworkProvider, dataSourceName, agentRuleName), + }, + }, + }) +} + +func checkCSMThreatsAgentRulesDataSourceContent(accProvider *fwprovider.FrameworkProvider, dataSourceName string, agentRuleName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + res, ok := state.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("resource missing from state: %s", dataSourceName) + } + + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + allAgentRulesResponse, _, err := apiInstances.GetCSMThreatsApiV2().ListCSMThreatsAgentRules(auth) + if err != nil { + return err + } + + // Check the agentRule we created is in the API response + agentRuleId := "" + ruleName := "" + for _, rule := range allAgentRulesResponse.GetData() { + if rule.Attributes.GetName() == agentRuleName { + agentRuleId = rule.GetId() + ruleName = rule.Attributes.GetName() + break + } + } + if agentRuleId == "" { + return fmt.Errorf("agent rule with name '%s' not found in API responses", agentRuleName) + } + + // Check that the data_source fetched is correct + resourceAttributes := res.Primary.Attributes + agentRulesIdsCount, err := strconv.Atoi(resourceAttributes["agent_rules_ids.#"]) + if err != nil { + return err + } + agentRulesCount, err := strconv.Atoi(resourceAttributes["agent_rules.#"]) + if err != nil { + return err + } + if agentRulesCount != agentRulesIdsCount { + return fmt.Errorf("the data source contains %d agent rules IDs but %d agent rules", agentRulesIdsCount, agentRulesCount) + } + + // Find in which position is the agent rule we created, and check its values + idx := 0 + for idx < agentRulesIdsCount && resourceAttributes[fmt.Sprintf("agent_rules_ids.%d", idx)] != agentRuleId { + idx++ + } + if idx == len(resourceAttributes) { + return fmt.Errorf("agent rule with ID '%s' not found in data source", agentRuleId) + } + + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.name", idx), ruleName), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.enabled", idx), "false"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.description", idx), "im a rule"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("agent_rules.%d.expression", idx), "open.file.name == \"etc/shadow/password\""), + )(state) + } +} diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go index 0b81a14232..1abbec9977 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -58,6 +58,7 @@ var testFiles2EndpointTags = map[string]string{ "tests/data_source_datadog_application_key_test": "application_keys", "tests/data_source_datadog_cloud_workload_security_agent_rules_test": "cloud-workload-security", "tests/data_source_datadog_action_connection_test": "action_connection", + "tests/data_source_datadog_csm_threats_agent_rule_test": "cloud-workload-security", "tests/data_source_datadog_csm_threats_agent_rules_test": "cloud-workload-security", "tests/data_source_datadog_csm_threats_multi_policy_agent_rules_test": "cloud-workload-security", "tests/data_source_datadog_csm_threats_policies_test": "cloud-workload-security", diff --git a/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go new file mode 100644 index 0000000000..a99e0c7420 --- /dev/null +++ b/datadog/tests/resource_datadog_csm_threats_agent_rule_test.go @@ -0,0 +1,120 @@ +package test + +import ( + "context" + "errors" + "fmt" + "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" +) + +// Create an agent rule and update its description +func TestAccCSMThreatsAgentRule_CreateAndUpdate(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + agentRuleName := uniqueAgentRuleName(ctx) + resourceName := "datadog_csm_threats_agent_rule.agent_rule_test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsAgentRuleDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "datadog_csm_threats_agent_rule" "agent_rule_test" { + name = "%s" + enabled = true + description = "im a rule" + expression = "open.file.name == \"etc/shadow/password\"" + } + `, agentRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsAgentRuleExists(providers.frameworkProvider, "datadog_csm_threats_agent_rule.agent_rule_test"), + checkCSMThreatsAgentRuleContent( + resourceName, + agentRuleName, + "im a rule", + "open.file.name == \"etc/shadow/password\"", + ), + ), + }, + // Update description + { + Config: fmt.Sprintf(` + resource "datadog_csm_threats_agent_rule" "agent_rule_test" { + name = "%s" + enabled = true + description = "updated agent rule for terraform provider test" + expression = "open.file.name == \"etc/shadow/password\"" + } + `, agentRuleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsAgentRuleExists(providers.frameworkProvider, resourceName), + checkCSMThreatsAgentRuleContent( + resourceName, + agentRuleName, + "updated agent rule for terraform provider test", + "open.file.name == \"etc/shadow/password\"", + ), + ), + }, + }, + }) +} + +func checkCSMThreatsAgentRuleContent(resourceName string, name string, description string, expression string) resource.TestCheckFunc { + return resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "expression", expression), + ) +} + +func testAccCheckCSMThreatsAgentRuleExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource '%s' not found in the state %s", resourceName, s.RootModule().Resources) + } + + if resource.Type != "datadog_csm_threats_agent_rule" { + return fmt.Errorf("resource %s is not of type datadog_csm_threats_agent_rule, found %s instead", resourceName, resource.Type) + } + + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + _, _, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentRule(auth, resource.Primary.ID) + if err != nil { + return fmt.Errorf("received an error retrieving agent rule: %s", err) + } + + return nil + } +} + +func testAccCheckCSMThreatsAgentRuleDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { + return func(s *terraform.State) error { + auth := accProvider.Auth + apiInstances := accProvider.DatadogApiInstances + + for _, resource := range s.RootModule().Resources { + if resource.Type == "datadog_csm_threats_agent_rule" { + _, httpResponse, err := apiInstances.GetCSMThreatsApiV2().GetCSMThreatsAgentRule(auth, resource.Primary.ID) + if err == nil { + return errors.New("agent rule still exists") + } + if httpResponse == nil || httpResponse.StatusCode != 404 { + return fmt.Errorf("received an error while getting the agent rule: %s", err) + } + } + } + + return nil + } +} diff --git a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go index ff6ae35cba..120c2fc667 100644 --- a/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go +++ b/datadog/tests/resource_datadog_csm_threats_multi_policy_agent_rule_test.go @@ -54,7 +54,7 @@ func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { `, policyConfig, agentRuleName), Check: resource.ComposeTestCheckFunc( testAccCheckCSMThreatsMultiPolicyAgentRuleExists(providers.frameworkProvider, resourceName), - checkCSMThreatsAgentRuleContent( + checkCSMThreatsAgentRuleContentMultiPolicy( resourceName, agentRuleName, "im a rule", @@ -78,7 +78,7 @@ func TestAccCSMThreatsMultiPolicyAgentRule_CreateAndUpdate(t *testing.T) { `, policyConfig, agentRuleName), Check: resource.ComposeTestCheckFunc( testAccCheckCSMThreatsMultiPolicyAgentRuleExists(providers.frameworkProvider, resourceName), - checkCSMThreatsAgentRuleContent( + checkCSMThreatsAgentRuleContentMultiPolicy( resourceName, agentRuleName, "updated agent rule for terraform provider test", @@ -137,7 +137,7 @@ func testAccCheckCSMThreatsMultiPolicyAgentRuleDestroy(accProvider *fwprovider.F } } -func checkCSMThreatsAgentRuleContent(resourceName string, name string, description string, expression string, product_tags string) resource.TestCheckFunc { +func checkCSMThreatsAgentRuleContentMultiPolicy(resourceName string, name string, description string, expression string, product_tags string) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "description", description), diff --git a/datadog/tests/resource_datadog_csm_threats_policies_list_test.go b/datadog/tests/resource_datadog_csm_threats_policies_list_test.go deleted file mode 100644 index 06f2cee752..0000000000 --- a/datadog/tests/resource_datadog_csm_threats_policies_list_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package test - -import ( - "context" - "fmt" - "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" -) - -// Create a policies_list and update the name and priority of its policy -func TestAccCSMThreatsPolicies_CreateAndUpdate(t *testing.T) { - _, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) - - resourceName := "datadog_csm_threats_policies.all_policies" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV5ProviderFactories: accProviders, - CheckDestroy: testAccCheckCSMThreatsPoliciesDestroy(providers.frameworkProvider), - Steps: []resource.TestStep{ - { - Config: testAccCSMThreatsPoliciesConfig(), - Check: resource.ComposeTestCheckFunc( - testAccCheckCSMThreatsPoliciesExists(providers.frameworkProvider, resourceName), - resource.TestCheckResourceAttr(resourceName, "policies.0.name", "terraform_policy"), - resource.TestCheckResourceAttr(resourceName, "policies.0.description", "description"), - resource.TestCheckResourceAttr(resourceName, "policies.0.enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "policies.0.tags.0", "env:staging"), - ), - }, - { - Config: testAccCSMThreatsPoliciesConfigUpdate(), - Check: resource.ComposeTestCheckFunc( - testAccCheckCSMThreatsPoliciesExists(providers.frameworkProvider, resourceName), - resource.TestCheckResourceAttr(resourceName, "policies.0.name", "terraform_policy updated"), - resource.TestCheckResourceAttr(resourceName, "policies.0.description", "new description"), - resource.TestCheckResourceAttr(resourceName, "policies.0.enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "policies.0.tags.0", "foo:bar"), - ), - }, - }, - }) -} - -func testAccCheckCSMThreatsPoliciesExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] - if !ok { - return fmt.Errorf("resource '%s' not found in state", resourceName) - } - if rs.Type != "datadog_csm_threats_policies" { - return fmt.Errorf( - "resource %s is not a datadog_csm_threats_policies, got: %s", - resourceName, - rs.Type, - ) - } - - if rs.Primary.ID != "policies_list" { - return fmt.Errorf("expected resource ID to be 'policies_list', got %s", rs.Primary.ID) - } - - return nil - } -} - -func testAccCheckCSMThreatsPoliciesDestroy(accProvider *fwprovider.FrameworkProvider) resource.TestCheckFunc { - return func(s *terraform.State) error { - for _, r := range s.RootModule().Resources { - if r.Type != "datadog_csm_threats_policies" { - continue - } - - if _, ok := s.RootModule().Resources[r.Primary.ID]; ok { - return fmt.Errorf("Resource %s still exists in state", r.Primary.ID) - } - } - return nil - } -} - -func testAccCSMThreatsPoliciesConfig() string { - return ` - resource "datadog_csm_threats_policies" "all_policies" { - policies { - policy_label = "policy" - name = "terraform_policy" - description = "description" - enabled = false - tags = ["env:staging"] - } - } - ` -} - -func testAccCSMThreatsPoliciesConfigUpdate() string { - return ` - resource "datadog_csm_threats_policies" "all_policies" { - policies { - policy_label = "policy" - name = "terraform_policy updated" - description = "new description" - enabled = true - tags = ["foo:bar"] - } - } - ` -} diff --git a/go.mod b/go.mod index a02074fa79..5225487d19 100644 --- a/go.mod +++ b/go.mod @@ -102,3 +102,4 @@ require ( ) go 1.23.0 +replace github.com/DataDog/datadog-api-client-go/v2 v2.36.1 => ../datadog-api-spec/generated/datadog-api-client-go \ No newline at end of file From 37ea69b5fcb1a21c3510cc89e902832fede8ff33 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Thu, 3 Apr 2025 14:05:51 +0200 Subject: [PATCH 10/14] generate docs --- .../cloud_workload_security_agent_rules.md | 37 ---------------- docs/data-sources/csm_threats_agent_rules.md | 4 -- .../csm_threats_multi_policy_agent_rules.md | 38 +++++++++++++++++ docs/resources/csm_threats_policies.md | 42 ------------------- docs/resources/csm_threats_policy.md | 5 ++- go.mod | 3 +- 6 files changed, 43 insertions(+), 86 deletions(-) delete mode 100644 docs/data-sources/cloud_workload_security_agent_rules.md create mode 100644 docs/data-sources/csm_threats_multi_policy_agent_rules.md delete mode 100644 docs/resources/csm_threats_policies.md diff --git a/docs/data-sources/cloud_workload_security_agent_rules.md b/docs/data-sources/cloud_workload_security_agent_rules.md deleted file mode 100644 index 6bfafbe24b..0000000000 --- a/docs/data-sources/cloud_workload_security_agent_rules.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "datadog_cloud_workload_security_agent_rules Data Source - terraform-provider-datadog" -subcategory: "" -description: |- - Use this data source to retrieve information about existing Cloud Workload Security Agent Rules for use in other resources. Deprecated, use datadog_csm_threats_agent_rules data source instead: https://registry.terraform.io/providers/DataDog/datadog/latest/docs/data-sources/csm_threats_agent_rules ---- - -# datadog_cloud_workload_security_agent_rules (Data Source) - -Use this data source to retrieve information about existing Cloud Workload Security Agent Rules for use in other resources. Deprecated, use datadog_csm_threats_agent_rules data source instead: https://registry.terraform.io/providers/DataDog/datadog/latest/docs/data-sources/csm_threats_agent_rules - -## Example Usage - -```terraform -data "datadog_cloud_workload_security_agent_rules" "test" { -} -``` - - -## Schema - -### Read-Only - -- `agent_rules` (List of Object) List of Agent rules. (see [below for nested schema](#nestedatt--agent_rules)) -- `id` (String) The ID of this resource. - - -### Nested Schema for `agent_rules` - -Read-Only: - -- `description` (String) -- `enabled` (Boolean) -- `expression` (String) -- `id` (String) -- `name` (String) diff --git a/docs/data-sources/csm_threats_agent_rules.md b/docs/data-sources/csm_threats_agent_rules.md index 6f5ea6e33a..6e6e7a0d19 100644 --- a/docs/data-sources/csm_threats_agent_rules.md +++ b/docs/data-sources/csm_threats_agent_rules.md @@ -15,10 +15,6 @@ Use this data source to retrieve information about existing Agent rules. ## Schema -### Optional - -- `policy_id` (String) Listing only the rules in the policy with this field as the ID - ### Read-Only - `agent_rules` (List of Object) List of Agent rules (see [below for nested schema](#nestedatt--agent_rules)) diff --git a/docs/data-sources/csm_threats_multi_policy_agent_rules.md b/docs/data-sources/csm_threats_multi_policy_agent_rules.md new file mode 100644 index 0000000000..db71337c99 --- /dev/null +++ b/docs/data-sources/csm_threats_multi_policy_agent_rules.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_csm_threats_multi_policy_agent_rules Data Source - terraform-provider-datadog" +subcategory: "" +description: |- + Use this data source to retrieve information about existing Agent rules. +--- + +# datadog_csm_threats_multi_policy_agent_rules (Data Source) + +Use this data source to retrieve information about existing Agent rules. + + + + +## Schema + +### Optional + +- `policy_id` (String) Listing only the rules in the policy with this field as the ID + +### Read-Only + +- `agent_rules` (List of Object) List of Agent rules (see [below for nested schema](#nestedatt--agent_rules)) +- `agent_rules_ids` (List of String) List of IDs for the Agent rules. +- `id` (String) The ID of the data source + + +### Nested Schema for `agent_rules` + +Read-Only: + +- `description` (String) +- `enabled` (Boolean) +- `expression` (String) +- `id` (String) +- `name` (String) +- `product_tags` (Set of String) diff --git a/docs/resources/csm_threats_policies.md b/docs/resources/csm_threats_policies.md deleted file mode 100644 index 3143eb1d21..0000000000 --- a/docs/resources/csm_threats_policies.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "datadog_csm_threats_policies Resource - terraform-provider-datadog" -subcategory: "" -description: |- - Manages multiple Datadog CSM Threats policies in a single resource. ---- - -# datadog_csm_threats_policies (Resource) - -Manages multiple Datadog CSM Threats policies in a single resource. - - - - -## Schema - -### Optional - -- `policies` (Block Set) Set of policy blocks. Each block requires a unique policy_label. (see [below for nested schema](#nestedblock--policies)) - -### Read-Only - -- `id` (String) The ID of this resource. - - -### Nested Schema for `policies` - -Required: - -- `name` (String) Name of the policy. -- `policy_label` (String) The ID of the policy to manage (from csm_threats_policy). - -Optional: - -- `description` (String) A description for the policy. -- `enabled` (Boolean) Indicates whether the policy is enabled. -- `tags` (Set of String) Host tags that define where the policy is deployed. - -Read-Only: - -- `id` (String) The Datadog-assigned policy ID. diff --git a/docs/resources/csm_threats_policy.md b/docs/resources/csm_threats_policy.md index 85b84a448c..ac2c43b584 100644 --- a/docs/resources/csm_threats_policy.md +++ b/docs/resources/csm_threats_policy.md @@ -15,6 +15,10 @@ Provides a Datadog CSM Threats policy API resource. ## Schema +### Required + +- `name` (String) The name of the policy. + ### Optional - `description` (String) A description for the policy. @@ -24,4 +28,3 @@ Provides a Datadog CSM Threats policy API resource. ### Read-Only - `id` (String) The ID of this resource. -- `name` (String) The name of the policy. diff --git a/go.mod b/go.mod index 5225487d19..0590bf5dd5 100644 --- a/go.mod +++ b/go.mod @@ -101,5 +101,4 @@ require ( google.golang.org/protobuf v1.36.3 // indirect ) -go 1.23.0 -replace github.com/DataDog/datadog-api-client-go/v2 v2.36.1 => ../datadog-api-spec/generated/datadog-api-client-go \ No newline at end of file +go 1.23.0 \ No newline at end of file From 072d311b283f55e277a62e3f7763f07ace202112 Mon Sep 17 00:00:00 2001 From: Laure Runser Date: Fri, 18 Apr 2025 15:41:45 +0200 Subject: [PATCH 11/14] [cws-4175] add `hostTagsLists` field --- .../resource_datadog_csm_threats_policy.go | 56 ++++++++++--- ...ource_datadog_csm_threats_policies_test.go | 5 ++ ...esource_datadog_csm_threats_policy_test.go | 83 +++++++++++++++++++ docs/resources/csm_threats_policy.md | 3 +- 4 files changed, 134 insertions(+), 13 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_csm_threats_policy.go b/datadog/fwprovider/resource_datadog_csm_threats_policy.go index 2569747259..8ca301d792 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_policy.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_policy.go @@ -15,11 +15,12 @@ import ( ) type csmThreatsPolicyModel struct { - Id types.String `tfsdk:"id"` - Tags types.Set `tfsdk:"tags"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Enabled types.Bool `tfsdk:"enabled"` + Id types.String `tfsdk:"id"` + Tags types.Set `tfsdk:"tags"` + HostTagsLists types.Set `tfsdk:"host_tags_lists"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` } type csmThreatsPolicyResource struct { @@ -63,10 +64,17 @@ func (r *csmThreatsPolicyResource) Schema(_ context.Context, _ resource.SchemaRe }, "tags": schema.SetAttribute{ Optional: true, - Description: "Host tags that define where the policy is deployed.", + Description: "Host tags that define where the policy is deployed. Deprecated, use host_tags_lists instead.", ElementType: types.StringType, Computed: true, }, + "host_tags_lists": schema.SetAttribute{ + Optional: true, + Description: "Host tags that define where the policy is deployed. Inner values are ANDed, outer arrays are ORed.", + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, }, } } @@ -182,7 +190,7 @@ func (r *csmThreatsPolicyResource) Delete(ctx context.Context, request resource. } func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyCreateRequest, error) { - _, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + _, name, description, enabled, tags, hostTagsLists, err := r.extractPolicyAttributesFromResource(state) if err != nil { return nil, err } @@ -192,13 +200,14 @@ func (r *csmThreatsPolicyResource) buildCreateCSMThreatsPolicyPayload(state *csm attributes.Description = description attributes.Enabled = enabled attributes.HostTags = tags + attributes.HostTagsLists = hostTagsLists data := datadogV2.NewCloudWorkloadSecurityAgentPolicyCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYTYPE_POLICY) return datadogV2.NewCloudWorkloadSecurityAgentPolicyCreateRequest(*data), nil } func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csmThreatsPolicyModel) (*datadogV2.CloudWorkloadSecurityAgentPolicyUpdateRequest, error) { - policyId, name, description, enabled, tags, err := r.extractPolicyAttributesFromResource(state) + policyId, name, description, enabled, tags, hostTagsLists, err := r.extractPolicyAttributesFromResource(state) if err != nil { return nil, err } @@ -207,13 +216,14 @@ func (r *csmThreatsPolicyResource) buildUpdateCSMThreatsPolicyPayload(state *csm attributes.Description = description attributes.Enabled = enabled attributes.HostTags = tags + attributes.HostTagsLists = hostTagsLists data := datadogV2.NewCloudWorkloadSecurityAgentPolicyUpdateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTPOLICYTYPE_POLICY) data.Id = &policyId return datadogV2.NewCloudWorkloadSecurityAgentPolicyUpdateRequest(*data), nil } -func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, string, *string, *bool, []string, error) { +func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *csmThreatsPolicyModel) (string, string, *string, *bool, []string, [][]string, error) { // Mandatory fields id := state.Id.ValueString() name := state.Name.ValueString() @@ -224,13 +234,30 @@ func (r *csmThreatsPolicyResource) extractPolicyAttributesFromResource(state *cs for _, tag := range state.Tags.Elements() { tagStr, ok := tag.(types.String) if !ok { - return "", "", nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) + return "", "", nil, nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", tag) } tags = append(tags, tagStr.ValueString()) } } - - return id, name, description, enabled, tags, nil + var hostTagsLists [][]string + if !state.HostTagsLists.IsNull() && !state.HostTagsLists.IsUnknown() { + for _, hostTagList := range state.HostTagsLists.Elements() { + hostTagListStr, ok := hostTagList.(types.List) + if !ok { + return "", "", nil, nil, nil, nil, fmt.Errorf("expected item to be of type types.List, got %T", hostTagList) + } + var tags []string + for _, hostTag := range hostTagListStr.Elements() { + hostTagStr, ok := hostTag.(types.String) + if !ok { + return "", "", nil, nil, nil, nil, fmt.Errorf("expected item to be of type types.String, got %T", hostTag) + } + tags = append(tags, hostTagStr.ValueString()) + } + hostTagsLists = append(hostTagsLists, tags) + } + } + return id, name, description, enabled, tags, hostTagsLists, nil } func (r *csmThreatsPolicyResource) updateStateFromResponse(ctx context.Context, state *csmThreatsPolicyModel, res *datadogV2.CloudWorkloadSecurityAgentPolicyResponse) { @@ -242,4 +269,9 @@ func (r *csmThreatsPolicyResource) updateStateFromResponse(ctx context.Context, state.Description = types.StringValue(attributes.GetDescription()) state.Enabled = types.BoolValue(attributes.GetEnabled()) state.Tags, _ = types.SetValueFrom(ctx, types.StringType, attributes.GetHostTags()) + state.HostTagsLists, _ = types.SetValueFrom(ctx, types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, attributes.GetHostTagsLists()) } diff --git a/datadog/tests/data_source_datadog_csm_threats_policies_test.go b/datadog/tests/data_source_datadog_csm_threats_policies_test.go index 15a9f322d4..f9bed4ab41 100644 --- a/datadog/tests/data_source_datadog_csm_threats_policies_test.go +++ b/datadog/tests/data_source_datadog_csm_threats_policies_test.go @@ -23,6 +23,7 @@ func TestAccCSMThreatsPoliciesDataSource(t *testing.T) { enabled = true description = "im a policy" tags = ["host_name:test_host"] + host_tags_lists = [["host_name:test_host", "env:prod"], ["host_name:test_host2", "env:staging"]] } `, policyName) @@ -101,6 +102,10 @@ func checkCSMThreatsPoliciesDataSourceContent(accProvider *fwprovider.FrameworkP resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.name", idx), policyName), resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.enabled", idx), "true"), resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.tags.0", idx), "host_name:test_host"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.host_tags_lists.0.0", idx), "host_name:test_host"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.host_tags_lists.0.1", idx), "env:prod"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.host_tags_lists.1.0", idx), "host_name:test_host2"), + resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.host_tags_lists.1.1", idx), "env:staging"), resource.TestCheckResourceAttr(dataSourceName, fmt.Sprintf("policies.%d.description", idx), "im a policy"), )(state) } diff --git a/datadog/tests/resource_datadog_csm_threats_policy_test.go b/datadog/tests/resource_datadog_csm_threats_policy_test.go index 4e9099c01a..4eca2fdd0f 100644 --- a/datadog/tests/resource_datadog_csm_threats_policy_test.go +++ b/datadog/tests/resource_datadog_csm_threats_policy_test.go @@ -67,6 +67,68 @@ func TestAccCSMThreatsPolicy_CreateAndUpdate(t *testing.T) { }) } +// Create an agent policy with host_tags_lists and update its description +func TestAccCSMThreatsPolicy_CreateAndUpdateWithHostTagsLists(t *testing.T) { + ctx, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + policyName := uniqueAgentRuleName(ctx) + resourceName := "datadog_csm_threats_policy.policy_test" + hostTagsLists := [][]string{ + {"host_name:test_host", "env:prod"}, + {"host_name:test_host2", "env:staging"}, + } + updatedHostTagsLists := [][]string{ + {"host_name:test", "env:prod"}, + {"host_name:test_host2", "env:test"}, + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckCSMThreatsPolicyDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_test" { + name = "%s" + enabled = true + description = "im a policy" + host_tags_lists = [["host_name:test_host", "env:prod"], ["host_name:test_host2", "env:staging"]] + } + `, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, "datadog_csm_threats_policy.policy_test"), + checkCSMThreatsPolicyContentWithHostTagsLists( + resourceName, + policyName, + "im a policy", + hostTagsLists, + ), + ), + }, + // Update description and tags + { + Config: fmt.Sprintf(` + resource "datadog_csm_threats_policy" "policy_test" { + name = "%s" + enabled = true + description = "updated policy for terraform provider test" + host_tags_lists = [["host_name:test", "env:prod"], ["host_name:test_host2", "env:test"]] + } + `, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCSMThreatsPolicyExists(providers.frameworkProvider, resourceName), + checkCSMThreatsPolicyContentWithHostTagsLists( + resourceName, + policyName, + "updated policy for terraform provider test", + updatedHostTagsLists, + ), + ), + }, + }, + }) +} + func checkCSMThreatsPolicyContent(resourceName string, name string, description string, tags []string) resource.TestCheckFunc { return resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", name), @@ -76,6 +138,27 @@ func checkCSMThreatsPolicyContent(resourceName string, name string, description ) } +func checkCSMThreatsPolicyContentWithHostTagsLists(resourceName string, name string, description string, hostTagsLists [][]string) resource.TestCheckFunc { + checks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + } + + // Add checks for each host tags list + for i, tagList := range hostTagsLists { + for j, tag := range tagList { + checks = append(checks, resource.TestCheckResourceAttr( + resourceName, + fmt.Sprintf("host_tags_lists.%d.%d", i, j), + tag, + )) + } + } + + return resource.ComposeTestCheckFunc(checks...) +} + func testAccCheckCSMThreatsPolicyExists(accProvider *fwprovider.FrameworkProvider, resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { resource, ok := s.RootModule().Resources[resourceName] diff --git a/docs/resources/csm_threats_policy.md b/docs/resources/csm_threats_policy.md index ac2c43b584..61039b47f7 100644 --- a/docs/resources/csm_threats_policy.md +++ b/docs/resources/csm_threats_policy.md @@ -23,7 +23,8 @@ Provides a Datadog CSM Threats policy API resource. - `description` (String) A description for the policy. - `enabled` (Boolean) Indicates whether the policy is enabled. Defaults to `false`. -- `tags` (Set of String) Host tags that define where the policy is deployed. +- `host_tags_lists` (Set of List of String) Host tags that define where the policy is deployed. Inner values are ANDed, outer arrays are ORed. +- `tags` (Set of String) Host tags that define where the policy is deployed. Deprecated, use host_tags_lists instead. ### Read-Only From 5da9e51f3c1427d04c5f834ea861a2935dd3ad41 Mon Sep 17 00:00:00 2001 From: "sarra.zaghbib" Date: Tue, 29 Apr 2025 13:43:33 +0200 Subject: [PATCH 12/14] initial commit --- go.mod | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0590bf5dd5..053540190b 100644 --- a/go.mod +++ b/go.mod @@ -101,4 +101,9 @@ require ( google.golang.org/protobuf v1.36.3 // indirect ) -go 1.23.0 \ No newline at end of file +<<<<<<< Updated upstream +go 1.23.0 +======= +go 1.23.0 +replace github.com/DataDog/datadog-api-client-go/v2 v2.31.0 => ../datadog-api-client-go +>>>>>>> Stashed changes From 194f7daa102bbd74d1c53dbd4c88d68d8d16813b Mon Sep 17 00:00:00 2001 From: "sarra.zaghbib" Date: Tue, 29 Apr 2025 15:47:15 +0200 Subject: [PATCH 13/14] tmp --- .../resource_datadog_csm_threats_multi_policy_agent_rule.go | 6 ++++++ go.mod | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go index 4e724823d9..8fadea22e2 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -35,6 +35,7 @@ type csmThreatsMultiPolicyAgentRuleModel struct { Enabled types.Bool `tfsdk:"enabled"` Expression types.String `tfsdk:"expression"` ProductTags types.Set `tfsdk:"product_tags"` + Actions types.Array `tfsdk:"actions"` } func NewCSMThreatsMultiPolicyAgentRuleResource() resource.Resource { @@ -89,6 +90,11 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Schema(_ context.Context, _ res Description: "The list of product tags associated with the rule", Computed: true, }, + "actions": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "The array of actions the rule can perform if triggered", + }, }, } } diff --git a/go.mod b/go.mod index 053540190b..81e450d30c 100644 --- a/go.mod +++ b/go.mod @@ -101,9 +101,6 @@ require ( google.golang.org/protobuf v1.36.3 // indirect ) -<<<<<<< Updated upstream -go 1.23.0 -======= go 1.23.0 replace github.com/DataDog/datadog-api-client-go/v2 v2.31.0 => ../datadog-api-client-go ->>>>>>> Stashed changes + From d6cf07926da3fd2695584160466fb49ea5ce7a5c Mon Sep 17 00:00:00 2001 From: "sarra.zaghbib" Date: Mon, 26 May 2025 14:22:37 +0200 Subject: [PATCH 14/14] changes --- ...dog_csm_threats_multi_policy_agent_rule.go | 33 +++++++++++++++++-- go.mod | 2 +- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go index 8fadea22e2..db8c3a6c04 100644 --- a/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go +++ b/datadog/fwprovider/resource_datadog_csm_threats_multi_policy_agent_rule.go @@ -35,7 +35,7 @@ type csmThreatsMultiPolicyAgentRuleModel struct { Enabled types.Bool `tfsdk:"enabled"` Expression types.String `tfsdk:"expression"` ProductTags types.Set `tfsdk:"product_tags"` - Actions types.Array `tfsdk:"actions"` + Actions types.List `tfsdk:"actions"` } func NewCSMThreatsMultiPolicyAgentRuleResource() resource.Resource { @@ -93,7 +93,7 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) Schema(_ context.Context, _ res "actions": schema.ListAttribute{ Optional: true, ElementType: types.StringType, - Description: "The array of actions the rule can perform if triggered", + Description: "The array of action names the rule can perform if triggered. Note: Actions can only be set during creation and cannot be updated.", }, }, } @@ -219,6 +219,22 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) buildCreateCSMThreatsAgentRuleP attributes.PolicyId = &policyId attributes.ProductTags = productTags + if !state.Actions.IsNull() && !state.Actions.IsUnknown() { + var actionNames []string + state.Actions.ElementsAs(context.Background(), &actionNames, false) + if len(actionNames) > 0 { + actions := make([]datadogV2.CloudWorkloadSecurityAgentRuleAction, len(actionNames)) + for i, actionName := range actionNames { + setAction := datadogV2.NewCloudWorkloadSecurityAgentRuleActionSet() + setAction.SetName(actionName) + ruleAction := datadogV2.NewCloudWorkloadSecurityAgentRuleAction() + ruleAction.SetSet(*setAction) + actions[i] = *ruleAction + } + attributes.SetActions(actions) + } + } + data := datadogV2.NewCloudWorkloadSecurityAgentRuleCreateData(attributes, datadogV2.CLOUDWORKLOADSECURITYAGENTRULETYPE_AGENT_RULE) return datadogV2.NewCloudWorkloadSecurityAgentRuleCreateRequest(*data), nil } @@ -245,6 +261,7 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) extractAgentRuleAttributesFromR enabled := state.Enabled.ValueBool() expression := state.Expression.ValueString() description := state.Description.ValueStringPointer() + var productTags []string if !state.ProductTags.IsNull() && !state.ProductTags.IsUnknown() { for _, tag := range state.ProductTags.Elements() { @@ -275,4 +292,16 @@ func (r *csmThreatsMultiPolicyAgentRuleResource) updateStateFromResponse(ctx con state.ProductTags, _ = types.SetValueFrom(ctx, types.StringType, tags) } + actions := attributes.GetActions() + if len(actions) > 0 { + actionNames := make([]string, 0, len(actions)) + for _, action := range actions { + if action.Set != nil { + actionNames = append(actionNames, action.Set.GetName()) + } + } + state.Actions, _ = types.ListValueFrom(ctx, types.StringType, actionNames) + } else { + state.Actions = types.ListNull(types.StringType) + } } diff --git a/go.mod b/go.mod index 81e450d30c..13fe6151b1 100644 --- a/go.mod +++ b/go.mod @@ -102,5 +102,5 @@ require ( ) go 1.23.0 -replace github.com/DataDog/datadog-api-client-go/v2 v2.31.0 => ../datadog-api-client-go +replace github.com/DataDog/datadog-api-client-go/v2 v2.36.1 => ../datadog-api-spec/generated/datadog-api-client-go