Skip to content

Commit 8b565e6

Browse files
EspenAlbertCopilot
andauthored
chore: Adds provider_meta support to the provider (#3618)
* feat: Adds initial provider_meta support and UserAgentTransport * feat: Adds AnalyticsResource support to all resources * feat: PoC SDKv2 and use provider to override * feat: Refactor Analytics resource initialization and add context handling * revert non config/provider-files * refactor: Small cleanups * revert nolint * chore: Fully implement the sdkv2 behavior * refactor: Support PlanModifier * feat: Add MoveState and UpgradeState methods to RSCommon; enhance user agent operations * fix: Add comment to clarify embedding in ProviderMocked struct * fix: Handle null type in provider meta * chore: Minor change and test fix * Update internal/config/resource_base_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: Remove unnecessary blank line in getHTTPClient function * fix: Correct import path for advancedcluster in resource_base_test.go * refactor: Simplify user agent name handling and remove unnecessary string manipulation * fix: Ensure compliance with ImplementedResource interface in AnalyticsResourceFunc * docs: Add clarifying comments for analytics wrapper and context flow - Explain why analyticsResource wrapper is necessary for intercepting CRUD ops - Document complete UserAgentExtra context flow from provider_meta to HTTP transport --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d7048c2 commit 8b565e6

File tree

11 files changed

+694
-26
lines changed

11 files changed

+694
-26
lines changed

internal/config/client.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ func NewClient(c *Credentials, terraformVersion string) (*MongoDBClient, error)
143143
}
144144

