Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions datadog/fwprovider/data_source_datadog_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (d *apiKeyDataSource) Metadata(_ context.Context, req datasource.MetadataRe

func (d *apiKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Use this data source to retrieve information about an existing api key. Deprecated. This will be removed in a future release with prior notice. Securely store your API keys using a secret management system or use the datadog_api_key resource to manage API keys in your Datadog account.",
Description: "Use this data source to retrieve information about an existing API key. **Deprecated**: This will be removed in a future release with prior notice. For secure access to API key values without storing them in Terraform state, use the ephemeral `datadog_api_key` resource instead. See the ephemeral resource documentation for examples of secure API key access patterns.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Name for API Key.",
Expand All @@ -59,7 +59,7 @@ func (d *apiKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
Optional: true,
},
"key": schema.StringAttribute{
Description: "The value of the API Key.",
Description: "The value of the API Key. **Security Note**: This field exposes sensitive data in Terraform state. For secure access without state storage, use the ephemeral `datadog_api_key` resource instead.",
Computed: true,
Sensitive: true,
},
Expand All @@ -68,7 +68,7 @@ func (d *apiKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
Computed: true,
},
},
DeprecationMessage: "Deprecated. This will be removed in a future release with prior notice. Securely store your API keys using a secret management system or use the datadog_api_key resource to manage API keys in your Datadog account.",
DeprecationMessage: "This data source is deprecated and will be removed in a future release with prior notice. For secure access to API key values without storing them in Terraform state, use the ephemeral datadog_api_key resource instead.",
}
}

Expand Down Expand Up @@ -168,7 +168,7 @@ func (r *apiKeyDataSource) updateState(state *apiKeyDataSourceModel, apiKeyData
func (r *apiKeyDataSource) checkAPIDeprecated(apiKeyData *datadogV2.FullAPIKey, resp *datasource.ReadResponse) bool {
apiKeyAttributes := apiKeyData.GetAttributes()
if !apiKeyAttributes.HasKey() {
resp.Diagnostics.AddError("Deprecated", "The datadog_api_key data source is deprecated and will be removed in a future release. Securely store your API key using a secret management system or use the datadog_api_key resource to manage API keys in your Datadog account.")
resp.Diagnostics.AddError("Deprecated", "The datadog_api_key data source is deprecated and will be removed in a future release. For secure access to API key values without storing them in Terraform state, use the ephemeral datadog_api_key resource instead.")
return true
}
return false
Expand Down
137 changes: 137 additions & 0 deletions datadog/fwprovider/ephemeral_resource_datadog_api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package fwprovider

import (
"context"
"log"

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

// Interface assertions for EphemeralAPIKeyResource
var (
_ ephemeral.EphemeralResource = &EphemeralAPIKeyResource{}
_ ephemeral.EphemeralResourceWithConfigure = &EphemeralAPIKeyResource{}
)

// EphemeralAPIKeyResource implements ephemeral API key resource
type EphemeralAPIKeyResource struct {
Api *datadogV2.KeyManagementApi
Auth context.Context
}

// EphemeralAPIKeyModel represents the data model for the ephemeral API key resource
type EphemeralAPIKeyModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Key types.String `tfsdk:"key"`
RemoteConfigReadEnabled types.Bool `tfsdk:"remote_config_read_enabled"`
}

// NewEphemeralAPIKeyResource creates a new ephemeral API key resource
func NewEphemeralAPIKeyResource() ephemeral.EphemeralResource {
return &EphemeralAPIKeyResource{}
}

// Metadata implements the core ephemeral.EphemeralResource interface
func (r *EphemeralAPIKeyResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = "api_key" // Will become "datadog_api_key" via wrapper
}

// Schema implements the core ephemeral.EphemeralResource interface
func (r *EphemeralAPIKeyResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Retrieves an existing Datadog API key as an ephemeral resource. The API key value is retrieved securely and made available for use in other resources without being stored in state.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
Description: "The ID of the API key to retrieve.",
},
"name": schema.StringAttribute{
Computed: true,
Description: "The name of the API key.",
},
"key": schema.StringAttribute{
Computed: true,
Sensitive: true,
Description: "The actual API key value (sensitive).",
},
"remote_config_read_enabled": schema.BoolAttribute{
Computed: true,
Description: "Whether remote configuration reads are enabled for this key.",
},
},
}
}

