Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor muxing 1 : Re-use same config to configure the SDK and PF providers, fix VCR testing #11903

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
adb7ffa
Add comments highlighting that parallel schemas are needed and must m…
SarahFrench Aug 29, 2024
8ceeaec
When creating the PF provider, store a pointer to the SDK provider st…
SarahFrench Oct 3, 2024
6d447a5
Update the plugin-framework provider to pass the SDK's `Config` meta …
SarahFrench Oct 3, 2024
5377e6d
Update all PF-implemented things to use the Config struct, update the…
SarahFrench Oct 3, 2024
767e709
Update `google_provider_config_plugin_framework` to use Config struct…
SarahFrench Oct 3, 2024
cddd298
Update `TestFrameworkProvider_impl` to use primary when creating PF p…
SarahFrench Oct 3, 2024
0d87c40
Fix how the plugin-framework is used in VCR tests
SarahFrench Oct 3, 2024
80674a9
Remove most instances of VCR skips referencing https://github.com/has…
SarahFrench Oct 3, 2024
5ade5f7
Update `project` acctest to reflect how unset string values are now "…
SarahFrench Oct 3, 2024
42baac2
Update `request_reason` acctest to reflect how unset string values ar…
SarahFrench Oct 3, 2024
13e4829
Update `add_terraform_attribution_label` + `terraform_attribution_lab…
SarahFrench Oct 3, 2024
8a5f263
Fix TestAccFrameworkProviderBasePath_setBasePath in VCR by removing u…
SarahFrench Oct 3, 2024
8d80772
Change how acctests check that fields are unset, post-muxing fixes
SarahFrench Oct 25, 2024
bc0e02f
Update test for PF handling of request_timeout to use the SDK default…
SarahFrench Oct 25, 2024
92feb8c
Update test for PF handling of request_timeout to expect normalistion…
SarahFrench Oct 25, 2024
d3f094a
Add tests for existing `NonNegativeDurationValidator` validator
SarahFrench Oct 25, 2024
aca5d33
Add validators to `request_timeout` field in the plugin-framework sch…
SarahFrench Oct 25, 2024
0f85c17
Fix check in `TestAccFwProvider_user_project_override` subtest
SarahFrench Oct 28, 2024
2ef48bb
Update comments to describe conversions between type systems in `goog…
SarahFrench Nov 7, 2024
6783a7a
go fmt
SarahFrench Nov 7, 2024
d81573a
Add comment about need to stop empty strings being passed from Config…
SarahFrench Nov 11, 2024
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
27 changes: 0 additions & 27 deletions mmv1/third_party/terraform/acctest/framework_test_utils.go.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion mmv1/third_party/terraform/acctest/provider_test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ var testAccProvider *schema.Provider

