Skip to content

Commit f08b4be

Browse files
committed
feat(api-key): add ephemeral datadog_api_key resource
Implements ephemeral API key resource using Terraform Plugin Framework: - New ephemeral.datadog_api_key resource for stateless key access - Secure retrieval without storing sensitive values in state - Framework provider integration with EphemeralResources registry - Test coverage and validation for ephemeral operations Enables secure patterns where API keys are fetched at runtime without persistence in Terraform state files.
1 parent f46e92d commit f08b4be

File tree

3 files changed

+217
-1
lines changed

3 files changed

+217
-1
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package fwprovider
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
8+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
9+
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
)
12+
13+
// Interface assertions for EphemeralAPIKeyResource
14+
var (
15+
_ ephemeral.EphemeralResource = &EphemeralAPIKeyResource{}
16+
_ ephemeral.EphemeralResourceWithConfigure = &EphemeralAPIKeyResource{}
17+
)
18+
19+
// EphemeralAPIKeyResource implements ephemeral API key resource
20+
type EphemeralAPIKeyResource struct {
21+
Api *datadogV2.KeyManagementApi
22+
Auth context.Context
23+
}
24+
25+
// EphemeralAPIKeyModel represents the data model for the ephemeral API key resource
26+
type EphemeralAPIKeyModel struct {
27+
ID types.String `tfsdk:"id"`
28+
Name types.String `tfsdk:"name"`
29+
Key types.String `tfsdk:"key"`
30+
RemoteConfigReadEnabled types.Bool `tfsdk:"remote_config_read_enabled"`
31+
}
32+
33+
// NewEphemeralAPIKeyResource creates a new ephemeral API key resource
34+
func NewEphemeralAPIKeyResource() ephemeral.EphemeralResource {
35+
return &EphemeralAPIKeyResource{}
36+
}
37+
38+
// Metadata implements the core ephemeral.EphemeralResource interface
39+
func (r *EphemeralAPIKeyResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
40+
resp.TypeName = "api_key" // Will become "datadog_api_key" via wrapper
41+
}
42+
43+
// Schema implements the core ephemeral.EphemeralResource interface
44+
func (r *EphemeralAPIKeyResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
45+
resp.Schema = schema.Schema{
46+
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.",
47+
Attributes: map[string]schema.Attribute{
48+
"id": schema.StringAttribute{
49+
Required: true,
50+
Description: "The ID of the API key to retrieve.",
51+
},
52+
"name": schema.StringAttribute{
53+
Computed: true,
54+
Description: "The name of the API key.",
55+
},
56+
"key": schema.StringAttribute{
57+
Computed: true,
58+
Sensitive: true,
59+
Description: "The actual API key value (sensitive).",
60+
},
61+
"remote_config_read_enabled": schema.BoolAttribute{
62+
Computed: true,
63+
Description: "Whether remote configuration reads are enabled for this key.",
64+
},
65+
},
66+
}
67+
}
68+
69+
// Open implements the core ephemeral.EphemeralResource interface
70+
// This is where the ephemeral resource acquires the API key data
71+
func (r *EphemeralAPIKeyResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
72+
// 1. Extract API key ID from config
73+
var config EphemeralAPIKeyModel
74+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
75+
if resp.Diagnostics.HasError() {
76+
return
77+
}
78+
79+
// 2. Fetch API key from Datadog API
80+
apiKey, httpResp, err := r.Api.GetAPIKey(r.Auth, config.ID.ValueString())
81+
if err != nil {
82+
log.Printf("[ERROR] Ephemeral open operation failed for api_key: %v", err)
83+
resp.Diagnostics.AddError(
84+
"API Key Retrieval Failed",
85+
"Unable to fetch API key data from Datadog API",
86+
)
87+
return
88+
}
89+
90+
// Check HTTP response status
91+
if httpResp != nil && httpResp.StatusCode >= 400 {
92+
log.Printf("[WARN] Ephemeral open operation failed for api_key")
93+
resp.Diagnostics.AddError(
94+
"API Key Retrieval Failed",
95+
"Received error response from Datadog API",
96+
)
97+
return
98+
}
99+
100+
// 3. Extract API key data from response
101+
apiKeyData := apiKey.GetData()
102+
apiKeyAttributes := apiKeyData.GetAttributes()
103+
104+
// 4. Set result data (including the sensitive key value)
105+
result := EphemeralAPIKeyModel{
106+
ID: config.ID,
107+
Name: types.StringValue(apiKeyAttributes.GetName()),
108+
Key: types.StringValue(apiKeyAttributes.GetKey()), // SENSITIVE
109+
RemoteConfigReadEnabled: types.BoolValue(apiKeyAttributes.GetRemoteConfigReadEnabled()),
110+
}
111+
112+
resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...)
113+
if resp.Diagnostics.HasError() {
114+
return
115+
}
116+
117+
log.Printf("[DEBUG] Ephemeral open operation succeeded for api_key")
118+
}
119+
120+
// Configure implements the optional ephemeral.EphemeralResourceWithConfigure interface
121+
func (r *EphemeralAPIKeyResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
122+
if req.ProviderData == nil {
123+
return
124+
}
125+
126+
providerData, ok := req.ProviderData.(*FrameworkProvider)
127+
if !ok {
128+
resp.Diagnostics.AddError(
129+
"Unexpected Configure Type",
130+
"Expected *FrameworkProvider",
131+
)
132+
return
133+
}
134+
135+
r.Api = providerData.DatadogApiInstances.GetKeyManagementApiV2()
136+
r.Auth = providerData.Auth
137+
}