// Open implements the core ephemeral.EphemeralResource interface
// This is where the ephemeral resource acquires the API key data
func (r *EphemeralAPIKeyResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
// 1. Extract API key ID from config
var config EphemeralAPIKeyModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// 2. Fetch API key from Datadog API
apiKey, httpResp, err := r.Api.GetAPIKey(r.Auth, config.ID.ValueString())
Copy link
Contributor

Choose a reason for hiding this comment

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

question (blocking) - Is there a way to make this configurable such that someone could retrieve their API key from their own Secret Manager instead of relying on the Datadog API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tyjet do you mean using some external (non-Datadog) API/provider?
If so, yes, if someone wants to configure their infra to use a secret manager instead of using this ephemeral resource they 💯 can do that, most known secret managers have terraform providers and resources devs can use.

That would mean, that devs won't be using this resource, as this is specifically to make a state-secure way to fetch this data via Datadog API.

I believe this leads to an important question: do we want to allow a way to get API key via our provider or do we want to just cut this API path from our provider entirely and force devs to use external secret managers?
And if we believe restricting this in our Terraform provider (despite still having API support because of reasons) is "the way" to lead devs to better/proper patterns then we don't need an ephemeral resource for datadog_api_key at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation, I think I understand much better now.

do we want to allow a way to get API key via our provider or do we want to just cut this API path from our provider entirely and force devs to use external secret managers?
^^ -- At this point, yes, we want to allow API key access via the provider (and public API). Restricting the terraform provider would be too onerous.

if err != nil {
log.Printf("[ERROR] Ephemeral open operation failed for api_key: %v", err)
resp.Diagnostics.AddError(
"API Key Retrieval Failed",
"Unable to fetch API key data from Datadog API",
)
return
}

// Check HTTP response status
if httpResp != nil && httpResp.StatusCode >= 400 {
log.Printf("[WARN] Ephemeral open operation failed for api_key")
resp.Diagnostics.AddError(
"API Key Retrieval Failed",
"Received error response from Datadog API",
)
return
}

// 3. Extract API key data from response
apiKeyData := apiKey.GetData()
apiKeyAttributes := apiKeyData.GetAttributes()

// 4. Set result data (including the sensitive key value)
result := EphemeralAPIKeyModel{
ID: config.ID,
Name: types.StringValue(apiKeyAttributes.GetName()),
Key: types.StringValue(apiKeyAttributes.GetKey()), // SENSITIVE
RemoteConfigReadEnabled: types.BoolValue(apiKeyAttributes.GetRemoteConfigReadEnabled()),
}

resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...)
if resp.Diagnostics.HasError() {
return
}

log.Printf("[DEBUG] Ephemeral open operation succeeded for api_key")
}

// Configure implements the optional ephemeral.EphemeralResourceWithConfigure interface
func (r *EphemeralAPIKeyResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
if req.ProviderData == nil {
return
}

providerData, ok := req.ProviderData.(*FrameworkProvider)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Configure Type",
"Expected *FrameworkProvider",
)
return
}

r.Api = providerData.DatadogApiInstances.GetKeyManagementApiV2()
r.Auth = providerData.Auth
}
104 changes: 104 additions & 0 deletions datadog/fwprovider/framework_ephemeral_resource_wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package fwprovider

