Skip to content
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

[datadog_integration_gcp] Migrate to FW Provider, Add ResourceCollectionEnabled and IsSecurityCommandCenterEnabled fields #2230

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
datadog/*datadog_dashboard* @DataDog/integrations-tools-and-libraries @DataDog/dashboards
datadog/*datadog_downtime* @DataDog/integrations-tools-and-libraries @DataDog/monitor-app
datadog/*datadog_integration_aws* @DataDog/integrations-tools-and-libraries @DataDog/cloud-integrations
datadog/*datadog_integration_gcp* @DataDog/integrations-tools-and-libraries @DataDog/cloud-integrations
datadog/*datadog_integration_pagerduty* @DataDog/integrations-tools-and-libraries @DataDog/saas-integrations @DataDog/saas-integrations
datadog/*datadog_integration_opsgenie* @DataDog/integrations-tools-and-libraries @Datadog/collaboration-integrations
datadog/*datadog_logs* @DataDog/integrations-tools-and-libraries @DataDog/logs-backend @DataDog/logs-app
Expand All @@ -37,7 +36,7 @@ datadog/**/*datadog_integration_azure* @DataDog/integrations-tools-and-
datadog/**/*datadog_integration_cloudflare* @DataDog/integrations-tools-and-libraries @DataDog/saas-integrations
datadog/**/*datadog_integration_confluent* @DataDog/integrations-tools-and-libraries @DataDog/saas-integrations
datadog/**/*datadog_integration_fastly* @DataDog/integrations-tools-and-libraries @DataDog/saas-integrations
datadog/**/*datadog_integration_gcp_sts* @DataDog/integrations-tools-and-libraries @DataDog/gcp-integrations
datadog/**/*datadog_integration_gcp* @DataDog/integrations-tools-and-libraries @DataDog/gcp-integrations
datadog/**/*datadog_restriction_policy* @DataDog/integrations-tools-and-libraries @DataDog/aaa-granular-access
datadog/**/*datadog_sensitive_data_scanner* @DataDog/integrations-tools-and-libraries @DataDog/logs-app @DataDog/sensitive-data-scanner
datadog/**/*datadog_service_account* @DataDog/integrations-tools-and-libraries @DataDog/team-aaa
Expand Down
1 change: 1 addition & 0 deletions datadog/fwprovider/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var Resources = []func() resource.Resource{
NewIntegrationConfluentResourceResource,
NewIntegrationFastlyAccountResource,
NewIntegrationFastlyServiceResource,
NewIntegrationGcpResource,
NewIntegrationGcpStsResource,
NewIpAllowListResource,
NewRestrictionPolicyResource,
Expand Down
352 changes: 352 additions & 0 deletions datadog/fwprovider/resource_datadog_integration_gcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
package fwprovider

import (
"context"
"sync"

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

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
"github.com/hashicorp/terraform-plugin-framework/diag"
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/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"

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

const (
defaultType = "service_account"
defaultAuthURI = "https://accounts.google.com/o/oauth2/auth"
defaultTokenURI = "https://oauth2.googleapis.com/token"
defaultAuthProviderX509CertURL = "https://www.googleapis.com/oauth2/v1/certs"
defaultClientX509CertURLPrefix = "https://www.googleapis.com/robot/v1/metadata/x509/"
)

var (
integrationGcpMutex sync.Mutex
_ resource.ResourceWithConfigure = (*integrationGcpResource)(nil)
_ resource.ResourceWithImportState = (*integrationGcpResource)(nil)
)

type integrationGcpResource struct {
api *datadogV1.GCPIntegrationApi
auth context.Context
}

type integrationGcpModel struct {
ID types.String `tfsdk:"id"`
ProjectID types.String `tfsdk:"project_id"`
PrivateKeyId types.String `tfsdk:"private_key_id"`
PrivateKey types.String `tfsdk:"private_key"`
ClientEmail types.String `tfsdk:"client_email"`
ClientId types.String `tfsdk:"client_id"`
Automute types.Bool `tfsdk:"automute"`
HostFilters types.String `tfsdk:"host_filters"`
ResourceCollectionEnabled types.Bool `tfsdk:"resource_collection_enabled"`
CspmResourceCollectionEnabled types.Bool `tfsdk:"cspm_resource_collection_enabled"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this typically called is_cspm_enabled?

Copy link

@ash-ddog ash-ddog Jan 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if we have to leave the `tfsdk:"cspm_resource_collection_enabled"` , we could rename the field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but keeping it named as is for backwards compatibility (and the fact that this is a public repo) with https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/integration_gcp#optional

IsSecurityCommandCenterEnabled types.Bool `tfsdk:"is_security_command_center_enabled"`
}

func NewIntegrationGcpResource() resource.Resource {
return &integrationGcpResource{}
}

func (r *integrationGcpResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
providerData, _ := request.ProviderData.(*FrameworkProvider)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I believe the , _ can be removed? It's a bool that makes this a safe cast, but we ignore the bool anyways. I think we'll get an NPE on the next two lines anyway if the cast fails.

r.api = providerData.DatadogApiInstances.GetGCPIntegrationApiV1()
r.auth = providerData.Auth
}

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

func (r *integrationGcpResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Description: "This resource is deprecated—use the `datadog_integration_gcp_sts` resource instead. Provides a Datadog - Google Cloud Platform integration resource. This can be used to create and manage Datadog - Google Cloud Platform integration.",
Attributes: map[string]schema.Attribute{
"project_id": schema.StringAttribute{
Description: "Your Google Cloud project ID found in your JSON service account key.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"private_key_id": schema.StringAttribute{
Description: "Your private key ID found in your JSON service account key.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"private_key": schema.StringAttribute{
Description: "Your private key name found in your JSON service account key.",
Required: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"client_email": schema.StringAttribute{
Description: "Your email found in your JSON service account key.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"client_id": schema.StringAttribute{
Description: "Your ID found in your JSON service account key.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"host_filters": schema.StringAttribute{
Description: "Limit the GCE instances that are pulled into Datadog by using tags. Only hosts that match one of the defined tags are imported into Datadog.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
"automute": schema.BoolAttribute{
Description: "Silence monitors for expected GCE instance shutdowns.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"resource_collection_enabled": schema.BoolAttribute{
Description: "When enabled, Datadog scans for all resources in your GCP environment.",
Optional: true,
Computed: true,
},
"cspm_resource_collection_enabled": schema.BoolAttribute{
Description: "Whether Datadog collects cloud security posture management resources from your GCP project. If enabled, requires `resource_collection_enabled` to also be enabled.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"is_security_command_center_enabled": schema.BoolAttribute{
Description: "When enabled, Datadog will attempt to collect Security Command Center Findings. Note: This requires additional permissions on the service account.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"id": utils.ResourceIDAttribute(),
},
}
}

func (r *integrationGcpResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, frameworkPath.Root("id"), request, response)
}

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

integration, err := r.getGCPIntegration(state)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error listing GCP integration"))
return
}

if integration == nil {
response.State.RemoveResource(ctx)
return
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always been weird to me that our APIs for POST and PUT do not return the resource that was created/updated. Obv out of scope for this PR, but we should move towards the typical REST API (i.e. always return a resource). We can make the updates to our API without even having to change our UI or this terraform code (but we should eventually, just doesn't have to be done at the same time).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, but that would be a larger discussion beyond this PR.


// Save data into Terraform state
r.updateState(ctx, &state, integration)

response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

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

integrationGcpMutex.Lock()
defer integrationGcpMutex.Unlock()

diags := diag.Diagnostics{}
body := r.buildIntegrationGcpRequestBodyBase(state)
r.addDefaultsToBody(body, state)
r.addRequiredFieldsToBody(body, state)
r.addOptionalFieldsToBody(body, state)

response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

_, _, err := r.api.CreateGCPIntegration(r.auth, *body)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error creating GCP integration"))
return
}
integration, err := r.getGCPIntegration(state)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error listing GCP integration"))
return
}
if integration == nil {
response.Diagnostics.AddError("error retrieving GCP integration", "")
return
}

// Save data into Terraform state
r.updateState(ctx, &state, integration)

response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

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

integrationGcpMutex.Lock()
defer integrationGcpMutex.Unlock()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is locking here in Update but in Create it's the first thing that happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving to be consistent with all three methods.


diags := diag.Diagnostics{}
body := r.buildIntegrationGcpRequestBodyBase(state)
r.addOptionalFieldsToBody(body, state)

response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

_, _, err := r.api.UpdateGCPIntegration(r.auth, *body)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error updating GCP integration"))
return
}
integration, err := r.getGCPIntegration(state)
if err != nil {
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error listing GCP integration"))
return
}
if integration == nil {
response.Diagnostics.AddError("error retrieving GCP integration", "")
return
}

// Save data into Terraform state
r.updateState(ctx, &state, integration)

response.Diagnostics.Append(response.State.Set(ctx, &state)...)
}

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

integrationGcpMutex.Lock()
defer integrationGcpMutex.Unlock()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: locking in different place than Create, but inline with Update


diags := diag.Diagnostics{}
body := r.buildIntegrationGcpRequestBodyBase(state)

response.Diagnostics.Append(diags...)

_, httpResp, err := r.api.DeleteGCPIntegration(r.auth, *body)
if err != nil {
if httpResp != nil && httpResp.StatusCode == 404 {
return
}
response.Diagnostics.Append(utils.FrameworkErrorDiag(err, "error deleting GCP integration"))
return
}
}

func (r *integrationGcpResource) updateState(ctx context.Context, state *integrationGcpModel, resp *datadogV1.GCPAccount) {
projectId := types.StringValue(resp.GetProjectId())
// ProjectID and ClientEmail are the only parameters required in all mutating API requests
state.ID = projectId
state.ProjectID = projectId
state.ClientEmail = types.StringValue(resp.GetClientEmail())

// Computed Values
state.Automute = types.BoolValue(resp.GetAutomute())
state.HostFilters = types.StringValue(resp.GetHostFilters())
state.CspmResourceCollectionEnabled = types.BoolValue(resp.GetIsCspmEnabled())
state.ResourceCollectionEnabled = types.BoolValue(resp.GetResourceCollectionEnabled())
state.IsSecurityCommandCenterEnabled = types.BoolValue(resp.GetIsSecurityCommandCenterEnabled())

// Non-computed values
if clientId, ok := resp.GetClientIdOk(); ok {
state.ClientId = types.StringValue(*clientId)
}
if privateKey, ok := resp.GetPrivateKeyOk(); ok {
state.PrivateKey = types.StringValue(*privateKey)
}
if privateKeyId, ok := resp.GetPrivateKeyIdOk(); ok {
state.PrivateKeyId = types.StringValue(*privateKeyId)
}
}

func (r *integrationGcpResource) getGCPIntegration(state integrationGcpModel) (*datadogV1.GCPAccount, error) {
resp, _, err := r.api.ListGCPIntegration(r.auth)
if err != nil {
return nil, err
}

for _, integration := range resp {
if integration.GetProjectId() == state.ProjectID.ValueString() && integration.GetClientEmail() == state.ClientEmail.ValueString() {
if err := utils.CheckForUnparsed(integration); err != nil {
return nil, err
}
return &integration, nil
}
}

return nil, nil // Leave handling of how to deal with nil account to the caller
}

func (r *integrationGcpResource) buildIntegrationGcpRequestBodyBase(state integrationGcpModel) *datadogV1.GCPAccount {
body := datadogV1.NewGCPAccountWithDefaults()
body.SetProjectId(state.ProjectID.ValueString())
body.SetClientEmail(state.ClientEmail.ValueString())

return body
}

func (r *integrationGcpResource) addDefaultsToBody(body *datadogV1.GCPAccount, state integrationGcpModel) {
body.SetType(defaultType)
body.SetAuthUri(defaultAuthURI)
body.SetAuthProviderX509CertUrl(defaultAuthProviderX509CertURL)
body.SetClientX509CertUrl(defaultClientX509CertURLPrefix + state.ClientEmail.ValueString())
body.SetTokenUri(defaultTokenURI)
}

func (r *integrationGcpResource) addRequiredFieldsToBody(body *datadogV1.GCPAccount, state integrationGcpModel) {
body.SetClientId(state.ClientId.ValueString())
body.SetPrivateKey(state.PrivateKey.ValueString())
body.SetPrivateKeyId(state.PrivateKeyId.ValueString())
}

func (r *integrationGcpResource) addOptionalFieldsToBody(body *datadogV1.GCPAccount, state integrationGcpModel) {
body.SetAutomute(state.Automute.ValueBool())
body.SetIsCspmEnabled(state.CspmResourceCollectionEnabled.ValueBool())
body.SetIsSecurityCommandCenterEnabled(state.IsSecurityCommandCenterEnabled.ValueBool())
body.SetHostFilters(state.HostFilters.ValueString())
if !state.ResourceCollectionEnabled.IsUnknown() {
body.SetResourceCollectionEnabled(state.ResourceCollectionEnabled.ValueBool())
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this is special case? (i.e. I don't see IsUnknown() used anywhere else but ResourceCollectionEnabled)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly for backwards compatibility. The API will return an error if the CSPM flag is on but the Resource Collection Flag is explicitly set to False. In this case, we don't want to set any value for the Resource Collection flag unless a value was explicitly set by the user.

}
1 change: 0 additions & 1 deletion datadog/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ func Provider() *schema.Provider {
"datadog_integration_aws_tag_filter": resourceDatadogIntegrationAwsTagFilter(),
"datadog_integration_aws_lambda_arn": resourceDatadogIntegrationAwsLambdaArn(),
"datadog_integration_aws_log_collection": resourceDatadogIntegrationAwsLogCollection(),
"datadog_integration_gcp": resourceDatadogIntegrationGcp(),
"datadog_integration_opsgenie_service_object": resourceDatadogIntegrationOpsgenieService(),
"datadog_integration_pagerduty": resourceDatadogIntegrationPagerduty(),
"datadog_integration_pagerduty_service_object": resourceDatadogIntegrationPagerdutySO(),
Expand Down