Skip to content

[datadog_compliance_custom_framework] Terraform Provider for Custom Frameworks #2975

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 51 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a444409
provider for custom frameworks
nkonjeti Apr 16, 2025
6f38f6a
passed set create test
nkonjeti Apr 24, 2025
2f56157
clean up code and add tests
nkonjeti Apr 24, 2025
6e8e30a
add invalid create framework tests
nkonjeti Apr 24, 2025
ebc825b
test files
nkonjeti Apr 24, 2025
0504d3a
add import state functionality
nkonjeti Apr 25, 2025
588e70f
update mod file
nkonjeti Apr 25, 2025
b4b31d4
clean up code
nkonjeti Apr 25, 2025
a93ec43
update tests
nkonjeti Apr 25, 2025
ed9aace
test update is not triggered if order is changed
nkonjeti Apr 27, 2025
f6a5907
change retrieve custom framework to get custom framework
nkonjeti Apr 28, 2025
9273dd7
update api spec in go mod
nkonjeti Apr 29, 2025
6da58e3
add docs for terraform provider
nkonjeti Apr 29, 2025
50e5b15
remove unstable endpoint
nkonjeti Apr 29, 2025
1ddc9b9
add more tests
nkonjeti Apr 29, 2025
f94c1ab
add validators
nkonjeti May 1, 2025
c042cf7
change tests to use same handle and version
nkonjeti May 1, 2025
5cbef69
add test for 409 conflict
nkonjeti May 1, 2025
fb2e9e4
add a resource file
nkonjeti May 5, 2025
7cf94a8
add example in doc and remove comments
nkonjeti May 6, 2025
1ace048
fix required requirements and control block
nkonjeti May 7, 2025
ebc74dc
changeexample to compliance custom framework
nkonjeti May 7, 2025
2c25611
fix docs
nkonjeti May 7, 2025
7df1175
update go mod
nkonjeti May 14, 2025
539285b
make icon url optional and remove description
nkonjeti May 14, 2025
92734e1
add comment to describe why requirements is a set
nkonjeti May 14, 2025
bd5a81c
remove description from resource example
nkonjeti May 14, 2025
c5c0c23
remove comments and extra cassettes
nkonjeti May 14, 2025
e768276
fix description of icon url
nkonjeti May 14, 2025
5081952
fix format
nkonjeti May 14, 2025
5042d13
delete framework in conflict test
nkonjeti May 15, 2025
6d56771
remove import resource and update when create conflicts
nkonjeti May 16, 2025
b420695
use real rule ids in the example resource
nkonjeti May 16, 2025
b744bca
remove logs
nkonjeti May 16, 2025
75d3526
test same state framework id
nkonjeti May 16, 2025
0e02eb5
add better comments for delete after delete case
nkonjeti May 16, 2025
1498f72
add cassetes for same config no update test
nkonjeti May 16, 2025
367c92c
move around error handling
nkonjeti May 16, 2025
830d9a4
Revert "move around error handling"
nkonjeti May 16, 2025
1111d5d
remove err check
nkonjeti May 16, 2025
ab1976b
add invalidcreate cassettes
nkonjeti May 16, 2025
1684e7e
use real rule ids
nkonjeti May 18, 2025
7f5d4b1
RecreateAfterAPIDelete cassettes
nkonjeti May 19, 2025
74992a6
add immutable fields edge case
nkonjeti May 19, 2025
7e74564
change requirements and controls to lists'
nkonjeti May 21, 2025
c1ec028
fix modify plan
nkonjeti May 21, 2025
f3ae423
fix the apply issue
nkonjeti May 21, 2025
3561635
remove modify plan because read API response order is changed
nkonjeti May 22, 2025
25f4df7
remove import file
nkonjeti May 22, 2025
424a1a8
check for rule ids length and update docs
nkonjeti May 22, 2025
c1e72f2
remove same config no update test
nkonjeti May 22, 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 @@ -85,6 +85,7 @@ var Resources = []func() resource.Resource{
NewAppBuilderAppResource,
NewObservabilitPipelineResource,
NewSecurityMonitoringRuleJSONResource,
NewComplianceCustomFrameworkResource,
}

