Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d020917
feat: Adds initial provider_meta support and UserAgentTransport
EspenAlbert Aug 25, 2025
d9cfbe8
feat: Adds AnalyticsResource support to all resources
EspenAlbert Aug 25, 2025
674267c
feat: PoC SDKv2 and use provider to override
EspenAlbert Aug 26, 2025
73277ba
feat: Refactor Analytics resource initialization and add context hand…
EspenAlbert Aug 26, 2025
b4a6333
revert non config/provider-files
EspenAlbert Aug 26, 2025
3c04f7d
refactor: Small cleanups
EspenAlbert Aug 26, 2025
2216dbf
revert nolint
EspenAlbert Aug 26, 2025
9d22131
chore: Fully implement the sdkv2 behavior
EspenAlbert Aug 26, 2025
c78545c
refactor: Support PlanModifier
EspenAlbert Aug 26, 2025
aef3be7
feat: Add MoveState and UpgradeState methods to RSCommon; enhance use…
EspenAlbert Aug 27, 2025
b6a75e4
fix: Add comment to clarify embedding in ProviderMocked struct
EspenAlbert Aug 27, 2025
8518f30
fix: Handle null type in provider meta
EspenAlbert Aug 27, 2025
d79c43b
chore: Minor change and test fix
EspenAlbert Aug 27, 2025
716023e
Update internal/config/resource_base_test.go
EspenAlbert Aug 27, 2025
ac083fc
Merge branch 'master' into CLOUDP-340210_provider_meta_modules
EspenAlbert Sep 5, 2025
b39c792
Merge branch 'master' into CLOUDP-340210_provider_meta_modules
EspenAlbert Nov 3, 2025
62e9334
chore: Remove unnecessary blank line in getHTTPClient function
EspenAlbert Nov 3, 2025
29a4e0f
fix: Correct import path for advancedcluster in resource_base_test.go
EspenAlbert Nov 3, 2025
7b690f1
refactor: Simplify user agent name handling and remove unnecessary st…
EspenAlbert Nov 3, 2025
5861f3f
fix: Ensure compliance with ImplementedResource interface in Analytic…
EspenAlbert Nov 6, 2025
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
13 changes: 10 additions & 3 deletions internal/config/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,20 @@ type UAMetadata struct {
}

func (c *Config) NewClient(ctx context.Context) (any, error) {
// Network Logging transport is before Digest transport so it can log the first Digest requests with 401 Unauthorized.
// Terraform logging transport is after Digest transport so the Unauthorized request bodies are not logged.
// Transport chain (outermost to innermost):
// userAgentTransport -> tfLoggingTransport -> digestTransport -> networkLoggingTransport -> baseTransport
//
// This ordering ensures:
// 1. networkLoggingTransport logs ALL requests including digest auth 401 challenges
// 2. tfLoggingTransport only logs final authenticated requests (not sensitive auth details)
// 3. userAgentTransport modifies User-Agent before tfLoggingTransport logs it
networkLoggingTransport := NewTransportWithNetworkLogging(baseTransport, logging.IsDebugOrHigher())
digestTransport := digest.NewTransportWithHTTPRoundTripper(cast.ToString(c.PublicKey), cast.ToString(c.PrivateKey), networkLoggingTransport)
// Don't change logging.NewTransport to NewSubsystemLoggingHTTPTransport until all resources are in TPF.
tfLoggingTransport := logging.NewTransport("Atlas", digestTransport)
client := &http.Client{Transport: tfLoggingTransport}
// Add UserAgentExtra fields to the User-Agent header, see wrapper_provider_server.go
userAgentTransport := NewUserAgentTransport(tfLoggingTransport, true)
client := &http.Client{Transport: userAgentTransport}

optsAtlas := []matlasClient.ClientOpt{matlasClient.SetUserAgent(userAgent(c))}
if c.BaseURL != "" {
Expand Down
142 changes: 142 additions & 0 deletions internal/config/resource_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,53 @@ import (

"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"
)

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

type ProviderMeta struct {
ModuleName types.String `tfsdk:"module_name"`
ModuleVersion types.String `tfsdk:"module_version"`
UserAgentExtra types.Map `tfsdk:"user_agent_extra"`
}

type ImplementedResource interface {
resource.ResourceWithImportState
// Additional methods such as upgrade state & plan modifier are optional
SetClient(*MongoDBClient)
GetName() string
}

func AnalyticsResourceFunc(iResource resource.Resource) func() resource.Resource {
a := func() resource.Resource {
commonResource, ok := iResource.(ImplementedResource)
Copy link
Collaborator

Choose a reason for hiding this comment

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

  • Would there be an advantage in doing the cast outside of the returned "a" function?
  • Is there any resource that fails this cast? Should we enforce implementation of the interface instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, can choose to panic since all resources should comply to the resource.ResourceWithImportState interface.
This way we would detect any problem earlier. Adding in 5861f3f
TY!

if ok {
return analyticsResource(commonResource)
}
return iResource
}
return a
}

func analyticsResource(iResource ImplementedResource) resource.Resource {
return &RSCommon{
ResourceName: iResource.GetName(),
ImplementedResource: iResource,
}
}

// RSCommon is used as an embedded struct for all framework resources. Implements the following plugin-framework defined functions:
// - Metadata
// - Configure
// Client is left empty and populated by the framework when envoking Configure method.
// ResourceName must be defined when creating an instance of a resource.
type RSCommon struct {
ImplementedResource
Client *MongoDBClient
ResourceName string
}
Expand All @@ -33,9 +67,117 @@ func (r *RSCommon) Configure(ctx context.Context, req resource.ConfigureRequest,
resp.Diagnostics.AddError(errorConfigureSummary, err.Error())
return
}
r.ImplementedResource.SetClient(client)
}

func (r *RSCommon) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
extra := r.asUserAgentExtra(ctx, UserAgentOperationValueCreate, 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)
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)
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)
ctx = AddUserAgentExtra(ctx, extra)
r.ImplementedResource.Delete(ctx, req, resp)
}

