Skip to content

[datadog_compliance_resource_evaluation_filter] Adding resource evaluation filters as a terraform resource #3004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1056b0e
Initial resource evaluation filter implementation
Matzoc May 8, 2025
b07e39f
revert CloudProvider string refactor
Matzoc May 9, 2025
ac8063f
updated terraform provider to use skipCache
Matzoc May 16, 2025
353a825
Adding descriptions to schema. Removing debug specific code
Matzoc May 19, 2025
ae91d24
adding documentation examples
Matzoc May 21, 2025
5c66b29
Merge branch 'master' into manuel.costa/resource-filter-resource
Matzoc May 21, 2025
bac3ce1
updating go client version
Matzoc May 21, 2025
a0bd6f9
pushing the docs changes
Matzoc May 21, 2025
8adc196
go fmt
Matzoc May 21, 2025
a06ec10
adding generated cassettes
Matzoc May 21, 2025
9f1767e
swapping from set to list to stay consistent with backend implementat…
Matzoc May 22, 2025
25460bf
applying suggested documentation changes.
Matzoc May 22, 2025
9e97616
adding missing ponctuation
Matzoc May 22, 2025
f121dc2
pushing missing doc
Matzoc May 22, 2025
8913363
renaming resource
Matzoc May 22, 2025
8123dc6
removed debug line. Addressed various comments.
Matzoc May 22, 2025
d7bade0
updated error messages
Matzoc May 22, 2025
595ea54
fix resources not being deleted when id or cloud provider is changed
Matzoc May 22, 2025
fbde816
remove commented out code
Matzoc May 23, 2025
5779d0d
adding updated cassettes
Matzoc May 23, 2025
3ab35b2
Updating remaining instances of resource evaluation filters to compli…
Matzoc May 23, 2025
838d17e
fixing incorrect state management (overlap between id field and id in…
Matzoc May 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions datadog/fwprovider/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ var Resources = []func() resource.Resource{
NewWorkflowAutomationResource,
NewAppBuilderAppResource,
NewObservabilitPipelineResource,
NewComplianceResourceEvaluationFilter,
NewSecurityMonitoringRuleJSONResource,
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
package fwprovider

import (
"context"
"fmt"
"regexp"
"strings"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"

"github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils"
)

type ComplianceResourceEvaluationFilter struct {
API *datadogV2.SecurityMonitoringApi
Auth context.Context
}

type ResourceEvaluationFilterModel struct {
CloudProvider types.String `tfsdk:"cloud_provider"`
ResourceID types.String `tfsdk:"resource_id"`
Tags types.List `tfsdk:"tags"`

ID types.String `tfsdk:"id"`
}

func NewComplianceResourceEvaluationFilter() resource.Resource {
return &ComplianceResourceEvaluationFilter{}
}

var (
_ resource.ResourceWithConfigure = &ComplianceResourceEvaluationFilter{}
_ resource.ResourceWithImportState = &ComplianceResourceEvaluationFilter{}
)

func (r *ComplianceResourceEvaluationFilter) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
providerData, _ := request.ProviderData.(*FrameworkProvider)
r.API = providerData.DatadogApiInstances.GetSecurityMonitoringApiV2()
r.Auth = providerData.Auth
}

func (r *ComplianceResourceEvaluationFilter) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "compliance_resource_evaluation_filter"
}

var tagFormatValidator = stringvalidator.RegexMatches(
regexp.MustCompile(`^[^:]+:[^:]+$`),
"each tag must be in the format 'key:value' (colon-separated)",
)

func toSliceString(list types.List) ([]string, diag.Diagnostics) {
var diags diag.Diagnostics
result := make([]string, 0)

if list.IsNull() || list.IsUnknown() {
return result, nil
}

for _, elem := range list.Elements() {
strVal, ok := elem.(types.String)
if !ok {
diags.AddError("Invalid element type creating tags list", fmt.Sprintf("Expected string in list but found %T", elem))
continue
}
result = append(result, strVal.ValueString())
}

return result, diags
}

func (r *ComplianceResourceEvaluationFilter) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Provides a Datadog ComplianceResourceEvaluationFilter resource. This can be used to create and manage a compliance resource evaluation filter.",
Attributes: map[string]schema.Attribute{
"cloud_provider": schema.StringAttribute{
Required: true,
Description: "The cloud provider of the filter's targeted resource. Only `aws`, `gcp` or `azure` are considered valid cloud providers.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"resource_id": schema.StringAttribute{
Required: true,
Description: "The ID of the of the filter's targeted resource. Different cloud providers target different resource IDs:\n - `aws`: account id \n - `gcp`: project id\n - `azure`: subscription id",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
}},

"id": schema.StringAttribute{
Description: "The ID of the resource evaluation filter resource.",
Computed: true,
},
"tags": schema.ListAttribute{
Required: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(tagFormatValidator),
},
Description: "List of tags to filter misconfiguration detections. Each entry should follow the format: \"key\":\"value\".",
},
},
}
}

func (r *ComplianceResourceEvaluationFilter) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var state ResourceEvaluationFilterModel
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
if response.Diagnostics.HasError() {
return
}