var Datasources = []func() datasource.DataSource{
Expand Down
355 changes: 355 additions & 0 deletions datadog/fwprovider/resource_datadog_compliance_custom_framework.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
package fwprovider

import (
"context"
"strings"

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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/terraform-providers/terraform-provider-datadog/datadog/internal/utils"
"github.com/terraform-providers/terraform-provider-datadog/datadog/internal/validators"
)

var _ resource.Resource = &complianceCustomFrameworkResource{}

type complianceCustomFrameworkResource struct {
Api *datadogV2.SecurityMonitoringApi
Auth context.Context
}

type complianceCustomFrameworkModel struct {
ID types.String `tfsdk:"id"`
Version types.String `tfsdk:"version"`
Handle types.String `tfsdk:"handle"`
Name types.String `tfsdk:"name"`
IconURL types.String `tfsdk:"icon_url"`
Requirements types.Set `tfsdk:"requirements"` // have to define requirements as a set to be unordered
}

func NewComplianceCustomFrameworkResource() resource.Resource {
return &complianceCustomFrameworkResource{}
}

func (r *complianceCustomFrameworkResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = "compliance_custom_framework"
}

func (r *complianceCustomFrameworkResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Description: "Provides a Datadog Compliance Custom Framework resource, which is used to create and manage compliance custom frameworks.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The ID of the compliance custom framework resource.",
Computed: true,
},
"version": schema.StringAttribute{
Description: "The framework version.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
Required: true,
},
"handle": schema.StringAttribute{
Description: "The framework handle.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
Required: true,
},
"name": schema.StringAttribute{
Description: "The framework name.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
Required: true,
},
"icon_url": schema.StringAttribute{
Description: "The URL of the icon representing the framework",
Optional: true,
},
},
Blocks: map[string]schema.Block{
"requirements": schema.SetNestedBlock{
Description: "The requirements of the framework.",
Validators: []validator.Set{
setvalidator.IsRequired(),
validators.RequirementNameValidator(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The name of the requirement.",
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
},
Blocks: map[string]schema.Block{
"controls": schema.SetNestedBlock{
Description: "The controls of the requirement.",
Validators: []validator.Set{
setvalidator.IsRequired(),
validators.ControlNameValidator(),
},
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The name of the control.",
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"rules_id": schema.SetAttribute{
Description: "The list of rules IDs for the control.",
ElementType: types.StringType,
Required: true,
},
},
},
},
},
},
},
},
}
}

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

func (r *complianceCustomFrameworkResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var state complianceCustomFrameworkModel
diags := request.Config.Get(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

_, _, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state))

if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating compliance custom framework"))
return
}
state.ID = types.StringValue(state.Handle.ValueString() + string('-') + state.Version.ValueString())
diags = response.State.Set(ctx, &state)
response.Diagnostics.Append(diags...)
}

func (r *complianceCustomFrameworkResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
var state complianceCustomFrameworkModel
diags := request.State.Get(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
_, _, err := r.Api.DeleteCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString())
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting framework"))
return
}
}

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

data, _, err := r.Api.GetCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString())
// If the framework does not exist, remove it from terraform state
// This is to avoid the provider to return an error when the framework is deleted in the UI prior
if err != nil && err.Error() == "400 Bad Request" {
response.State.RemoveResource(ctx)
return
}
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error reading compliance custom framework"))
return
}
databaseState := readStateFromDatabase(data, state.Handle.ValueString(), state.Version.ValueString())
diags = response.State.Set(ctx, &databaseState)
response.Diagnostics.Append(diags...)
}

func (r *complianceCustomFrameworkResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var state complianceCustomFrameworkModel
diags := request.Config.Get(ctx, &state)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

_, _, err := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state))
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating compliance custom framework"))
return
}
diags = response.State.Set(ctx, &state)
response.Diagnostics.Append(diags...)
}

func setControl(name string, rulesID []attr.Value) types.Object {
return types.ObjectValueMust(
map[string]attr.Type{
"name": types.StringType,
"rules_id": types.SetType{ElemType: types.StringType},
},
map[string]attr.Value{
"name": types.StringValue(name),
"rules_id": types.SetValueMust(types.StringType, rulesID),
},
)
}

func setRequirement(name string, controls []attr.Value) types.Object {
return types.ObjectValueMust(
map[string]attr.Type{
"name": types.StringType,
"controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{
"name": types.StringType,
"rules_id": types.SetType{ElemType: types.StringType},
}}},
},
map[string]attr.Value{
"name": types.StringValue(name),
"controls": types.SetValueMust(types.ObjectType{AttrTypes: map[string]attr.Type{
"name": types.StringType,
"rules_id": types.SetType{ElemType: types.StringType},
}}, controls),
},
)
}