import (
"context"

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

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

// Interface assertions for FrameworkEphemeralResourceWrapper
var (
_ ephemeral.EphemeralResource = &FrameworkEphemeralResourceWrapper{}
_ ephemeral.EphemeralResourceWithConfigure = &FrameworkEphemeralResourceWrapper{}
_ ephemeral.EphemeralResourceWithValidateConfig = &FrameworkEphemeralResourceWrapper{}
_ ephemeral.EphemeralResourceWithConfigValidators = &FrameworkEphemeralResourceWrapper{}
_ ephemeral.EphemeralResourceWithRenew = &FrameworkEphemeralResourceWrapper{}
_ ephemeral.EphemeralResourceWithClose = &FrameworkEphemeralResourceWrapper{}
)

// NewFrameworkEphemeralResourceWrapper creates a new ephemeral resource wrapper following
// the same pattern as the existing FrameworkResourceWrapper
func NewFrameworkEphemeralResourceWrapper(i *ephemeral.EphemeralResource) ephemeral.EphemeralResource {
return &FrameworkEphemeralResourceWrapper{
innerResource: i,
}
}

// FrameworkEphemeralResourceWrapper wraps ephemeral resources to provide consistent behavior
// across all ephemeral resources, following the existing FrameworkResourceWrapper pattern
type FrameworkEphemeralResourceWrapper struct {
innerResource *ephemeral.EphemeralResource
}

// Metadata implements the core ephemeral.EphemeralResource interface
// Adds provider type name prefix to the resource type name, following existing pattern
func (r *FrameworkEphemeralResourceWrapper) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
(*r.innerResource).Metadata(ctx, req, resp)
resp.TypeName = req.ProviderTypeName + resp.TypeName
}

// Schema implements the core ephemeral.EphemeralResource interface
// Enriches schema with common framework patterns
func (r *FrameworkEphemeralResourceWrapper) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
(*r.innerResource).Schema(ctx, req, resp)
fwutils.EnrichFrameworkEphemeralResourceSchema(&resp.Schema)
}

// Open implements the core ephemeral.EphemeralResource interface
// This is where ephemeral resources create/acquire their temporary resources
func (r *FrameworkEphemeralResourceWrapper) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
(*r.innerResource).Open(ctx, req, resp)
}

// Configure implements the optional ephemeral.EphemeralResourceWithConfigure interface
// Uses interface detection to only call if the inner resource supports configuration
func (r *FrameworkEphemeralResourceWrapper) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
rCasted, ok := (*r.innerResource).(ephemeral.EphemeralResourceWithConfigure)
if ok {
if req.ProviderData == nil {
return
}
_, ok := req.ProviderData.(*FrameworkProvider)
if !ok {
resp.Diagnostics.AddError("Unexpected Ephemeral Resource Configure Type", "")
return
}

rCasted.Configure(ctx, req, resp)
}
}

// ValidateConfig implements the optional ephemeral.EphemeralResourceWithValidateConfig interface
// Uses interface detection to only call if the inner resource supports validation
func (r *FrameworkEphemeralResourceWrapper) ValidateConfig(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
if rCasted, ok := (*r.innerResource).(ephemeral.EphemeralResourceWithValidateConfig); ok {
rCasted.ValidateConfig(ctx, req, resp)
}
}

// ConfigValidators implements the optional ephemeral.EphemeralResourceWithConfigValidators interface
// Uses interface detection to only call if the inner resource supports declarative validators
func (r *FrameworkEphemeralResourceWrapper) ConfigValidators(ctx context.Context) []ephemeral.ConfigValidator {
if rCasted, ok := (*r.innerResource).(ephemeral.EphemeralResourceWithConfigValidators); ok {
return rCasted.ConfigValidators(ctx)
}
return nil
}

// Renew implements the optional ephemeral.EphemeralResourceWithRenew interface
// Uses interface detection to only call if the inner resource supports renewal
func (r *FrameworkEphemeralResourceWrapper) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {
if rCasted, ok := (*r.innerResource).(ephemeral.EphemeralResourceWithRenew); ok {
rCasted.Renew(ctx, req, resp)
}
}

// Close implements the optional ephemeral.EphemeralResourceWithClose interface
// Uses interface detection to only call if the inner resource supports cleanup
func (r *FrameworkEphemeralResourceWrapper) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {
if rCasted, ok := (*r.innerResource).(ephemeral.EphemeralResourceWithClose); ok {
rCasted.Close(ctx, req, resp)
}
}
32 changes: 31 additions & 1 deletion datadog/fwprovider/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
Expand All @@ -29,6 +30,7 @@ import (
)

var _ provider.Provider = &FrameworkProvider{}
var _ provider.ProviderWithEphemeralResources = &FrameworkProvider{}

