From 6fc7905e1cb232b0fee887e078f56b721757cd08 Mon Sep 17 00:00:00 2001 From: Quentin Guillard Date: Thu, 27 Feb 2025 13:58:13 +0100 Subject: [PATCH] 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 ac4491be6..8dce55435 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 "" }