// Optional interfaces for resource.Resource
func (r *RSCommon) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// req resource.ImportStateRequest doesn't have ProviderMeta
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
Name: r.ResourceName,
Operation: UserAgentOperationValueImport,
})
r.ImplementedResource.ImportState(ctx, req, resp)
}

func (r *RSCommon) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
resourceWithModifier, ok := r.ImplementedResource.(resource.ResourceWithModifyPlan)
if !ok {
return
}
extra := r.asUserAgentExtra(ctx, UserAgentOperationValuePlanModify, req.ProviderMeta)
ctx = AddUserAgentExtra(ctx, extra)
resourceWithModifier.ModifyPlan(ctx, req, resp)
}

func (r *RSCommon) MoveState(ctx context.Context) []resource.StateMover {
resourceWithMoveState, ok := r.ImplementedResource.(resource.ResourceWithMoveState)
if !ok {
return nil
}
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
Name: r.ResourceName,
Operation: UserAgentOperationValueMoveState,
})
return resourceWithMoveState.MoveState(ctx)
}

func (r *RSCommon) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
resourceWithUpgradeState, ok := r.ImplementedResource.(resource.ResourceWithUpgradeState)
if !ok {
return nil
}
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
Name: r.ResourceName,
Operation: UserAgentOperationValueUpgradeState,
})
return resourceWithUpgradeState.UpgradeState(ctx)
}

// Extra methods not found on resource.Resource
func (r *RSCommon) GetName() string {
return r.ResourceName
}

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: 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
Expand Down
198 changes: 198 additions & 0 deletions internal/config/resource_base_sdkv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package config