func init() {
configs = make(map[string]*transport_tpg.Config)
fwProviders = make(map[string]*frameworkTestProvider)
sources = make(map[string]VcrSource)
testAccProvider = provider.Provider()
TestAccProviders = map[string]*schema.Provider{
Expand Down
8 changes: 6 additions & 2 deletions mmv1/third_party/terraform/acctest/test_utils.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ func CheckDataSourceStateMatchesResourceStateWithIgnores(dataSourceName, resourc
func MuxedProviders(testName string) (func() tfprotov5.ProviderServer, error) {
ctx := context.Background()

// primary is the SDKv2 implementation of the provider
// If tests are run in VCR mode, the provider will use a cached config specific to the test name
primary := GetSDKProvider(testName)

providers := []func() tfprotov5.ProviderServer{
providerserver.NewProtocol5(NewFrameworkTestProvider(testName)), // framework provider
GetSDKProvider(testName).GRPCProvider, // sdk provider
primary.GRPCProvider, // sdk provider
providerserver.NewProtocol5(NewFrameworkTestProvider(testName, primary)), // framework provider
}

muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...)
Expand Down
94 changes: 18 additions & 76 deletions mmv1/third_party/terraform/acctest/vcr_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"testing"
"time"

"github.com/hashicorp/terraform-provider-google/google/fwmodels"
"github.com/hashicorp/terraform-provider-google/google/fwprovider"
tpgprovider "github.com/hashicorp/terraform-provider-google/google/provider"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
Expand All @@ -30,12 +29,9 @@ import (
"github.com/dnaeon/go-vcr/cassette"
"github.com/dnaeon/go-vcr/recorder"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"

fwDiags "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand All @@ -53,7 +49,6 @@ var configsLock = sync.RWMutex{}
var sourcesLock = sync.RWMutex{}

var configs map[string]*transport_tpg.Config
var fwProviders map[string]*frameworkTestProvider

var sources map[string]VcrSource

Expand Down Expand Up @@ -203,39 +198,6 @@ func closeRecorder(t *testing.T) {
delete(sources, t.Name())
sourcesLock.Unlock()
}

configsLock.RLock()
fwProvider, fwOk := fwProviders[t.Name()]
configsLock.RUnlock()
if fwOk {
// We did not cache the config if it does not use VCR
if !t.Failed() && IsVcrEnabled() {
// If a test succeeds, write new seed/yaml to files
err := fwProvider.Client.Transport.(*recorder.Recorder).Stop()
if err != nil {
t.Error(err)
}
envPath := os.Getenv("VCR_PATH")

sourcesLock.RLock()
vcrSource, ok := sources[t.Name()]
sourcesLock.RUnlock()
if ok {
err = writeSeedToFile(vcrSource.seed, vcrSeedFile(envPath, t.Name()))
if err != nil {
t.Error(err)
}
}
}
// Clean up test config
configsLock.Lock()
delete(fwProviders, t.Name())
configsLock.Unlock()

sourcesLock.Lock()
delete(sources, t.Name())
sourcesLock.Unlock()
}
}

func isReleaseDiffEnabled() bool {
Expand Down Expand Up @@ -319,6 +281,11 @@ func reformConfigWithProvider(config, provider string) string {
return string(resourceHeader.ReplaceAll(configBytes, providerReplacementBytes))
}

// HandleVCRConfiguration configures the recorder (github.com/dnaeon/go-vcr/recorder) used in the VCR test
// This includes:
// - Setting the recording/replaying mode
// - Determining the path to the file API interactions will be recorded to/read from
// - Determining the logic used to match requests against recorded HTTP interactions (see rec.SetMatcher)
func HandleVCRConfiguration(ctx context.Context, testName string, rndTripper http.RoundTripper, pollInterval time.Duration) (time.Duration, http.RoundTripper, fwDiags.Diagnostics) {
var diags fwDiags.Diagnostics
var vcrMode recorder.Mode
Expand Down Expand Up @@ -402,45 +369,32 @@ func NewVcrMatcherFunc(ctx context.Context) func(r *http.Request, i cassette.Req
// test versions of the provider that will call the same configure function, only append the VCR
// configuration to it.

func NewFrameworkTestProvider(testName string) *frameworkTestProvider {
func NewFrameworkTestProvider(testName string, primary *schema.Provider) *frameworkTestProvider {
return &frameworkTestProvider{
FrameworkProvider: fwprovider.FrameworkProvider{
Version: "test",
Primary: primary,
},
TestName: testName,
}
}

// frameworkTestProvider is a test version of the plugin-framework version of the provider
// that embeds FrameworkProvider whose configure function we can use
// the Configure function is overwritten in the framework_provider_test file
// frameworkTestProvider is a test version of the plugin-framework version of the provider.
// This facilitates overwriting the Configure function that's used during acceptance tests.
type frameworkTestProvider struct {
fwprovider.FrameworkProvider
TestName string
}

// Configure is here to overwrite the FrameworkProvider configure function for VCR testing
func (p *frameworkTestProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
p.FrameworkProvider.Configure(ctx, req, resp)
if IsVcrEnabled() {
if resp.Diagnostics.HasError() {
return
}

var diags fwDiags.Diagnostics
p.PollInterval, p.Client.Transport, diags = HandleVCRConfiguration(ctx, p.TestName, p.Client.Transport, p.PollInterval)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

configsLock.Lock()
fwProviders[p.TestName] = p
configsLock.Unlock()
return
} else {
tflog.Debug(ctx, "VCR_PATH or VCR_MODE not set, skipping VCR")
}
// When creating the frameworkTestProvider struct we took in a pointer to the the SDK provider.
// That SDK provider was configured using `GetSDKProvider` and `getCachedConfig`, so this framework provider will also
// use a cached client for the correct test name.
// No extra logic is required here, but in future when the SDK provider is removed this function will
// need to be updated with logic similar to that in `GetSDKProvider`.
p.FrameworkProvider.Configure(ctx, req, resp)
}

// DataSources overrides the provider's DataSources function so that we can append test-specific data sources to the list of data sources on the provider.
Expand All @@ -451,21 +405,9 @@ func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() dataso
return ds
}

func configureApiClient(ctx context.Context, p *fwprovider.FrameworkProvider, diags *fwDiags.Diagnostics) {
var data fwmodels.ProviderModel
var d fwDiags.Diagnostics

// Set defaults if needed - the only attribute without a default is ImpersonateServiceAccountDelegates
// this is a bit of a hack, but we'll just initialize it here so that it's been initialized at least
data.ImpersonateServiceAccountDelegates, d = types.ListValue(types.StringType, []attr.Value{})
diags.Append(d...)
if diags.HasError() {
return
}
p.LoadAndValidateFramework(ctx, &data, "test", diags, p.Version)
}

// GetSDKProvider gets the SDK provider with an overwritten configure function to be called by MuxedProviders
// GetSDKProvider gets the SDK provider for use in acceptance tests
// If VCR is in use, the configure function is overwritten.
// See usage in MuxedProviders
func GetSDKProvider(testName string) *schema.Provider {
prov := tpgprovider.Provider()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-google/google/fwmodels"
"github.com/hashicorp/terraform-provider-google/google/fwresource"
"github.com/hashicorp/terraform-provider-google/google/fwtransport"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
)

// Ensure the data source satisfies the expected interfaces.
Expand All @@ -25,15 +26,17 @@ func NewGoogleProviderConfigPluginFrameworkDataSource() datasource.DataSource {
}

type GoogleProviderConfigPluginFrameworkDataSource struct {
providerConfig *fwtransport.FrameworkProviderConfig
providerConfig *transport_tpg.Config
}

// GoogleProviderConfigPluginFrameworkModel describes the data source and matches the schema. Its fields match those in a Config struct (google/transport/config.go) but uses a different type system.
// - In the original Config struct old SDK/Go primitives types are used, e.g. `string`
// - Here in the GoogleProviderConfigPluginFrameworkModel struct we need to use the terraform-plugin-framework/types type system, e.g. `types.String`
// - This is needed because the PF type system is 'baked into' how we define schemas. The schema will expect a nullable type.
// - See terraform-plugin-framework/datasource/schema#StringAttribute's CustomType: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework@v1.7.0/datasource/schema#StringAttribute
// - Due to the different type systems of Config versus GoogleProviderConfigPluginFrameworkModel struct, we need to convert from primitive types to terraform-plugin-framework/types when we populate
// GoogleProviderConfigPluginFrameworkModel structs with data in this data source's Read method.
type GoogleProviderConfigPluginFrameworkModel struct {
// Currently this reflects the FrameworkProviderConfig struct and ProviderModel in google/fwmodels/provider_model.go
// which means it uses the plugin-framework type system where values can be explicitly Null or Unknown.
//
// As part of future muxing fixes/refactoring we'll change this struct to reflect structs used in the SDK code, and will move to
// using the SDK type system.
Credentials types.String `tfsdk:"credentials"`
AccessToken types.String `tfsdk:"access_token"`
ImpersonateServiceAccount types.String `tfsdk:"impersonate_service_account"`
Expand All @@ -53,12 +56,12 @@ type GoogleProviderConfigPluginFrameworkModel struct {
TerraformAttributionLabelAdditionStrategy types.String `tfsdk:"terraform_attribution_label_addition_strategy"`
}

func (m *GoogleProviderConfigPluginFrameworkModel) GetLocationDescription(providerConfig *fwtransport.FrameworkProviderConfig) fwresource.LocationDescription {
func (m *GoogleProviderConfigPluginFrameworkModel) GetLocationDescription(providerConfig *transport_tpg.Config) fwresource.LocationDescription {
return fwresource.LocationDescription{
RegionSchemaField: types.StringValue("region"),
ZoneSchemaField: types.StringValue("zone"),
ProviderRegion: providerConfig.Region,
ProviderZone: providerConfig.Zone,
ProviderRegion: types.StringValue(providerConfig.Region),
ProviderZone: types.StringValue(providerConfig.Zone),
}
}

Expand Down Expand Up @@ -172,11 +175,11 @@ func (d *GoogleProviderConfigPluginFrameworkDataSource) Configure(ctx context.Co
return
}

p, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig)
p, ok := req.ProviderData.(*transport_tpg.Config)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *fwtransport.FrameworkProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData),
fmt.Sprintf("Expected *transport_tpg.Config, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
Expand All @@ -202,23 +205,54 @@ func (d *GoogleProviderConfigPluginFrameworkDataSource) Read(ctx context.Context
}

// Copy all values from the provider config into this data source
// - The 'meta' from the provider configuration process uses Go primitive types (e.g. `string`) but this data source needs to use the plugin-framework type system due to being PF-implemented
// - As a result we have to make conversions between type systems in the value assignments below
data.Credentials = types.StringValue(d.providerConfig.Credentials)
data.AccessToken = types.StringValue(d.providerConfig.AccessToken)
data.ImpersonateServiceAccount = types.StringValue(d.providerConfig.ImpersonateServiceAccount)

data.Credentials = d.providerConfig.Credentials
data.AccessToken = d.providerConfig.AccessToken
data.ImpersonateServiceAccount = d.providerConfig.ImpersonateServiceAccount
data.ImpersonateServiceAccountDelegates = d.providerConfig.ImpersonateServiceAccountDelegates
data.Project = d.providerConfig.Project
data.Region = d.providerConfig.Region
data.BillingProject = d.providerConfig.BillingProject
data.Zone = d.providerConfig.Zone
data.UniverseDomain = d.providerConfig.UniverseDomain
data.Scopes = d.providerConfig.Scopes
data.UserProjectOverride = d.providerConfig.UserProjectOverride
data.RequestReason = d.providerConfig.RequestReason
data.RequestTimeout = d.providerConfig.RequestTimeout
data.DefaultLabels = d.providerConfig.DefaultLabels
data.AddTerraformAttributionLabel = d.providerConfig.AddTerraformAttributionLabel
data.TerraformAttributionLabelAdditionStrategy = d.providerConfig.TerraformAttributionLabelAdditionStrategy
delegateAttrs := make([]attr.Value, len(d.providerConfig.ImpersonateServiceAccountDelegates))
Copy link
Contributor

Choose a reason for hiding this comment

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

to double check, this extended logic is from the need to apply the string type assertions to every item within a list (as well as the list type on the list itself)?

seperately I think it would be good to have a comment somewhere around here summarizing the need for these assertions and the resulting mix of SDK behavior in this data source (as was mentioned here, albeit not user-facing

Copy link
Contributor Author

@SarahFrench SarahFrench Nov 7, 2024

Choose a reason for hiding this comment

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

to double check, this extended logic is from the need to apply the string type assertions to every item within a list (as well as the list type on the list itself)?

This code:

	delegateAttrs := make([]attr.Value, len(d.providerConfig.ImpersonateServiceAccountDelegates))
	for i, delegate := range d.providerConfig.ImpersonateServiceAccountDelegates {
		delegateAttrs[i] = types.StringValue(delegate)
	}
	delegates, di := types.ListValue(types.StringType, delegateAttrs)
	if di.HasError() {
		resp.Diagnostics.Append(di...)
	}
	data.ImpersonateServiceAccountDelegates = delegates

is all the code necessary to convert a []string value to a types.List value where the elements are types.StringType type.

I realise that in the past I had intended that the GoogleProviderConfigPluginFrameworkModel struct in the google_provider_config_plugin_framework data source would be updated to use the SDK type system, e.g GoogleProviderConfigPluginFrameworkModel's ImpersonateServiceAccountDelegates field would become a []string value and similar for all other fields. I tried that here (SarahFrench#10) and I think that that the issues I describe there are unavoidable and we cannot use the SDK type system in this way.

seperately I think it would be good to have a comment somewhere around here summarizing the need for these assertions and the resulting mix of SDK behavior in this data source (as was mentioned #11903 (comment), albeit not user-facing

I'll add that comment, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 2ef48bb

for i, delegate := range d.providerConfig.ImpersonateServiceAccountDelegates {
delegateAttrs[i] = types.StringValue(delegate)
}
delegates, di := types.ListValue(types.StringType, delegateAttrs)
if di.HasError() {
resp.Diagnostics.Append(di...)
}
data.ImpersonateServiceAccountDelegates = delegates

data.Project = types.StringValue(d.providerConfig.Project)
data.Region = types.StringValue(d.providerConfig.Region)
data.BillingProject = types.StringValue(d.providerConfig.BillingProject)
data.Zone = types.StringValue(d.providerConfig.Zone)
data.UniverseDomain = types.StringValue(d.providerConfig.UniverseDomain)

scopeAttrs := make([]attr.Value, len(d.providerConfig.Scopes))
for i, scope := range d.providerConfig.Scopes {
scopeAttrs[i] = types.StringValue(scope)
}
scopes, di := types.ListValue(types.StringType, scopeAttrs)
if di.HasError() {
resp.Diagnostics.Append(di...)
}
data.Scopes = scopes

data.UserProjectOverride = types.BoolValue(d.providerConfig.UserProjectOverride)
data.RequestReason = types.StringValue(d.providerConfig.RequestReason)
data.RequestTimeout = types.StringValue(d.providerConfig.RequestTimeout.String())

lbs := make(map[string]attr.Value, len(d.providerConfig.DefaultLabels))
for k, v := range d.providerConfig.DefaultLabels {
lbs[k] = types.StringValue(v)
}
labels, di := types.MapValueFrom(ctx, types.StringType, lbs)
if di.HasError() {
resp.Diagnostics.Append(di...)
}
data.DefaultLabels = labels

data.AddTerraformAttributionLabel = types.BoolValue(d.providerConfig.AddTerraformAttributionLabel)
data.TerraformAttributionLabelAdditionStrategy = types.StringValue(d.providerConfig.TerraformAttributionLabelAdditionStrategy)

// Warn users against using this data source
resp.Diagnostics.Append(diag.NewWarningDiagnostic(
Expand Down
Loading
Loading