var Resources = []func() resource.Resource{
NewAgentlessScanningAwsScanOptionsResource,
Expand Down Expand Up @@ -101,6 +103,10 @@ var Resources = []func() resource.Resource{
NewIncidentNotificationRuleResource,
}

var EphemeralResources = []func() ephemeral.EphemeralResource{
NewEphemeralAPIKeyResource,
}

var Datasources = []func() datasource.DataSource{
NewAPIKeyDataSource,
NewApplicationKeyDataSource,
Expand Down Expand Up @@ -146,6 +152,7 @@ type FrameworkProvider struct {
CommunityClient *datadogCommunity.Client
DatadogApiInstances *utils.ApiInstances
Auth context.Context
StoreSensitiveState bool

ConfigureCallbackFunc func(p *FrameworkProvider, request *provider.ConfigureRequest, config *ProviderSchema) diag.Diagnostics
Now func() time.Time
Expand All @@ -170,6 +177,7 @@ type ProviderSchema struct {
HttpClientRetryBackoffBase types.Int64 `tfsdk:"http_client_retry_backoff_base"`
HttpClientRetryMaxRetries types.Int64 `tfsdk:"http_client_retry_max_retries"`
DefaultTags []DefaultTag `tfsdk:"default_tags"`
StoreSensitiveState types.String `tfsdk:"store_sensitive_state"`
}

type DefaultTag struct {
Expand Down Expand Up @@ -207,6 +215,18 @@ func (p *FrameworkProvider) DataSources(_ context.Context) []func() datasource.D
return wrappedDatasources
}

func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
var wrappedResources []func() ephemeral.EphemeralResource
for _, f := range EphemeralResources {
r := f()
wrappedResources = append(wrappedResources, func() ephemeral.EphemeralResource {
return NewFrameworkEphemeralResourceWrapper(&r)
})
}

return wrappedResources
}

func (p *FrameworkProvider) Metadata(_ context.Context, _ provider.MetadataRequest, response *provider.MetadataResponse) {
response.TypeName = "datadog_"
}
Expand Down Expand Up @@ -282,6 +302,10 @@ func (p *FrameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest,
Optional: true,
Description: "The HTTP request maximum retry number. Defaults to 3.",
},
"store_sensitive_state": schema.StringAttribute{
Optional: true,
Description: "Whether to expose API key values in Terraform state. Valid values are [`true`, `false`]. Defaults to `true` for backwards compatibility. When false, API key resources will not include the key value, requiring the use of ephemeral datadog_api_key resources instead.",
},
},
Blocks: map[string]schema.Block{
"default_tags": schema.ListNestedBlock{
Expand Down Expand Up @@ -320,9 +344,10 @@ func (p *FrameworkProvider) Configure(ctx context.Context, request provider.Conf
return
}

// Make config available for data sources and resources
// Make config available for data sources, resources, and ephemeral resources
response.DataSourceData = p
response.ResourceData = p
response.EphemeralResourceData = p
}

func (p *FrameworkProvider) ConfigureConfigDefaults(ctx context.Context, config *ProviderSchema) diag.Diagnostics {
Expand Down Expand Up @@ -421,6 +446,9 @@ func (p *FrameworkProvider) ConfigureConfigDefaults(ctx context.Context, config
if config.HttpClientRetryEnabled.IsNull() {
config.HttpClientRetryEnabled = types.StringValue("true")
}
if config.StoreSensitiveState.IsNull() {
config.StoreSensitiveState = types.StringValue("true")
}

// Run validations on the provider config after defaults and values from
// env var has been set.
Expand Down Expand Up @@ -474,6 +502,8 @@ func defaultConfigureFunc(p *FrameworkProvider, request *provider.ConfigureReque
diags := diag.Diagnostics{}
validate, _ := strconv.ParseBool(config.Validate.ValueString())
httpClientRetryEnabled, _ := strconv.ParseBool(config.HttpClientRetryEnabled.ValueString())
storeSensitiveState, _ := strconv.ParseBool(config.StoreSensitiveState.ValueString())
p.StoreSensitiveState = storeSensitiveState

cloudProviderType := config.CloudProviderType.ValueString()
cloudProviderRegion := config.CloudProviderRegion.ValueString()
Expand Down
Loading
Loading