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
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
Copy link
Member

Choose a reason for hiding this comment

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

NewClient is changing extensively in SA dev branch but that shouldn't be a problem

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