From 7b2b73538c74306d3753e344a7dba9847995a6d0 Mon Sep 17 00:00:00 2001 From: Espen Albert Date: Mon, 10 Nov 2025 12:14:46 +0000 Subject: [PATCH] feat: Add provider_meta support to all data sources Adds provider_meta support to all data sources (TPF and SDKv2) for module usage tracking via User-Agent headers. Follows the same pattern as resources (PR #3618). - Created datasource_base.go for data source analytics in TPF - Added SetClient() method pattern matching resources - Shared asUserAgentExtraFromProviderMeta() between resources and data sources --- internal/config/datasource_base.go | 146 +++++++++++++++++++++++++ internal/config/resource_base.go | 78 +------------ internal/config/resource_base_sdkv2.go | 27 +++-- internal/config/user_agent.go | 3 + internal/provider/provider.go | 6 +- internal/provider/provider_sdk2.go | 8 +- 6 files changed, 185 insertions(+), 83 deletions(-) create mode 100644 internal/config/datasource_base.go diff --git a/internal/config/datasource_base.go b/internal/config/datasource_base.go new file mode 100644 index 0000000000..c30cc2b214 --- /dev/null +++ b/internal/config/datasource_base.go @@ -0,0 +1,146 @@ +package config + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type ImplementedDataSource interface { + datasource.DataSourceWithConfigure + GetName() string + SetClient(*MongoDBClient) +} + +func AnalyticsDataSourceFunc(iDataSource datasource.DataSource) func() datasource.DataSource { + commonDataSource, ok := iDataSource.(ImplementedDataSource) + if !ok { + panic(fmt.Sprintf("data source %T didn't comply with the ImplementedDataSource interface", iDataSource)) + } + return func() datasource.DataSource { + return analyticsDataSource(commonDataSource) + } +} + +// DSCommon is used as an embedded struct for all framework data sources. Implements the following plugin-framework defined functions: +// - Metadata +// - Configure +// Client is left empty and populated by the framework when envoking Configure method. +// DataSourceName must be defined when creating an instance of a data source. +// +// When used as a wrapper (ImplementedDataSource is set), it intercepts Read to add analytics tracking. +// When embedded in a data source struct, the data source's own Read method is used. +type DSCommon struct { + ImplementedDataSource // Set when used as a wrapper, nil when embedded + Client *MongoDBClient + DataSourceName string +} + +func (d *DSCommon) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, d.DataSourceName) +} + +func (d *DSCommon) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + if d.ImplementedDataSource != nil { + // When used as a wrapper, delegate to the wrapped data source + d.ImplementedDataSource.Schema(ctx, req, resp) + } + // When embedded, the data source's own Schema method is used +} + +func (d *DSCommon) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + client, err := configureClient(req.ProviderData) + if err != nil { + resp.Diagnostics.AddError(errorConfigureSummary, err.Error()) + return + } + d.Client = client + // If used as a wrapper, set the client on the wrapped data source + if d.ImplementedDataSource != nil { + d.ImplementedDataSource.SetClient(client) + } +} + +// Read intercepts the Read operation when DSCommon is used as a wrapper to add analytics tracking. +// When DSCommon is embedded, this method is not used (the data source's own Read method is called). +func (d *DSCommon) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + if d.ImplementedDataSource == nil { + // This shouldn't happen, but if DSCommon is embedded, the data source's Read is used instead + return + } + extra := asUserAgentExtraFromProviderMeta(ctx, d.DataSourceName, UserAgentOperationValueRead, true, req.ProviderMeta) + ctx = AddUserAgentExtra(ctx, extra) + d.ImplementedDataSource.Read(ctx, req, resp) +} + +func (d *DSCommon) GetName() string { + return d.DataSourceName +} + +func (d *DSCommon) SetClient(client *MongoDBClient) { + d.Client = client +} + +func configureClient(providerData any) (*MongoDBClient, error) { + if providerData == nil { + return nil, nil + } + + if client, ok := providerData.(*MongoDBClient); ok { + return client, nil + } + + return nil, fmt.Errorf(errorConfigure, providerData) +} + +// analyticsDataSource wraps an ImplementedDataSource with DSCommon to add analytics tracking. +// We cannot return iDataSource directly because we need to intercept the Read operation +// to inject provider_meta information into the context before calling the actual data source method. +func analyticsDataSource(iDataSource ImplementedDataSource) datasource.DataSource { + return &DSCommon{ + DataSourceName: iDataSource.GetName(), + ImplementedDataSource: iDataSource, + } +} + +// asUserAgentExtraFromProviderMeta extracts UserAgentExtra from provider_meta. +// This is a shared function used by both resources and data sources. +func asUserAgentExtraFromProviderMeta(ctx context.Context, name, reqOperation string, isDataSource bool, reqProviderMeta tfsdk.Config) UserAgentExtra { + var meta ProviderMeta + var nameValue string + if isDataSource { + nameValue = userAgentNameValueDataSource(name) + } else { + nameValue = userAgentNameValue(name) + } + uaExtra := UserAgentExtra{ + Name: nameValue, + Operation: reqOperation, + } + if reqProviderMeta.Raw.IsNull() { + return uaExtra + } + diags := reqProviderMeta.Get(ctx, &meta) + if diags.HasError() { + return uaExtra + } + + extrasLen := len(meta.UserAgentExtra.Elements()) + userExtras := make(map[string]types.String, extrasLen) + diags.Append(meta.UserAgentExtra.ElementsAs(ctx, &userExtras, false)...) + if diags.HasError() { + return uaExtra + } + userExtrasString := make(map[string]string, extrasLen) + for k, v := range userExtras { + userExtrasString[k] = v.ValueString() + } + return uaExtra.Combine(UserAgentExtra{ + Extras: userExtrasString, + ModuleName: meta.ModuleName.ValueString(), + ModuleVersion: meta.ModuleVersion.ValueString(), + }) +} diff --git a/internal/config/resource_base.go b/internal/config/resource_base.go index bf4ff6b797..9a8adadda4 100644 --- a/internal/config/resource_base.go +++ b/internal/config/resource_base.go @@ -4,9 +4,7 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -73,25 +71,25 @@ func (r *RSCommon) Configure(ctx context.Context, req resource.ConfigureRequest, } func (r *RSCommon) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - extra := r.asUserAgentExtra(ctx, UserAgentOperationValueCreate, req.ProviderMeta) + extra := asUserAgentExtraFromProviderMeta(ctx, r.ResourceName, UserAgentOperationValueCreate, false, req.ProviderMeta) ctx = AddUserAgentExtra(ctx, extra) r.ImplementedResource.Create(ctx, req, resp) } func (r *RSCommon) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - extra := r.asUserAgentExtra(ctx, UserAgentOperationValueRead, req.ProviderMeta) + extra := asUserAgentExtraFromProviderMeta(ctx, r.ResourceName, UserAgentOperationValueRead, false, req.ProviderMeta) ctx = AddUserAgentExtra(ctx, extra) r.ImplementedResource.Read(ctx, req, resp) } func (r *RSCommon) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - extra := r.asUserAgentExtra(ctx, UserAgentOperationValueUpdate, req.ProviderMeta) + extra := asUserAgentExtraFromProviderMeta(ctx, r.ResourceName, UserAgentOperationValueUpdate, false, req.ProviderMeta) ctx = AddUserAgentExtra(ctx, extra) r.ImplementedResource.Update(ctx, req, resp) } func (r *RSCommon) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - extra := r.asUserAgentExtra(ctx, UserAgentOperationValueDelete, req.ProviderMeta) + extra := asUserAgentExtraFromProviderMeta(ctx, r.ResourceName, UserAgentOperationValueDelete, false, req.ProviderMeta) ctx = AddUserAgentExtra(ctx, extra) r.ImplementedResource.Delete(ctx, req, resp) } @@ -111,7 +109,7 @@ func (r *RSCommon) ModifyPlan(ctx context.Context, req resource.ModifyPlanReques if !ok { return } - extra := r.asUserAgentExtra(ctx, UserAgentOperationValuePlanModify, req.ProviderMeta) + extra := asUserAgentExtraFromProviderMeta(ctx, r.ResourceName, UserAgentOperationValuePlanModify, false, req.ProviderMeta) ctx = AddUserAgentExtra(ctx, extra) resourceWithModifier.ModifyPlan(ctx, req, resp) } @@ -148,69 +146,3 @@ func (r *RSCommon) GetName() string { func (r *RSCommon) SetClient(client *MongoDBClient) { r.Client = client } - -func (r *RSCommon) asUserAgentExtra(ctx context.Context, reqOperation string, reqProviderMeta tfsdk.Config) UserAgentExtra { - var meta ProviderMeta - uaExtra := UserAgentExtra{ - Name: userAgentNameValue(r.ResourceName), - Operation: reqOperation, - } - if reqProviderMeta.Raw.IsNull() { - return uaExtra - } - diags := reqProviderMeta.Get(ctx, &meta) - if diags.HasError() { - return uaExtra - } - - extrasLen := len(meta.UserAgentExtra.Elements()) - userExtras := make(map[string]types.String, extrasLen) - diags.Append(meta.UserAgentExtra.ElementsAs(ctx, &userExtras, false)...) - if diags.HasError() { - return uaExtra - } - userExtrasString := make(map[string]string, extrasLen) - for k, v := range userExtras { - userExtrasString[k] = v.ValueString() - } - return uaExtra.Combine(UserAgentExtra{ - Extras: userExtrasString, - ModuleName: meta.ModuleName.ValueString(), - ModuleVersion: meta.ModuleVersion.ValueString(), - }) -} - -// DSCommon is used as an embedded struct for all framework data sources. Implements the following plugin-framework defined functions: -// - Metadata -// - Configure -// Client is left empty and populated by the framework when envoking Configure method. -// DataSourceName must be defined when creating an instance of a data source. -type DSCommon struct { - Client *MongoDBClient - DataSourceName string -} - -func (d *DSCommon) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, d.DataSourceName) -} - -func (d *DSCommon) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - client, err := configureClient(req.ProviderData) - if err != nil { - resp.Diagnostics.AddError(errorConfigureSummary, err.Error()) - return - } - d.Client = client -} - -func configureClient(providerData any) (*MongoDBClient, error) { - if providerData == nil { - return nil, nil - } - - if client, ok := providerData.(*MongoDBClient); ok { - return client, nil - } - - return nil, fmt.Errorf(errorConfigure, providerData) -} diff --git a/internal/config/resource_base_sdkv2.go b/internal/config/resource_base_sdkv2.go index aaba47e4fa..f59b79bd98 100644 --- a/internal/config/resource_base_sdkv2.go +++ b/internal/config/resource_base_sdkv2.go @@ -8,10 +8,11 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func NewAnalyticsResourceSDKv2(d *schema.Resource, name string) *schema.Resource { +func NewAnalyticsResourceSDKv2(d *schema.Resource, name string, isDataSource bool) *schema.Resource { analyticsResource := &AnalyticsResourceSDKv2{ - resource: d, - name: name, + resource: d, + name: name, + isDataSource: isDataSource, } /* We are not initializing deprecated fields, for example Update to avoid the message: @@ -83,8 +84,9 @@ type ProviderMetaSDKv2 struct { } type AnalyticsResourceSDKv2 struct { - resource *schema.Resource - name string + resource *schema.Resource + name string + isDataSource bool } func (a *AnalyticsResourceSDKv2) CreateContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics { @@ -167,7 +169,8 @@ func (a *AnalyticsResourceSDKv2) resourceImport(ctx context.Context, d *schema.R return a.resource.Importer.StateContext(ctx, d, meta) } -func (a *AnalyticsResourceSDKv2) updateContextWithProviderMeta(ctx context.Context, meta ProviderMetaSDKv2, operationName string) context.Context { +// updateContextWithProviderMetaSDKv2 is a shared function to update context with provider meta for SDKv2 resources and data sources. +func updateContextWithProviderMetaSDKv2(ctx context.Context, name string, isDataSource bool, meta ProviderMetaSDKv2, operationName string) context.Context { moduleName := "" if meta.ModuleName != nil { moduleName = *meta.ModuleName @@ -176,9 +179,15 @@ func (a *AnalyticsResourceSDKv2) updateContextWithProviderMeta(ctx context.Conte if meta.ModuleVersion != nil { moduleVersion = *meta.ModuleVersion } + var nameValue string + if isDataSource { + nameValue = userAgentNameValueDataSource(name) + } else { + nameValue = userAgentNameValue(name) + } uaExtra := UserAgentExtra{ - Name: userAgentNameValue(a.name), + Name: nameValue, Operation: operationName, Extras: meta.UserAgentExtra, ModuleName: moduleName, @@ -188,6 +197,10 @@ func (a *AnalyticsResourceSDKv2) updateContextWithProviderMeta(ctx context.Conte return ctx } +func (a *AnalyticsResourceSDKv2) updateContextWithProviderMeta(ctx context.Context, meta ProviderMetaSDKv2, operationName string) context.Context { + return updateContextWithProviderMetaSDKv2(ctx, a.name, a.isDataSource, meta, operationName) +} + func parseProviderMeta(r *schema.ResourceData) (ProviderMetaSDKv2, error) { meta := ProviderMetaSDKv2{} err := r.GetProviderMeta(&meta) diff --git a/internal/config/user_agent.go b/internal/config/user_agent.go index e4dd34a485..fcc21d3775 100644 --- a/internal/config/user_agent.go +++ b/internal/config/user_agent.go @@ -33,6 +33,9 @@ type UserAgentExtra struct { ModuleVersion string } +func userAgentNameValueDataSource(name string) string { + return "data." + userAgentNameValue(name) +} func userAgentNameValue(name string) string { return strings.TrimPrefix(name, "mongodbatlas_") } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5b7bc02ec3..b9cbf00a5d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -299,7 +299,11 @@ func (p *MongodbtlasProvider) DataSources(context.Context) []func() datasource.D advancedcluster.DataSource, advancedcluster.PluralDataSource, } - return dataSources + analyticsDataSources := []func() datasource.DataSource{} + for _, dataSourceFunc := range dataSources { + analyticsDataSources = append(analyticsDataSources, config.AnalyticsDataSourceFunc(dataSourceFunc())) + } + return analyticsDataSources } func (p *MongodbtlasProvider) Resources(context.Context) []func() resource.Resource { diff --git a/internal/provider/provider_sdk2.go b/internal/provider/provider_sdk2.go index 436e0dca4d..0ec409cfb3 100644 --- a/internal/provider/provider_sdk2.go +++ b/internal/provider/provider_sdk2.go @@ -243,7 +243,11 @@ func getDataSourcesMap() map[string]*schema.Resource { "mongodbatlas_shared_tier_snapshot": sharedtier.DataSourceSnapshot(), "mongodbatlas_shared_tier_snapshots": sharedtier.PluralDataSourceSnapshot(), } - return dataSourcesMap + analyticsMap := map[string]*schema.Resource{} + for name, dataSource := range dataSourcesMap { + analyticsMap[name] = config.NewAnalyticsResourceSDKv2(dataSource, name, true) + } + return analyticsMap } func getResourcesMap() map[string]*schema.Resource { @@ -292,7 +296,7 @@ func getResourcesMap() map[string]*schema.Resource { } analyticsMap := map[string]*schema.Resource{} for name, resource := range resourcesMap { - analyticsMap[name] = config.NewAnalyticsResourceSDKv2(resource, name) + analyticsMap[name] = config.NewAnalyticsResourceSDKv2(resource, name, false) } return analyticsMap }