145145
func getHTTPClient(c *Credentials) (*http.Client, error) {
146+
// Transport chain (outermost to innermost):
147+
// userAgentTransport -> tfLoggingTransport -> {digestTransport|oauth2.Transport} -> networkLoggingTransport -> baseTransport
148+
//
149+
// This ordering ensures:
150+
// 1. networkLoggingTransport logs ALL requests including digest auth 401 challenges
151+
// 2. tfLoggingTransport only logs final authenticated requests (not sensitive auth details)
152+
// 3. userAgentTransport modifies User-Agent before tfLoggingTransport logs it
146153
transport := networkLoggingBaseTransport()
147154
switch c.AuthMethod() {
148155
case AccessToken:
@@ -167,7 +174,9 @@ func getHTTPClient(c *Credentials) (*http.Client, error) {
167174
transport = digest.NewTransportWithHTTPRoundTripper(c.PublicKey, c.PrivateKey, networkLoggingBaseTransport())
168175
case Unknown:
169176
}
170-
return &http.Client{Transport: tfLoggingInterceptor(transport)}, nil
177+
transport = tfLoggingInterceptor(transport)
178+
transport = newUserAgentTransport(transport, true)
179+
return &http.Client{Transport: transport}, nil
171180
}
172181

173182
func newSDKV2Client(client *http.Client, baseURL, userAgent string) (*admin.APIClient, error) {

internal/config/resource_base.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,55 @@ import (
66

77
"github.com/hashicorp/terraform-plugin-framework/datasource"
88
"github.com/hashicorp/terraform-plugin-framework/resource"
9+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
911
)
1012

1113
const (
1214
errorConfigureSummary = "Unexpected Resource Configure Type"
1315
errorConfigure = "expected *MongoDBClient, got: %T. Please report this issue to the provider developers"
1416
)
1517

18+
type ProviderMeta struct {
19+
ModuleName types.String `tfsdk:"module_name"`
20+
ModuleVersion types.String `tfsdk:"module_version"`
21+
UserAgentExtra types.Map `tfsdk:"user_agent_extra"`
22+
}
23+
24+
type ImplementedResource interface {
25+
resource.ResourceWithImportState
26+
// Additional methods such as upgrade state & plan modifier are optional
27+
SetClient(*MongoDBClient)
28+
GetName() string
29+
}
30+
31+
func AnalyticsResourceFunc(iResource resource.Resource) func() resource.Resource {
32+
commonResource, ok := iResource.(ImplementedResource)
33+
if !ok {
34+
panic(fmt.Sprintf("resource %T didn't comply with the ImplementedResource interface", iResource))
35+
}
36+
return func() resource.Resource {
37+
return analyticsResource(commonResource)
38+
}
39+
}
40+
41+
// analyticsResource wraps an ImplementedResource with RSCommon to add analytics tracking.
42+
// We cannot return iResource directly because we need to intercept all CRUD operations
43+
// to inject provider_meta information into the context before calling the actual resource methods.
44+
func analyticsResource(iResource ImplementedResource) resource.Resource {
45+
return &RSCommon{
46+
ResourceName: iResource.GetName(),
47+
ImplementedResource: iResource,
48+
}
49+
}
50+
1651
// RSCommon is used as an embedded struct for all framework resources. Implements the following plugin-framework defined functions:
1752
// - Metadata
1853
// - Configure
1954
// Client is left empty and populated by the framework when envoking Configure method.
2055
// ResourceName must be defined when creating an instance of a resource.
2156
type RSCommon struct {
57+
ImplementedResource
2258
Client *MongoDBClient
2359
ResourceName string
2460
}
@@ -33,9 +69,117 @@ func (r *RSCommon) Configure(ctx context.Context, req resource.ConfigureRequest,
3369
resp.Diagnostics.AddError(errorConfigureSummary, err.Error())
3470
return
3571
}
72+
r.ImplementedResource.SetClient(client)
73+
}
74+
75+
func (r *RSCommon) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
76+
extra := r.asUserAgentExtra(ctx, UserAgentOperationValueCreate, req.ProviderMeta)
77+
ctx = AddUserAgentExtra(ctx, extra)
78+
r.ImplementedResource.Create(ctx, req, resp)
79+
}
80+
81+
func (r *RSCommon) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
82+
extra := r.asUserAgentExtra(ctx, UserAgentOperationValueRead, req.ProviderMeta)
83+
ctx = AddUserAgentExtra(ctx, extra)
84+
r.ImplementedResource.Read(ctx, req, resp)
85+
}
86+
87+
func (r *RSCommon) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
88+
extra := r.asUserAgentExtra(ctx, UserAgentOperationValueUpdate, req.ProviderMeta)
89+
ctx = AddUserAgentExtra(ctx, extra)
90+
r.ImplementedResource.Update(ctx, req, resp)
91+
}
92+
93+
func (r *RSCommon) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
94+
extra := r.asUserAgentExtra(ctx, UserAgentOperationValueDelete, req.ProviderMeta)
95+
ctx = AddUserAgentExtra(ctx, extra)
96+
r.ImplementedResource.Delete(ctx, req, resp)
97+
}
98+
99+
// Optional interfaces for resource.Resource
100+
func (r *RSCommon) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
101+
// req resource.ImportStateRequest doesn't have ProviderMeta
102+
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
103+
Name: r.ResourceName,
104+
Operation: UserAgentOperationValueImport,
105+
})
106+
r.ImplementedResource.ImportState(ctx, req, resp)
107+
}
108+
109+
func (r *RSCommon) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
110+
resourceWithModifier, ok := r.ImplementedResource.(resource.ResourceWithModifyPlan)
111+
if !ok {
112+
return
113+
}
114+
extra := r.asUserAgentExtra(ctx, UserAgentOperationValuePlanModify, req.ProviderMeta)
115+
ctx = AddUserAgentExtra(ctx, extra)
116+
resourceWithModifier.ModifyPlan(ctx, req, resp)
117+
}
118+
119+
func (r *RSCommon) MoveState(ctx context.Context) []resource.StateMover {
120+
resourceWithMoveState, ok := r.ImplementedResource.(resource.ResourceWithMoveState)
121+
if !ok {
122+
return nil
123+
}
124+
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
125+
Name: r.ResourceName,
126+
Operation: UserAgentOperationValueMoveState,
127+
})
128+
return resourceWithMoveState.MoveState(ctx)
129+
}
130+
131+
func (r *RSCommon) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
132+
resourceWithUpgradeState, ok := r.ImplementedResource.(resource.ResourceWithUpgradeState)
133+
if !ok {
134+
return nil
135+
}
136+
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
137+
Name: r.ResourceName,
138+
Operation: UserAgentOperationValueUpgradeState,
139+
})
140+
return resourceWithUpgradeState.UpgradeState(ctx)
141+
}
142+
143+
// Extra methods not found on resource.Resource
144+
func (r *RSCommon) GetName() string {
145+
return r.ResourceName
146+
}
147+
148+
func (r *RSCommon) SetClient(client *MongoDBClient) {
36149
r.Client = client
37150
}
38151

