From 5da3fb7578f3446b36fcfe6b3f8571db8e7b722a 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 1/5] 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 e6bc7cd901..be2baa9b85 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -66,6 +66,8 @@ var Resources = []func() resource.Resource{ NewWebhookResource, NewWebhookCustomVariableResource, NewLogsCustomDestinationResource, + NewCSMThreatsPolicyResource, + NewCSMThreatsMultiPolicyAgentRuleResource, } var Datasources = []func() datasource.DataSource{ @@ -86,6 +88,7 @@ var Datasources = []func() datasource.DataSource{ NewDatadogRoleUsersDataSource, NewSecurityMonitoringSuppressionDataSource, NewCSMThreatsAgentRulesDataSource, + 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 5b1c8984f4..ab04ea402c 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -57,6 +57,8 @@ 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_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", @@ -108,6 +110,8 @@ var testFiles2EndpointTags = map[string]string{ "tests/resource_datadog_cloud_configuration_rule_test": "security-monitoring", "tests/resource_datadog_cloud_workload_security_agent_rule_test": "cloud_workload_security", "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 ecd85ce87912aaa531904679fb016372ce4714d0 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Thu, 30 Jan 2025 14:50:03 +0100 Subject: [PATCH 2/5] 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 +- go.mod | 1 + 6 files changed, 305 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 36896a089c..06875cdc63 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -70,6 +70,7 @@ var Resources = []func() resource.Resource{ NewWebhookCustomVariableResource, NewLogsCustomDestinationResource, NewTenantBasedHandleResource, + 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. diff --git a/go.mod b/go.mod index cd1534fa8c..508067a989 100644 --- a/go.mod +++ b/go.mod @@ -97,3 +97,4 @@ require ( ) go 1.23 +replace github.com/DataDog/datadog-api-client-go/v2 v2.34.1-0.20241226155556-e60f30b0e84e => ../datadog-api-spec/generated/datadog-api-client-go From 4c19fd845dacdff0b8d8011f1c9feb772e6cbcc9 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Fri, 14 Feb 2025 14:03:03 +0100 Subject: [PATCH 3/5] 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 +- go.mod | 1 - 6 files changed, 167 insertions(+), 17 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 f543b3e2b5..5bc54fcec7 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -115,6 +115,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: diff --git a/go.mod b/go.mod index 508067a989..cd1534fa8c 100644 --- a/go.mod +++ b/go.mod @@ -97,4 +97,3 @@ require ( ) go 1.23 -replace github.com/DataDog/datadog-api-client-go/v2 v2.34.1-0.20241226155556-e60f30b0e84e => ../datadog-api-spec/generated/datadog-api-client-go From 6fc7905e1cb232b0fee887e078f56b721757cd08 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Thu, 27 Feb 2025 13:58:13 +0100 Subject: [PATCH 4/5] 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 595f7189d8209ef96375eb5a6816c6a09d910ba7 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Fri, 28 Feb 2025 16:34:02 +0100 Subject: [PATCH 5/5] 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.