import (
"context"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func NewAnalyticsResourceSDKv2(d *schema.Resource, name string) *schema.Resource {
analyticsResource := &AnalyticsResourceSDKv2{
resource: d,
name: name,
}
/*
We are not initializing deprecated fields, for example Update to avoid the message:
resource mongodbatlas_cloud_backup_snapshot: All fields are ForceNew or Computed w/out Optional, Update is superfluous

Ensure no deprecated fields are used by running `staticcheck ./internal/service/... | grep -v 'd.GetOkExists'` and looking for (SA1019)
GetOkExists we are using in many places; therefore, we use -v (invert match) to filter out lines with different deprecations
Example line:
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)
*/
resource := &schema.Resource{
CustomizeDiff: d.CustomizeDiff,
DeprecationMessage: d.DeprecationMessage,
Description: d.Description,
EnableLegacyTypeSystemApplyErrors: d.EnableLegacyTypeSystemApplyErrors,
EnableLegacyTypeSystemPlanErrors: d.EnableLegacyTypeSystemPlanErrors,
Identity: d.Identity,
ResourceBehavior: d.ResourceBehavior,
Schema: d.Schema,
SchemaFunc: d.SchemaFunc,
SchemaVersion: d.SchemaVersion,
StateUpgraders: d.StateUpgraders,
Timeouts: d.Timeouts,
UpdateWithoutTimeout: d.UpdateWithoutTimeout,
UseJSONNumber: d.UseJSONNumber,
ValidateRawResourceConfigFuncs: d.ValidateRawResourceConfigFuncs,
}
importer := d.Importer
if importer != nil {
resource.Importer = &schema.ResourceImporter{
StateContext: analyticsResource.resourceImport,
}
}
// CreateContext or CreateWithoutTimeout, cannot use both
if d.CreateContext != nil {
resource.CreateContext = analyticsResource.CreateContext
}
if d.CreateWithoutTimeout != nil {
resource.CreateWithoutTimeout = analyticsResource.CreateWithoutTimeout
}
// ReadContext or ReadWithoutTimeout, cannot use both
if d.ReadContext != nil {
resource.ReadContext = analyticsResource.ReadContext
}
if d.ReadWithoutTimeout != nil {
resource.ReadWithoutTimeout = analyticsResource.ReadWithoutTimeout
}
// UpdateContext is not set on all resources
if d.UpdateContext != nil {
resource.UpdateContext = analyticsResource.UpdateContext
}
if d.UpdateWithoutTimeout != nil {
resource.UpdateWithoutTimeout = analyticsResource.UpdateWithoutTimeout
}
// DeleteContext or DeleteWithoutTimeout, cannot use both
if d.DeleteContext != nil {
resource.DeleteContext = analyticsResource.DeleteContext
}
if d.DeleteWithoutTimeout != nil {
resource.DeleteWithoutTimeout = analyticsResource.DeleteWithoutTimeout
}
return resource
}

type ProviderMetaSDKv2 struct {
UserAgentExtra map[string]string `cty:"user_agent_extra"`
ModuleName *string `cty:"module_name"`
ModuleVersion *string `cty:"module_version"`
}

type AnalyticsResourceSDKv2 struct {
resource *schema.Resource
name string
}

func (a *AnalyticsResourceSDKv2) CreateContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.CreateContext(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueCreate)
return a.resource.CreateContext(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) CreateWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.CreateWithoutTimeout(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueCreate)
return a.resource.CreateWithoutTimeout(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) ReadWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.ReadWithoutTimeout(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueRead)
return a.resource.ReadWithoutTimeout(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) ReadContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.ReadContext(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueRead)
return a.resource.ReadContext(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) UpdateContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.UpdateContext(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueUpdate)
return a.resource.UpdateContext(ctx, r, m)
}
func (a *AnalyticsResourceSDKv2) UpdateWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.UpdateWithoutTimeout(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueUpdate)
return a.resource.UpdateWithoutTimeout(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) DeleteContext(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.DeleteContext(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueDelete)
return a.resource.DeleteContext(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) DeleteWithoutTimeout(ctx context.Context, r *schema.ResourceData, m interface{}) diag.Diagnostics {
meta, err := parseProviderMeta(r)
if err != nil {
return a.resource.DeleteWithoutTimeout(ctx, r, m)
}
ctx = a.updateContextWithProviderMeta(ctx, meta, UserAgentOperationValueDelete)
return a.resource.DeleteWithoutTimeout(ctx, r, m)
}

func (a *AnalyticsResourceSDKv2) resourceImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
// Import doesn't have providerMeta
ctx = AddUserAgentExtra(ctx, UserAgentExtra{
Name: a.name,
Operation: UserAgentOperationValueImport,
})
return a.resource.Importer.StateContext(ctx, d, meta)
}

func (a *AnalyticsResourceSDKv2) updateContextWithProviderMeta(ctx context.Context, meta ProviderMetaSDKv2, operationName string) context.Context {
moduleName := ""
if meta.ModuleName != nil {
moduleName = *meta.ModuleName
}
moduleVersion := ""
if meta.ModuleVersion != nil {
moduleVersion = *meta.ModuleVersion
}

uaExtra := UserAgentExtra{
Name: a.name,
Operation: operationName,
Extras: meta.UserAgentExtra,
ModuleName: moduleName,
ModuleVersion: moduleVersion,
}
ctx = AddUserAgentExtra(ctx, uaExtra)
return ctx
}

func parseProviderMeta(r *schema.ResourceData) (ProviderMetaSDKv2, error) {
meta := ProviderMetaSDKv2{}
err := r.GetProviderMeta(&meta)
if err != nil {
log.Printf("[WARN] failed to decode provider meta: %s, meta: %v", err, meta)
}
return meta, err
}
Loading