152+
func (r *RSCommon) asUserAgentExtra(ctx context.Context, reqOperation string, reqProviderMeta tfsdk.Config) UserAgentExtra {
153+
var meta ProviderMeta
154+
uaExtra := UserAgentExtra{
155+
Name: userAgentNameValue(r.ResourceName),
156+
Operation: reqOperation,
157+
}
158+
if reqProviderMeta.Raw.IsNull() {
159+
return uaExtra
160+
}
161+
diags := reqProviderMeta.Get(ctx, &meta)
162+
if diags.HasError() {
163+
return uaExtra
164+
}
165+
166+
extrasLen := len(meta.UserAgentExtra.Elements())
167+
userExtras := make(map[string]types.String, extrasLen)
168+
diags.Append(meta.UserAgentExtra.ElementsAs(ctx, &userExtras, false)...)
169+
if diags.HasError() {
170+
return uaExtra
171+
}
172+
userExtrasString := make(map[string]string, extrasLen)
173+
for k, v := range userExtras {
174+
userExtrasString[k] = v.ValueString()
175+
}
176+
return uaExtra.Combine(UserAgentExtra{
177+
Extras: userExtrasString,
178+
ModuleName: meta.ModuleName.ValueString(),
179+
ModuleVersion: meta.ModuleVersion.ValueString(),
180+
})
181+
}
182+
39183
// DSCommon is used as an embedded struct for all framework data sources. Implements the following plugin-framework defined functions:
40184
// - Metadata
41185
// - Configure
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
)
10+
11+
func NewAnalyticsResourceSDKv2(d *schema.Resource, name string) *schema.Resource {
12+
analyticsResource := &AnalyticsResourceSDKv2{
13+
resource: d,
14+
name: name,
15+
}
16+
/*
17+
We are not initializing deprecated fields, for example Update to avoid the message:
18+
resource mongodbatlas_cloud_backup_snapshot: All fields are ForceNew or Computed w/out Optional, Update is superfluous
19+
20+
Ensure no deprecated fields are used by running `staticcheck ./internal/service/... | grep -v 'd.GetOkExists'` and looking for (SA1019)
21+
GetOkExists we are using in many places; therefore, we use -v (invert match) to filter out lines with different deprecations
22+
Example line:
23+
internal/service/cluster/model_cluster.go:306:14: d.GetOkExists is deprecated: usage is discouraged due to undefined behaviors and may be removed in a future version of the SDK (SA1019)
24+
*/
25+
resource := &schema.Resource{
26+
CustomizeDiff: d.CustomizeDiff,
27+
DeprecationMessage: d.DeprecationMessage,
28+
Description: d.Description,
29+
EnableLegacyTypeSystemApplyErrors: d.EnableLegacyTypeSystemApplyErrors,
30+
EnableLegacyTypeSystemPlanErrors: d.EnableLegacyTypeSystemPlanErrors,
31+
Identity: d.Identity,
32+
ResourceBehavior: d.ResourceBehavior,
33+
Schema: d.Schema,
34+
SchemaFunc: d.SchemaFunc,
35+
SchemaVersion: d.SchemaVersion,
36+
StateUpgraders: d.StateUpgraders,
37+
Timeouts: d.Timeouts,
38+
UpdateWithoutTimeout: d.UpdateWithoutTimeout,
39+
UseJSONNumber: d.UseJSONNumber,
40+
ValidateRawResourceConfigFuncs: d.ValidateRawResourceConfigFuncs,
41+
}
42+
importer := d.Importer
43+
if importer != nil {
44+
resource.Importer = &schema.ResourceImporter{
45+
StateContext: analyticsResource.resourceImport,
46+
}
47+
}
48+
// CreateContext or CreateWithoutTimeout, cannot use both
49+
if d.CreateContext != nil {
50+
resource.CreateContext = analyticsResource.CreateContext
51+
}
52+
if d.CreateWithoutTimeout != nil {
53+
resource.CreateWithoutTimeout = analyticsResource.CreateWithoutTimeout
54+
}
55+
// ReadContext or ReadWithoutTimeout, cannot use both
56+
if d.ReadContext != nil {
57+
resource.ReadContext = analyticsResource.ReadContext
58+
}
59+
if d.ReadWithoutTimeout != nil {
60+
resource.ReadWithoutTimeout = analyticsResource.ReadWithoutTimeout
61+
}
62+
// UpdateContext is not set on all resources
63+
if d.UpdateContext != nil {
64+
resource.UpdateContext = analyticsResource.UpdateContext
65+
}
66+
if d.UpdateWithoutTimeout != nil {
67+
resource.UpdateWithoutTimeout = analyticsResource.UpdateWithoutTimeout
68+
}
69+
// DeleteContext or DeleteWithoutTimeout, cannot use both
70+
if d.DeleteContext != nil {
71+
resource.DeleteContext = analyticsResource.DeleteContext
72+
}
73+
if d.DeleteWithoutTimeout != nil {
74+
resource.DeleteWithoutTimeout = analyticsResource.DeleteWithoutTimeout
75+
}
76+
return resource
77+
}
78+
79+
type ProviderMetaSDKv2 struct {
80+
UserAgentExtra map[string]string `cty:"user_agent_extra"`
81+
ModuleName *string `cty:"module_name"`
82+
ModuleVersion *string `cty:"module_version"`
83+
}
84+
85+
type AnalyticsResourceSDKv2 struct {
86+
resource *schema.Resource
87+
name string
88+
}
89+
90+
func (a *AnalyticsResourceSDKv2) CreateContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
91+
meta, err := parseProviderMeta(r)
92+
if err != nil {
93+
return a.resource.CreateContext(ctx, r, m)
94+
}
95+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueCreate)
96+
return a.resource.CreateContext(ctx, r, m)
97+
}
98+
99+
func (a *AnalyticsResourceSDKv2) CreateWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
100+
meta, err := parseProviderMeta(r)
101+
if err != nil {
102+
return a.resource.CreateWithoutTimeout(ctx, r, m)
103+
}
104+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueCreate)
105+
return a.resource.CreateWithoutTimeout(ctx, r, m)
106+
}
107+
108+
func (a *AnalyticsResourceSDKv2) ReadWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
109+
meta, err := parseProviderMeta(r)
110+
if err != nil {
111+
return a.resource.ReadWithoutTimeout(ctx, r, m)
112+
}
113+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueRead)
114+
return a.resource.ReadWithoutTimeout(ctx, r, m)
115+
}
116+
117+
func (a *AnalyticsResourceSDKv2) ReadContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
118+
meta, err := parseProviderMeta(r)
119+
if err != nil {
120+
return a.resource.ReadContext(ctx, r, m)
121+
}
122+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueRead)
123+
return a.resource.ReadContext(ctx, r, m)
124+
}
125+
126+
func (a *AnalyticsResourceSDKv2) UpdateContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
127+
meta, err := parseProviderMeta(r)
128+
if err != nil {
129+
return a.resource.UpdateContext(ctx, r, m)
130+
}
131+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueUpdate)
132+
return a.resource.UpdateContext(ctx, r, m)
133+
}
134+
func (a *AnalyticsResourceSDKv2) UpdateWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
135+
meta, err := parseProviderMeta(r)
136+
if err != nil {
137+
return a.resource.UpdateWithoutTimeout(ctx, r, m)
138+
}
139+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueUpdate)
140+
return a.resource.UpdateWithoutTimeout(ctx, r, m)
141+
}
142+
143+
func (a *AnalyticsResourceSDKv2) DeleteContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
144+
meta, err := parseProviderMeta(r)
145+
if err != nil {
146+
return a.resource.DeleteContext(ctx, r, m)
147+
}
148+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueDelete)
149+
return a.resource.DeleteContext(ctx, r, m)
150+
}
151+
152+
func (a *AnalyticsResourceSDKv2) DeleteWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
153+
meta, err := parseProviderMeta(r)
154+
if err != nil {
155+
return a.resource.DeleteWithoutTimeout(ctx, r, m)
156+
}
157+
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueDelete)
158+
return a.resource.DeleteWithoutTimeout(ctx, r, m)
159+
}
160+
161+
func (a *AnalyticsResourceSDKv2) resourceImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
162+
// Import doesn't have providerMeta
163+
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
164+
Name: a.name,
165+
Operation: UserAgentOperationValueImport,
166+
})
167+
return a.resource.Importer.StateContext(ctx, d, meta)
168+
}
169+
170+
func (a *AnalyticsResourceSDKv2) updateContextWithProviderMeta(ctx context.Context, meta ProviderMetaSDKv2, operationName string) context.Context {
171+
moduleName := ""
172+
if meta.ModuleName != nil {
173+
moduleName = *meta.ModuleName
174+
}
175+
moduleVersion := ""
176+
if meta.ModuleVersion != nil {
177+
moduleVersion = *meta.ModuleVersion
178+
}
179+
180+
uaExtra := UserAgentExtra{
181+
Name: userAgentNameValue(a.name),
182+
Operation: operationName,
183+
Extras: meta.UserAgentExtra,
184+
ModuleName: moduleName,
185+
ModuleVersion: moduleVersion,
186+
}
187+
ctx = AddUserAgentExtra(ctx, uaExtra)
188+
return ctx
189+
}
190+
191+
func parseProviderMeta(r *schema.ResourceData) (ProviderMetaSDKv2, error) {
192+
meta := ProviderMetaSDKv2{}
193+
err := r.GetProviderMeta(&meta)
194+
if err != nil {
195+
log.Printf("[WARN] failed to decode provider meta: %s, meta: %v", err, meta)
196+
}
197+
return meta, err
198+
}

0 commit comments

Comments
 (0)