body, diags := r.buildUpdateResourceEvaluationFilterRequest(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

resp, _, err := r.API.UpdateResourceEvaluationFilters(r.Auth, *body)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating compliance resource evaluation filter"))
return
}
if err := utils.CheckForUnparsed(resp); err != nil {
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
return
}

attributes := resp.Data.GetAttributes()
r.UpdateState(ctx, &state, &attributes)
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

func convertStringSliceToAttrValues(s []string) []attr.Value {
out := make([]attr.Value, len(s))
for i, v := range s {
out[i] = types.StringValue(v)
}
return out
}

func (r *ComplianceResourceEvaluationFilter) UpdateState(_ context.Context, state *ResourceEvaluationFilterModel, attributes *datadogV2.ResourceFilterAttributes) {
for p, accounts := range attributes.CloudProvider {
for resource_id, tagList := range accounts {
tags := types.ListValueMust(types.StringType, convertStringSliceToAttrValues(tagList))
state.CloudProvider = types.StringValue(p)
state.ID = types.StringValue(p + ":" + resource_id)
state.ResourceID = types.StringValue(resource_id)
state.Tags = tags
break
}
break
}
}

func (r *ComplianceResourceEvaluationFilter) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
var state ResourceEvaluationFilterModel
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
if response.Diagnostics.HasError() {
return
}

if state.CloudProvider.IsNull() || state.CloudProvider.IsUnknown() {
response.Diagnostics.AddError("Missing cloud_provider", "cloud_provider is required for lookup")
return
}

provider := state.CloudProvider.ValueString()
skipCache := true

params := datadogV2.GetResourceEvaluationFiltersOptionalParameters{
CloudProvider: &provider,
AccountId: state.ID.ValueStringPointer(),
SkipCache: &skipCache,
}
resp, _, err := r.API.GetResourceEvaluationFilters(r.Auth, params)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error retrieving ComplianceResourceEvaluationFilter"))
return
}

attributes := resp.Data.GetAttributes()
r.UpdateState(ctx, &state, &attributes)
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

func (r *ComplianceResourceEvaluationFilter) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var state ResourceEvaluationFilterModel
response.Diagnostics.Append(request.Plan.Get(ctx, &state)...)
if response.Diagnostics.HasError() {
return
}

body, diags := r.buildUpdateResourceEvaluationFilterRequest(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

resp, _, err := r.API.UpdateResourceEvaluationFilters(r.Auth, *body)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating ComplianceResourceEvaluationFilter"))
return
}
if err := utils.CheckForUnparsed(resp); err != nil {
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
return
}

attributes := resp.Data.GetAttributes()
r.UpdateState(ctx, &state, &attributes)
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

func (r *ComplianceResourceEvaluationFilter) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
var state ResourceEvaluationFilterModel
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
if response.Diagnostics.HasError() {
return
}

state.Tags = types.ListValueMust(types.StringType, []attr.Value{})
body, diags := r.buildUpdateResourceEvaluationFilterRequest(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

resp, _, err := r.API.UpdateResourceEvaluationFilters(r.Auth, *body)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting ComplianceResourceEvaluationFilter"))
return
}
if err := utils.CheckForUnparsed(resp); err != nil {
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
return
}
}

func (r *ComplianceResourceEvaluationFilter) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
parts := strings.Split(req.ID, ":")
if len(parts) != 2 {
resp.Diagnostics.AddError(
"Invalid import format",
`Expected format: "cloud_provider:id" (e.g., "aws:123456789")`,
)
return
}

cloudProvider := parts[0]
id := parts[1]

resp.State.SetAttribute(ctx, path.Root("cloud_provider"), cloudProvider)
resp.State.SetAttribute(ctx, path.Root("id"), id)
}

func (r *ComplianceResourceEvaluationFilter) buildUpdateResourceEvaluationFilterRequest(ctx context.Context, state *ResourceEvaluationFilterModel) (*datadogV2.UpdateResourceEvaluationFiltersRequest, diag.Diagnostics) {
diags := diag.Diagnostics{}
data := datadogV2.NewUpdateResourceEvaluationFiltersRequestDataWithDefaults()

tagsList, tagDiags := toSliceString(state.Tags)
diags.Append(tagDiags...)
if tagDiags.HasError() {
return nil, diags
}

if state.CloudProvider.IsNull() || state.CloudProvider.IsUnknown() {
diags.AddError("Missing cloud_provider", "cloud_provider is required but was null or unknown")
return nil, diags
}
if state.ResourceID.IsNull() || state.ResourceID.IsUnknown() {
diags.AddError("Missing id", "id is required but was null or unknown")
return nil, diags
}

attributes := datadogV2.ResourceFilterAttributes{
CloudProvider: map[string]map[string][]string{
state.CloudProvider.ValueString(): {
state.ResourceID.ValueString(): tagsList,
},
},
}

data.SetId(string(datadogV2.RESOURCEFILTERREQUESTTYPE_CSM_RESOURCE_FILTER))
data.SetType(datadogV2.RESOURCEFILTERREQUESTTYPE_CSM_RESOURCE_FILTER)
data.SetAttributes(attributes)

req := datadogV2.NewUpdateResourceEvaluationFiltersRequestWithDefaults()
req.SetData(*data)

return req, diags
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2025-05-23T17:45:25.774964+01:00
Loading
Loading