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 10 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,
NewResourceEvaluationFilter,
NewSecurityMonitoringRuleJSONResource,
}

Expand Down
307 changes: 307 additions & 0 deletions datadog/fwprovider/resource_datadog_csm_resource_evaluation_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
package fwprovider

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

"github.com/hashicorp/terraform-plugin-framework/path"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"

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

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

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

type ResourceEvaluationFilterModel struct {
CloudProvider types.String `tfsdk:"cloud_provider"`
ID types.String `tfsdk:"id"`
Tags types.Set `tfsdk:"tags"`
}

func NewResourceEvaluationFilter() resource.Resource {
return &ResourceEvaluationFilter{}
}

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

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

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

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

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

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

for _, elem := range set.Elements() {
strVal, ok := elem.(types.String)
if !ok {
diags.AddError("Invalid element type", "Expected string in set but found a different type")
continue
}
result = append(result, strVal.ValueString())
}

return result, diags
}

func (r *ResourceEvaluationFilter) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manage a single resource evaluation filter.",
Attributes: map[string]schema.Attribute{
"cloud_provider": schema.StringAttribute{
Required: true,
Description: "The cloud provider of the resource that will be target of the filters.",
},
"id": schema.StringAttribute{
Required: true,
Description: "The ID of the resource that will be the target of the filters. Different cloud providers target different resource ids:\n - `aws`: account id \n - `gcp`: project id\n - `azure`: subscription id",
},
"tags": schema.SetAttribute{
Required: true,
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.ValueStringsAre(tagFormatValidator),
},
Description: "Set of tags to filter the misconfiguration detections on. Each entry should follow the format: \"key:\":\"value\".",
},
},
}
}

func (r *ResourceEvaluationFilter) 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 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)
// Save data into Terraform state
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 *ResourceEvaluationFilter) UpdateState(_ context.Context, state *ResourceEvaluationFilterModel, attributes *datadogV2.ResourceFilterAttributes) {
// since we are handling a response after an update/read request, the cloud provider map will have at most one key
// and the map of each cloud provider will also have at most one key
for p, accounts := range attributes.CloudProvider {
for id, tagList := range accounts {
tags := types.SetValueMust(types.StringType, convertStringSliceToAttrValues(tagList))
state.CloudProvider = types.StringValue(p)
state.ID = types.StringValue(id)
state.Tags = tags
break
}
break
}
}

func (r *ResourceEvaluationFilter) 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 ResourceEvaluationFilter"))
return
}

attributes := resp.Data.GetAttributes()
r.UpdateState(ctx, &state, &attributes)

// Save data into Terraform state
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

func (r *ResourceEvaluationFilter) 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
}

fmt.Println("DEBUG UPDATE - creating payload")
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 ResourceEvaluationFilter"))
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)
// Save data into Terraform state
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

func (r *ResourceEvaluationFilter) 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
}

// empty tags
state.Tags = types.SetValueMust(types.StringType, []attr.Value{})

// create body as normal with empty tags
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 ResourceEvaluationFilter"))
return
}
if err := utils.CheckForUnparsed(resp); err != nil {
response.Diagnostics.AddError("response contains unparsedObject", err.Error())
return
}
}

func (r *ResourceEvaluationFilter) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
// Expected import format: "cloud_provider:id" (e.g., "aws:123456789")
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]

// Set attributes into Terraform state so Read() can hydrate the full resource
resp.State.SetAttribute(ctx, path.Root("cloud_provider"), cloudProvider)
resp.State.SetAttribute(ctx, path.Root("id"), id)
}

func (r *ResourceEvaluationFilter) 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.ID.IsNull() || state.ID.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.ID.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-21T11:09:27.646789+01:00
Loading
Loading