datadog/fwprovider/framework_provider.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1717
"github.com/hashicorp/terraform-plugin-framework/datasource"
1818
"github.com/hashicorp/terraform-plugin-framework/diag"
19+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
1920
"github.com/hashicorp/terraform-plugin-framework/provider"
2021
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
2122
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -29,6 +30,7 @@ import (
2930
)
3031

3132
var _ provider.Provider = &FrameworkProvider{}
33+
var _ provider.ProviderWithEphemeralResources = &FrameworkProvider{}
3234

3335
var Resources = []func() resource.Resource{
3436
NewAgentlessScanningAwsScanOptionsResource,
@@ -101,6 +103,10 @@ var Resources = []func() resource.Resource{
101103
NewIncidentNotificationRuleResource,
102104
}
103105

106+
var EphemeralResources = []func() ephemeral.EphemeralResource{
107+
NewEphemeralAPIKeyResource,
108+
}
109+
104110
var Datasources = []func() datasource.DataSource{
105111
NewAPIKeyDataSource,
106112
NewApplicationKeyDataSource,
@@ -207,6 +213,18 @@ func (p *FrameworkProvider) DataSources(_ context.Context) []func() datasource.D
207213
return wrappedDatasources
208214
}
209215

216+
func (p *FrameworkProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
217+
var wrappedResources []func() ephemeral.EphemeralResource
218+
for _, f := range EphemeralResources {
219+
r := f()
220+
wrappedResources = append(wrappedResources, func() ephemeral.EphemeralResource {
221+
return NewFrameworkEphemeralResourceWrapper(&r)
222+
})
223+
}
224+
225+
return wrappedResources
226+
}
227+
210228
func (p *FrameworkProvider) Metadata(_ context.Context, _ provider.MetadataRequest, response *provider.MetadataResponse) {
211229
response.TypeName = "datadog_"
212230
}
@@ -320,9 +338,10 @@ func (p *FrameworkProvider) Configure(ctx context.Context, request provider.Conf
320338
return
321339
}
322340

323-
// Make config available for data sources and resources
341+
// Make config available for data sources, resources, and ephemeral resources
324342
response.DataSourceData = p
325343
response.ResourceData = p
344+
response.EphemeralResourceData = p
326345
}
327346

328347
func (p *FrameworkProvider) ConfigureConfigDefaults(ctx context.Context, config *ProviderSchema) diag.Diagnostics {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/terraform-providers/terraform-provider-datadog/datadog/fwprovider"
12+
)
13+
14+
// TODO(v6-protocol): once the provider upgrades to protocol v6, add an acceptance
15+
// test using terraform-plugin-testing's echo provider to assert the API key's
16+
// ephemeral value never persists in Terraform state.
17+
18+
// TestEphemeralAPIKeyResource_Metadata tests the Metadata method
19+
func TestEphemeralAPIKeyResource_Metadata(t *testing.T) {
20+
resource := fwprovider.NewEphemeralAPIKeyResource()
21+
22+
req := ephemeral.MetadataRequest{
23+
ProviderTypeName: "datadog",
24+
}
25+
resp := &ephemeral.MetadataResponse{}
26+
27+
resource.Metadata(context.Background(), req, resp)
28+
29+
assert.Equal(t, "_api_key", resp.TypeName)
30+
}
31+
32+
// TestEphemeralAPIKeyResource_Schema tests the Schema method
33+
func TestEphemeralAPIKeyResource_Schema(t *testing.T) {
34+
resource := fwprovider.NewEphemeralAPIKeyResource()
35+
36+
req := ephemeral.SchemaRequest{}
37+
resp := &ephemeral.SchemaResponse{}
38+
39+
resource.Schema(context.Background(), req, resp)
40+
41+
// Verify required attributes exist
42+
require.NotNil(t, resp.Schema.Attributes)
43+
44+
// Check that key is marked as sesnsitive
45+
keyAttr, exists := resp.Schema.Attributes["key"]
46+
require.True(t, exists)
47+
assert.True(t, keyAttr.IsSensitive())
48+
}
49+
50+
// TestEphemeralAPIKeyResource_InterfaceAssertion tests interface compliance
51+
func TestEphemeralAPIKeyResource_InterfaceAssertion(t *testing.T) {
52+
resource := fwprovider.NewEphemeralAPIKeyResource()
53+
54+
// Verify the resource implements required interfaces
55+
_, ok := resource.(ephemeral.EphemeralResource)
56+
assert.True(t, ok, "Resource should implement EphemeralResource")
57+
58+
_, ok = resource.(ephemeral.EphemeralResourceWithConfigure)
59+
assert.True(t, ok, "Resource should implement EphemeralResourceWithConfigure")
60+
}

0 commit comments

Comments
 (0)