diff --git a/datadog/fwprovider/framework_provider.go b/datadog/fwprovider/framework_provider.go index e6bc7cd901..2c2e7e5ba1 100644 --- a/datadog/fwprovider/framework_provider.go +++ b/datadog/fwprovider/framework_provider.go @@ -38,6 +38,7 @@ var Resources = []func() resource.Resource{ NewApmRetentionFiltersOrderResource, NewCatalogEntityResource, NewDashboardListResource, + NewDomainAllowlistResource, NewDowntimeScheduleResource, NewIntegrationAzureResource, NewIntegrationAwsEventBridgeResource, diff --git a/datadog/fwprovider/resource_datadog_domain_allowlist.go b/datadog/fwprovider/resource_datadog_domain_allowlist.go new file mode 100644 index 0000000000..559c595dfa --- /dev/null +++ b/datadog/fwprovider/resource_datadog_domain_allowlist.go @@ -0,0 +1,230 @@ +package fwprovider + +import ( + "context" + + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" + frameworkPath "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/types" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +var ( + _ resource.ResourceWithConfigure = &domainAllowlistResource{} + _ resource.ResourceWithImportState = &domainAllowlistResource{} +) + +func NewDomainAllowlistResource() resource.Resource { + return &domainAllowlistResource{} +} + +type domainAllowlistResource struct { + Api *datadogV2.DomainAllowlistApi + Auth context.Context +} + +type domainAllowlistResourceModel struct { + ID types.String `tfsdk:"id"` + Enabled types.Bool `tfsdk:"enabled"` + Domains []string `tfsdk:"domains"` +} + +func (r *domainAllowlistResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "domain_allowlist" +} + +func (r *domainAllowlistResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Description: "Provides the Datadog Email Domain Allowlist resource. This can be used to manage the Datadog Email Domain Allowlist.", + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Description: "Whether the Email Domain Allowlist is enabled.", + Required: true, + }, + "id": utils.ResourceIDAttribute(), + "domains": schema.ListAttribute{ + Description: "The domains within the domain allowlist.", + ElementType: types.StringType, + Required: true, + Computed: false, + }, + }, + } +} + +func (r *domainAllowlistResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + providerData := request.ProviderData.(*FrameworkProvider) + r.Api = providerData.DatadogApiInstances.GetDomainAllowlistApiV2() + r.Auth = providerData.Auth +} + +func (r *domainAllowlistResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, frameworkPath.Root("id"), request, response) +} + +func (r *domainAllowlistResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var state domainAllowlistResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + resp, httpResp, err := r.Api.GetDomainAllowlist(r.Auth) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(utils.TranslateClientError(err, httpResp, ""), "error getting team permission setting")) + return + } + if err := utils.CheckForUnparsed(resp); err != nil { + response.Diagnostics.AddError("", err.Error()) + return + } + domainAllowListData := resp.GetData() + + apiDomains, ok := domainAllowListData.Attributes.GetDomainsOk() + priorEntries := state.Domains + + if !compareDomainEntries(priorEntries, *apiDomains) && ok && priorEntries != nil { + state.Domains = *apiDomains + } + + r.updateEnableState(ctx, &state, domainAllowListData.GetAttributes()) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) + +} + +func (r *domainAllowlistResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var state domainAllowlistResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + domainAllowlistReq, _ := buildDomainAllowlistUpdateRequest(state) + resp, httpResp, err := r.Api.PatchDomainAllowlist(r.Auth, *domainAllowlistReq) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(utils.TranslateClientError(err, httpResp, ""), "error creating domain allowlist")) + return + } + + if err := utils.CheckForUnparsed(resp); err != nil { + response.Diagnostics.AddError("", err.Error()) + return + } + + domainAllowlistData := resp.GetData() + + state.ID = types.StringValue(domainAllowlistData.GetId()) + r.updateRequestState(ctx, &state, domainAllowlistData.Attributes) + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *domainAllowlistResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state domainAllowlistResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + domainAllowlistReq, err := buildDomainAllowlistUpdateRequest(state) + if err != nil { + response.Diagnostics.AddError("", err.Error()) + return + } + resp, httpResp, err := r.Api.PatchDomainAllowlist(r.Auth, *domainAllowlistReq) + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(utils.TranslateClientError(err, httpResp, "error updating domain allowlist"), "")) + return + } + if err := utils.CheckForUnparsed(resp); err != nil { + response.Diagnostics.AddError("", err.Error()) + return + } + + domainAllowlistData := resp.GetData() + r.updateRequestState(ctx, &state, domainAllowlistData.Attributes) + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *domainAllowlistResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var state domainAllowlistResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + domainAllowlistUpdateReq := datadogV2.NewDomainAllowlistRequestWithDefaults() + domainAllowlistData := datadogV2.NewDomainAllowlist(datadogV2.DOMAINALLOWLISTTYPE_DOMAIN_ALLOWLIST) + domainAllowlistAttributes := datadogV2.NewDomainAllowlistAttributesWithDefaults() + domainAllowlistAttributes.SetEnabled(false) + domainAllowlistAttributes.SetDomains([]string{}) + + domainAllowlistData.SetAttributes(*domainAllowlistAttributes) + domainAllowlistUpdateReq.SetData(*domainAllowlistData) + + resp, httpResp, err := r.Api.PatchDomainAllowlist(r.Auth, *domainAllowlistUpdateReq) + + if err != nil { + response.Diagnostics.Append(utils.FrameworkErrorDiag(utils.TranslateClientError(err, httpResp, ""), "error disabling and removing entries from domain allowlist")) + return + } + + if err := utils.CheckForUnparsed(resp); err != nil { + response.Diagnostics.AddError("", err.Error()) + return + } + +} + +func (r *domainAllowlistResource) updateRequestState(ctx context.Context, state *domainAllowlistResourceModel, domainAllowlistAttrs *datadogV2.DomainAllowlistResponseDataAttributes) { + if domainAllowlistAttrs != nil { + if enabled, ok := domainAllowlistAttrs.GetEnabledOk(); ok && enabled != nil { + state.Enabled = types.BoolValue(*enabled) + } + + if domains, ok := domainAllowlistAttrs.GetDomainsOk(); ok && len(*domains) > 0 { + state.Domains = domainAllowlistAttrs.GetDomains() + } + } +} + +func (r *domainAllowlistResource) updateEnableState(ctx context.Context, state *domainAllowlistResourceModel, domainAllowlistAttrs datadogV2.DomainAllowlistResponseDataAttributes) { + if enabled, ok := domainAllowlistAttrs.GetEnabledOk(); ok && enabled != nil { + state.Enabled = types.BoolValue(*enabled) + } +} + +func buildDomainAllowlistUpdateRequest(state domainAllowlistResourceModel) (*datadogV2.DomainAllowlistRequest, error) { + domainAllowlistRequest := datadogV2.NewDomainAllowlistRequestWithDefaults() + domainAllowlistData := datadogV2.NewDomainAllowlist(datadogV2.DOMAINALLOWLISTTYPE_DOMAIN_ALLOWLIST) + domainAllowlistAttributes := datadogV2.NewDomainAllowlistAttributesWithDefaults() + + enabled := state.Enabled + domainAllowlistAttributes.SetEnabled(enabled.ValueBool()) + domains := state.Domains + if domains != nil { + domainAllowlistDomains := make([]string, len(domains)) + copy(domainAllowlistDomains, domains) + domainAllowlistAttributes.SetDomains(domainAllowlistDomains) + } else { + domainAllowlistAttributes.SetDomains([]string{}) + } + + domainAllowlistData.SetAttributes(*domainAllowlistAttributes) + domainAllowlistRequest.SetData(*domainAllowlistData) + return domainAllowlistRequest, nil +} + +func compareDomainEntries(slice1 []string, slice2 []string) bool { + if len(slice1) != len(slice2) { + return false + } + for i := range slice1 { + if slice1[i] != slice2[i] { + return false + } + } + return true +} diff --git a/datadog/internal/utils/api_instances_helper.go b/datadog/internal/utils/api_instances_helper.go index eebd849f73..fa42a1eb0c 100644 --- a/datadog/internal/utils/api_instances_helper.go +++ b/datadog/internal/utils/api_instances_helper.go @@ -52,6 +52,7 @@ type ApiInstances struct { csmThreatsApiV2 *datadogV2.CSMThreatsApi confluentCloudApiV2 *datadogV2.ConfluentCloudApi dashboardListsApiV2 *datadogV2.DashboardListsApi + domainAllowlistApiV2 *datadogV2.DomainAllowlistApi downtimesApiV2 *datadogV2.DowntimesApi eventsApiV2 *datadogV2.EventsApi fastlyIntegrationApiV2 *datadogV2.FastlyIntegrationApi @@ -356,6 +357,14 @@ func (i *ApiInstances) GetCSMThreatsApiV2() *datadogV2.CSMThreatsApi { return i.csmThreatsApiV2 } +// GetDomainAllowlistApiV2 get instance of DomainAllowlistAPI +func (i *ApiInstances) GetDomainAllowlistApiV2() *datadogV2.DomainAllowlistApi { + if i.domainAllowlistApiV2 == nil { + i.domainAllowlistApiV2 = datadogV2.NewDomainAllowlistApi(i.HttpClient) + } + return i.domainAllowlistApiV2 +} + // GetDowntimesApiV2 get instance of DowntimesApi func (i *ApiInstances) GetDowntimesApiV2() *datadogV2.DowntimesApi { if i.downtimesApiV2 == nil { diff --git a/datadog/tests/cassettes/TestAccDatadogDomainAllowlist_CreateUpdate.freeze b/datadog/tests/cassettes/TestAccDatadogDomainAllowlist_CreateUpdate.freeze new file mode 100644 index 0000000000..b966a4882d --- /dev/null +++ b/datadog/tests/cassettes/TestAccDatadogDomainAllowlist_CreateUpdate.freeze @@ -0,0 +1 @@ +2024-11-06T12:01:45.413918-05:00 \ No newline at end of file diff --git a/datadog/tests/cassettes/TestAccDatadogDomainAllowlist_CreateUpdate.yaml b/datadog/tests/cassettes/TestAccDatadogDomainAllowlist_CreateUpdate.yaml new file mode 100644 index 0000000000..65324b287e --- /dev/null +++ b/datadog/tests/cassettes/TestAccDatadogDomainAllowlist_CreateUpdate.yaml @@ -0,0 +1,257 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 108 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"domains":["@test.com","@datadoghq.com"],"enabled":true},"type":"domain_allowlist"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: PATCH + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":true,"domains":["@test.com","@datadoghq.com"]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 179.44725ms + - id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":true,"domains":["@test.com","@datadoghq.com"]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 74.813583ms + - id: 2 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":true,"domains":["@test.com","@datadoghq.com"]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 84.83025ms + - id: 3 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 110 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"domains":["@gmail.com","@datadoghq.com"],"enabled":false},"type":"domain_allowlist"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: PATCH + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":false,"domains":["@gmail.com","@datadoghq.com"]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 149.79325ms + - id: 4 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":false,"domains":["@gmail.com","@datadoghq.com"]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 87.818833ms + - id: 5 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 81 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: | + {"data":{"attributes":{"domains":[],"enabled":false},"type":"domain_allowlist"}} + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: PATCH + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":false,"domains":[]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 146.024083ms + - id: 6 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + transfer_encoding: [] + trailer: {} + host: api.datadoghq.com + remote_addr: "" + request_uri: "" + body: "" + form: {} + headers: + Accept: + - application/json + url: https://api.datadoghq.com/api/v2/domain_allowlist + method: GET + response: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + transfer_encoding: + - chunked + trailer: {} + content_length: -1 + uncompressed: true + body: | + {"data":{"type":"domain_allowlist","attributes":{"enabled":false,"domains":[]}}} + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 86.334041ms diff --git a/datadog/tests/provider_test.go b/datadog/tests/provider_test.go index 5b1c8984f4..8407fb8b8d 100644 --- a/datadog/tests/provider_test.go +++ b/datadog/tests/provider_test.go @@ -146,6 +146,7 @@ var testFiles2EndpointTags = map[string]string{ "tests/resource_datadog_dashboard_topology_map_test": "dashboards", "tests/resource_datadog_dashboard_trace_service_test": "dashboards", "tests/resource_datadog_dashboard_treemap_test": "dashboards", + "tests/resource_datadog_domain_allowlist_test": "domain-allowlist", "tests/resource_datadog_openapi_api_test": "apimanagement", "tests/resource_datadog_powerpack_test": "powerpacks", "tests/resource_datadog_powerpack_alert_graph_test": "powerpacks", diff --git a/datadog/tests/resource_datadog_domain_allowlist_test.go b/datadog/tests/resource_datadog_domain_allowlist_test.go new file mode 100644 index 0000000000..272a2283e2 --- /dev/null +++ b/datadog/tests/resource_datadog_domain_allowlist_test.go @@ -0,0 +1,83 @@ +package test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider" + "github.com/terraform-providers/terraform-provider-datadog/datadog/internal/utils" +) + +func TestAccDatadogDomainAllowlist_CreateUpdate(t *testing.T) { + + _, providers, accProviders := testAccFrameworkMuxProviders(context.Background(), t) + + // When generating the casette, it may be necessary to add sleep functions before the check + // The endpoint has a zonal cache with a ttl of 2 second + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: accProviders, + CheckDestroy: testAccCheckDatadogDomainAllowlistDestroy(providers.frameworkProvider), + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogDomainAllowlistConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("datadog_domain_allowlist.foo", "enabled", "true"), + resource.TestCheckResourceAttr("datadog_domain_allowlist.foo", "domains.0", "@test.com"), + resource.TestCheckResourceAttr("datadog_domain_allowlist.foo", "domains.1", "@datadoghq.com"), + ), + }, + { + Config: testAccCheckDatadogDomainAllowlistConfigUpdated(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("datadog_domain_allowlist.foo", "domains.0", "@gmail.com"), + resource.TestCheckResourceAttr("datadog_domain_allowlist.foo", "domains.1", "@datadoghq.com"), + resource.TestCheckResourceAttr("datadog_domain_allowlist.foo", "enabled", "false"), + ), + }, + }, + }) +} + +func testAccCheckDatadogDomainAllowlistDestroy(accProvider *fwprovider.FrameworkProvider) func(*terraform.State) error { + return func(s *terraform.State) error { + apiInstances := accProvider.DatadogApiInstances + auth := accProvider.Auth + + for _, r := range s.RootModule().Resources { + if r.Type != "datadog_domain_allowlist" { + // Only care about domain allowlist + continue + } + resp, httpresp, err := apiInstances.GetDomainAllowlistApiV2().GetDomainAllowlist(auth) + if err != nil { + return utils.TranslateClientError(err, httpresp, "error getting Domain allowlist") + } + domainAllowlistAttributes := resp.GetData().Attributes + if domainAllowlistAttributes.GetEnabled() != false || len(domainAllowlistAttributes.GetDomains()) != 0 { + return fmt.Errorf("Domain allowlist not disabled or empty") + } + } + return nil + } +} + +func testAccCheckDatadogDomainAllowlistConfig() string { + return ` +resource "datadog_domain_allowlist" "foo" { + enabled = true + domains = ["@test.com", "@datadoghq.com"] +}` +} + +func testAccCheckDatadogDomainAllowlistConfigUpdated() string { + return ` +resource "datadog_domain_allowlist" "foo" { + enabled = false + domains = ["@gmail.com", "@datadoghq.com"] +}` +} diff --git a/docs/resources/domain_allowlist.md b/docs/resources/domain_allowlist.md new file mode 100644 index 0000000000..44408938b2 --- /dev/null +++ b/docs/resources/domain_allowlist.md @@ -0,0 +1,32 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "datadog_domain_allowlist Resource - terraform-provider-datadog" +subcategory: "" +description: |- + Provides the Datadog Email Domain Allowlist resource. This can be used to manage the Datadog Email Domain Allowlist. +--- + +# datadog_domain_allowlist (Resource) + +Provides the Datadog Email Domain Allowlist resource. This can be used to manage the Datadog Email Domain Allowlist. + +## Example Usage + +```terraform +resource "datadog_domain_allowlist" "example" { + enabled = true + domains = ["@gmail.com"] +} +``` + + +## Schema + +### Required + +- `domains` (List of String) The domains within the domain allowlist. +- `enabled` (Boolean) Whether the Email Domain Allowlist is enabled. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/datadog_domain_allowlist/resource.tf b/examples/resources/datadog_domain_allowlist/resource.tf new file mode 100644 index 0000000000..5329282c37 --- /dev/null +++ b/examples/resources/datadog_domain_allowlist/resource.tf @@ -0,0 +1,4 @@ +resource "datadog_domain_allowlist" "example" { + enabled = true + domains = ["@gmail.com"] +} \ No newline at end of file