-
Notifications
You must be signed in to change notification settings - Fork 392
[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
nkonjeti
wants to merge
51
commits into
master
Choose a base branch
from
neha.konjeti/framework-provider
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 36 commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
a444409
provider for custom frameworks
nkonjeti 6f38f6a
passed set create test
nkonjeti 2f56157
clean up code and add tests
nkonjeti 6e8e30a
add invalid create framework tests
nkonjeti ebc825b
test files
nkonjeti 0504d3a
add import state functionality
nkonjeti 588e70f
update mod file
nkonjeti b4b31d4
clean up code
nkonjeti a93ec43
update tests
nkonjeti ed9aace
test update is not triggered if order is changed
nkonjeti f6a5907
change retrieve custom framework to get custom framework
nkonjeti 9273dd7
update api spec in go mod
nkonjeti 6da58e3
add docs for terraform provider
nkonjeti 50e5b15
remove unstable endpoint
nkonjeti 1ddc9b9
add more tests
nkonjeti f94c1ab
add validators
nkonjeti c042cf7
change tests to use same handle and version
nkonjeti 5cbef69
add test for 409 conflict
nkonjeti fb2e9e4
add a resource file
nkonjeti 7cf94a8
add example in doc and remove comments
nkonjeti 1ace048
fix required requirements and control block
nkonjeti ebc74dc
changeexample to compliance custom framework
nkonjeti 2c25611
fix docs
nkonjeti 7df1175
update go mod
nkonjeti 539285b
make icon url optional and remove description
nkonjeti 92734e1
add comment to describe why requirements is a set
nkonjeti bd5a81c
remove description from resource example
nkonjeti c5c0c23
remove comments and extra cassettes
nkonjeti e768276
fix description of icon url
nkonjeti 5081952
fix format
nkonjeti 5042d13
delete framework in conflict test
nkonjeti 6d56771
remove import resource and update when create conflicts
nkonjeti b420695
use real rule ids in the example resource
nkonjeti b744bca
remove logs
nkonjeti 75d3526
test same state framework id
nkonjeti 0e02eb5
add better comments for delete after delete case
nkonjeti 1498f72
add cassetes for same config no update test
nkonjeti 367c92c
move around error handling
nkonjeti 830d9a4
Revert "move around error handling"
nkonjeti 1111d5d
remove err check
nkonjeti ab1976b
add invalidcreate cassettes
nkonjeti 1684e7e
use real rule ids
nkonjeti 7f5d4b1
RecreateAfterAPIDelete cassettes
nkonjeti 74992a6
add immutable fields edge case
nkonjeti 7e74564
change requirements and controls to lists'
nkonjeti c1ec028
fix modify plan
nkonjeti f3ae423
fix the apply issue
nkonjeti 3561635
remove modify plan because read API response order is changed
nkonjeti 25f4df7
remove import file
nkonjeti 424a1a8
check for rule ids length and update docs
nkonjeti c1e72f2
remove same config no update test
nkonjeti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
341 changes: 341 additions & 0 deletions
341
datadog/fwprovider/resource_datadog_compliance_custom_framework.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,341 @@ | ||
package fwprovider | ||
|
||
import ( | ||
"context" | ||
|
||
"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 | ||
} | ||
|
||
_, httpResp, err := r.Api.CreateCustomFramework(r.Auth, *buildCreateFrameworkRequest(state)) | ||
if err != nil { | ||
if httpResp != nil && httpResp.StatusCode == 409 { // if framework already exists, try to update it with the new state | ||
_, _, updateErr := r.Api.UpdateCustomFramework(r.Auth, state.Handle.ValueString(), state.Version.ValueString(), *buildUpdateFrameworkRequest(state)) | ||
if updateErr != nil { | ||
response.Diagnostics.Append(utils.FrameworkErrorDiag(updateErr, "error updating existing compliance custom framework")) | ||
return | ||
} | ||
} else { | ||
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, httpResp, 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 && httpResp != nil && httpResp.StatusCode == 400 { | ||
// 400 could only mean the framework does not exist | ||
// because terraform would have already validated the framework in the create function | ||
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 | ||
} | ||
|
||
// 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 | ||
} |
48 changes: 48 additions & 0 deletions
48
datadog/internal/validators/duplicate_control_validator.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package validators | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
) | ||
|
||
type controlNameValidator struct{} | ||
|
||
func (v controlNameValidator) Description(context.Context) string { | ||
return "checks for duplicate control names" | ||
} | ||
|
||
func (v controlNameValidator) MarkdownDescription(ctx context.Context) string { | ||
return v.Description(ctx) | ||
} | ||
|
||
func (v controlNameValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { | ||
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { | ||
return | ||
} | ||
|
||
var controlNames []string | ||
for _, control := range req.ConfigValue.Elements() { | ||
controlObj := control.(types.Object) | ||
name := controlObj.Attributes()["name"].(types.String).ValueString() | ||
controlNames = append(controlNames, name) | ||
} | ||
|
||
seen := make(map[string]bool) | ||
for _, name := range controlNames { | ||
if seen[name] { | ||
resp.Diagnostics.AddError( | ||
"Each Control must have a unique name under the same requirement", | ||
fmt.Sprintf("Control name '%s' is used more than once under the same requirement", name), | ||
) | ||
return | ||
} | ||
seen[name] = true | ||
} | ||
} | ||
|
||
func ControlNameValidator() validator.Set { | ||
return &controlNameValidator{} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.