func setRequirements(requirements []attr.Value) types.Set {
return types.SetValueMust(
types.ObjectType{AttrTypes: map[string]attr.Type{
"name": types.StringType,
"controls": types.SetType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{
"name": types.StringType,
"rules_id": types.SetType{ElemType: types.StringType},
}}},
}},
requirements,
)
}
func readStateFromDatabase(data datadogV2.GetCustomFrameworkResponse, handle string, version string) complianceCustomFrameworkModel {
var state complianceCustomFrameworkModel
state.ID = types.StringValue(handle + "-" + version)
state.Handle = types.StringValue(handle)
state.Version = types.StringValue(version)
state.Name = types.StringValue(data.GetData().Attributes.Name)
if data.GetData().Attributes.IconUrl != nil {
state.IconURL = types.StringValue(*data.GetData().Attributes.IconUrl)
}

requirements := make([]attr.Value, len(data.GetData().Attributes.Requirements))
for i, requirement := range data.GetData().Attributes.Requirements {
controls := make([]attr.Value, len(requirement.Controls))
for j, control := range requirement.Controls {
rulesID := make([]attr.Value, len(control.RulesId))
for k, ruleID := range control.RulesId {
rulesID[k] = types.StringValue(ruleID)
}
controls[j] = setControl(control.Name, rulesID)
}
requirements[i] = setRequirement(requirement.Name, controls)
}
state.Requirements = setRequirements(requirements)
return state
}

// ImportState is used to import a resource from an existing framework so we can update it if it exists in the database and not in terraform
func (r *complianceCustomFrameworkResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
// Split the ID into handle and version
// The last hyphen separates handle and version
lastHyphenIndex := strings.LastIndex(request.ID, "-")
if lastHyphenIndex == -1 {
response.Diagnostics.AddError("Invalid import ID", "Import ID must contain a hyphen to separate handle and version")
return
}
handle := request.ID[:lastHyphenIndex]
version := request.ID[lastHyphenIndex+1:]

data, _, err := r.Api.GetCustomFramework(r.Auth, handle, version)
if err != nil {
response.Diagnostics.AddError("Error importing resource", err.Error())
return
}

state := readStateFromDatabase(data, handle, version)
response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

// using sets for requirements in state to be unordered
func convertStateRequirementsToFrameworkRequirements(requirements types.Set) []datadogV2.CustomFrameworkRequirement {
frameworkRequirements := make([]datadogV2.CustomFrameworkRequirement, len(requirements.Elements()))
for i, requirement := range requirements.Elements() {
requirementState := requirement.(types.Object)
controls := make([]datadogV2.CustomFrameworkControl, len(requirementState.Attributes()["controls"].(types.Set).Elements()))
for j, control := range requirementState.Attributes()["controls"].(types.Set).Elements() {
controlState := control.(types.Object)
rulesID := make([]string, len(controlState.Attributes()["rules_id"].(types.Set).Elements()))
for k, ruleID := range controlState.Attributes()["rules_id"].(types.Set).Elements() {
rulesID[k] = ruleID.(types.String).ValueString()
}
controls[j] = *datadogV2.NewCustomFrameworkControl(controlState.Attributes()["name"].(types.String).ValueString(), rulesID)
}
frameworkRequirements[i] = *datadogV2.NewCustomFrameworkRequirement(controls, requirementState.Attributes()["name"].(types.String).ValueString())
}
return frameworkRequirements
}

func buildCreateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.CreateCustomFrameworkRequest {
var iconURL *string
if !state.IconURL.IsNull() && !state.IconURL.IsUnknown() {
iconURLStr := state.IconURL.ValueString()
iconURL = &iconURLStr
}
createFrameworkRequest := datadogV2.NewCreateCustomFrameworkRequestWithDefaults()
createFrameworkRequest.SetData(datadogV2.CustomFrameworkData{
Type: "custom_framework",
Attributes: datadogV2.CustomFrameworkDataAttributes{
Handle: state.Handle.ValueString(),
Name: state.Name.ValueString(),
IconUrl: iconURL,
Version: state.Version.ValueString(),
Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements),
},
})
return createFrameworkRequest
}

func buildUpdateFrameworkRequest(state complianceCustomFrameworkModel) *datadogV2.UpdateCustomFrameworkRequest {
var iconURL *string
if !state.IconURL.IsNull() && !state.IconURL.IsUnknown() {
iconURLStr := state.IconURL.ValueString()
iconURL = &iconURLStr
}
updateFrameworkRequest := datadogV2.NewUpdateCustomFrameworkRequestWithDefaults()
updateFrameworkRequest.SetData(datadogV2.CustomFrameworkData{
Type: "custom_framework",
Attributes: datadogV2.CustomFrameworkDataAttributes{
Handle: state.Handle.ValueString(),
Name: state.Name.ValueString(),
Version: state.Version.ValueString(),
IconUrl: iconURL,
Requirements: convertStateRequirementsToFrameworkRequirements(state.Requirements),
},
})
return updateFrameworkRequest
}
Loading
Loading