From a5dfcaa4496cac7864eeedbce363da39c3826c48 Mon Sep 17 00:00:00 2001 From: The Magician Date: Thu, 5 Sep 2024 04:08:09 -0700 Subject: [PATCH] Use acceptance tests to test handling of the credentials provider configuration argument, add new data source that surfaces configuration of SDK and plugin-framework providers to facilitate acctests (#11599) (#19376) [upstream:c4eadf3d3f8350f45c8b049212ca88645f18208b] Signed-off-by: Modular Magician --- .changelog/11599.txt | 2 + google/acctest/vcr_utils.go | 15 + ...source_provider_config_plugin_framework.go | 233 +++++++++++++++ .../framework_provider_credentials_test.go | 272 ++++++++++++++++++ google/fwtransport/framework_config.go | 11 + google/fwtransport/framework_config_test.go | 161 ----------- .../data_source_provider_config_sdk.go | 166 +++++++++++ google/provider/provider_credentials_test.go | 271 +++++++++++++++++ google/provider/provider_internal_test.go | 162 ----------- google/provider/provider_mmv1_resources.go | 1 - google/provider/provider_test.go | 69 ----- 11 files changed, 970 insertions(+), 393 deletions(-) create mode 100644 .changelog/11599.txt create mode 100644 google/fwprovider/data_source_provider_config_plugin_framework.go create mode 100644 google/fwprovider/framework_provider_credentials_test.go create mode 100644 google/provider/data_source_provider_config_sdk.go create mode 100644 google/provider/provider_credentials_test.go diff --git a/.changelog/11599.txt b/.changelog/11599.txt new file mode 100644 index 00000000000..126505bd3f5 --- /dev/null +++ b/.changelog/11599.txt @@ -0,0 +1,2 @@ +```release-note:none +``` \ No newline at end of file diff --git a/google/acctest/vcr_utils.go b/google/acctest/vcr_utils.go index 8abc7f41db7..889ae109200 100644 --- a/google/acctest/vcr_utils.go +++ b/google/acctest/vcr_utils.go @@ -33,6 +33,8 @@ import ( "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" @@ -438,6 +440,14 @@ func (p *frameworkTestProvider) Configure(ctx context.Context, req provider.Conf } } +// 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. +// This makes the data source(s) usable only in the context of acctests, and isn't available to users +func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + ds := p.FrameworkProvider.DataSources(ctx) + ds = append(ds, fwprovider.NewGoogleProviderConfigPluginFrameworkDataSource) // google_provider_config_plugin_framework + return ds +} + func configureApiClient(ctx context.Context, p *fwprovider.FrameworkProvider, diags *fwDiags.Diagnostics) { var data fwmodels.ProviderModel var d fwDiags.Diagnostics @@ -455,6 +465,11 @@ func configureApiClient(ctx context.Context, p *fwprovider.FrameworkProvider, di // GetSDKProvider gets the SDK provider with an overwritten configure function to be called by MuxedProviders func GetSDKProvider(testName string) *schema.Provider { prov := tpgprovider.Provider() + + // Append a test-specific data source to the list of data sources on the provider + // This makes the data source(s) usable only in the context of acctests, and isn't available to users + prov.DataSourcesMap["google_provider_config_sdk"] = tpgprovider.DataSourceGoogleProviderConfigSdk() + if IsVcrEnabled() { old := prov.ConfigureContextFunc prov.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { diff --git a/google/fwprovider/data_source_provider_config_plugin_framework.go b/google/fwprovider/data_source_provider_config_plugin_framework.go new file mode 100644 index 00000000000..f9c10513a91 --- /dev/null +++ b/google/fwprovider/data_source_provider_config_plugin_framework.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package fwprovider + +import ( + "context" + "fmt" + + "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" +) + +// Ensure the data source satisfies the expected interfaces. +var ( + _ datasource.DataSource = &GoogleProviderConfigPluginFrameworkDataSource{} + _ datasource.DataSourceWithConfigure = &GoogleProviderConfigPluginFrameworkDataSource{} + _ fwresource.LocationDescriber = &GoogleProviderConfigPluginFrameworkModel{} +) + +func NewGoogleProviderConfigPluginFrameworkDataSource() datasource.DataSource { + return &GoogleProviderConfigPluginFrameworkDataSource{} +} + +type GoogleProviderConfigPluginFrameworkDataSource struct { + providerConfig *fwtransport.FrameworkProviderConfig +} + +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"` + ImpersonateServiceAccountDelegates types.List `tfsdk:"impersonate_service_account_delegates"` + Project types.String `tfsdk:"project"` + BillingProject types.String `tfsdk:"billing_project"` + Region types.String `tfsdk:"region"` + Zone types.String `tfsdk:"zone"` + Scopes types.List `tfsdk:"scopes"` + // omit Batching + UserProjectOverride types.Bool `tfsdk:"user_project_override"` + RequestTimeout types.String `tfsdk:"request_timeout"` + RequestReason types.String `tfsdk:"request_reason"` + UniverseDomain types.String `tfsdk:"universe_domain"` + DefaultLabels types.Map `tfsdk:"default_labels"` + AddTerraformAttributionLabel types.Bool `tfsdk:"add_terraform_attribution_label"` + TerraformAttributionLabelAdditionStrategy types.String `tfsdk:"terraform_attribution_label_addition_strategy"` +} + +func (m *GoogleProviderConfigPluginFrameworkModel) GetLocationDescription(providerConfig *fwtransport.FrameworkProviderConfig) fwresource.LocationDescription { + return fwresource.LocationDescription{ + RegionSchemaField: types.StringValue("region"), + ZoneSchemaField: types.StringValue("zone"), + ProviderRegion: providerConfig.Region, + ProviderZone: providerConfig.Zone, + } +} + +func (d *GoogleProviderConfigPluginFrameworkDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_provider_config_plugin_framework" +} + +func (d *GoogleProviderConfigPluginFrameworkDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + + resp.Schema = schema.Schema{ + + Description: "Use this data source to access the configuration of the Google Cloud provider. This data source is implemented with the SDK.", + MarkdownDescription: "Use this data source to access the configuration of the Google Cloud provider. This data source is implemented with the SDK.", + Attributes: map[string]schema.Attribute{ + // Start of user inputs + "access_token": schema.StringAttribute{ + Description: "The access_token argument used to configure the provider", + MarkdownDescription: "The access_token argument used to configure the provider", + Computed: true, + Sensitive: true, + }, + "credentials": schema.StringAttribute{ + Description: "The credentials argument used to configure the provider", + MarkdownDescription: "The credentials argument used to configure the provider", + Computed: true, + Sensitive: true, + }, + "impersonate_service_account": schema.StringAttribute{ + Description: "The impersonate_service_account argument used to configure the provider", + MarkdownDescription: "The impersonate_service_account argument used to configure the provider.", + Computed: true, + }, + "impersonate_service_account_delegates": schema.ListAttribute{ + ElementType: types.StringType, + Description: "The impersonate_service_account_delegates argument used to configure the provider", + MarkdownDescription: "The impersonate_service_account_delegates argument used to configure the provider.", + Computed: true, + }, + "project": schema.StringAttribute{ + Description: "The project argument used to configure the provider", + MarkdownDescription: "The project argument used to configure the provider.", + Computed: true, + }, + "region": schema.StringAttribute{ + Description: "The region argument used to configure the provider.", + MarkdownDescription: "The region argument used to configure the provider.", + Computed: true, + }, + "billing_project": schema.StringAttribute{ + Description: "The billing_project argument used to configure the provider.", + MarkdownDescription: "The billing_project argument used to configure the provider.", + Computed: true, + }, + "zone": schema.StringAttribute{ + Description: "The zone argument used to configure the provider.", + MarkdownDescription: "The zone argument used to configure the provider.", + Computed: true, + }, + "universe_domain": schema.StringAttribute{ + Description: "The universe_domain argument used to configure the provider.", + MarkdownDescription: "The universe_domain argument used to configure the provider.", + Computed: true, + }, + "scopes": schema.ListAttribute{ + ElementType: types.StringType, + Description: "The scopes argument used to configure the provider.", + MarkdownDescription: "The scopes argument used to configure the provider.", + Computed: true, + }, + "user_project_override": schema.BoolAttribute{ + Description: "The user_project_override argument used to configure the provider.", + MarkdownDescription: "The user_project_override argument used to configure the provider.", + Computed: true, + }, + "request_reason": schema.StringAttribute{ + Description: "The request_reason argument used to configure the provider.", + MarkdownDescription: "The request_reason argument used to configure the provider.", + Computed: true, + }, + "request_timeout": schema.StringAttribute{ + Description: "The request_timeout argument used to configure the provider.", + MarkdownDescription: "The request_timeout argument used to configure the provider.", + Computed: true, + }, + "default_labels": schema.MapAttribute{ + ElementType: types.StringType, + Description: "The default_labels argument used to configure the provider.", + MarkdownDescription: "The default_labels argument used to configure the provider.", + Computed: true, + }, + "add_terraform_attribution_label": schema.BoolAttribute{ + Description: "The add_terraform_attribution_label argument used to configure the provider.", + MarkdownDescription: "The add_terraform_attribution_label argument used to configure the provider.", + Computed: true, + }, + "terraform_attribution_label_addition_strategy": schema.StringAttribute{ + Description: "The terraform_attribution_label_addition_strategy argument used to configure the provider.", + MarkdownDescription: "The terraform_attribution_label_addition_strategy argument used to configure the provider.", + Computed: true, + }, + // End of user inputs + + // Note - this data source excludes the default and custom endpoints for individual services + }, + } +} + +func (d *GoogleProviderConfigPluginFrameworkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + p, ok := req.ProviderData.(*fwtransport.FrameworkProviderConfig) + 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), + ) + return + } + + // Required for accessing project, region, zone and tokenSource + d.providerConfig = p +} + +func (d *GoogleProviderConfigPluginFrameworkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data GoogleProviderConfigPluginFrameworkModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Copy all values from the provider config into this data source + + data.Credentials = d.providerConfig.Credentials + // TODO(SarahFrench) - access_token + // TODO(SarahFrench) - impersonate_service_account + // TODO(SarahFrench) - impersonate_service_account_delegates + 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 + // TODO(SarahFrench) - request_reason + // TODO(SarahFrench) - request_timeout + data.DefaultLabels = d.providerConfig.DefaultLabels + // TODO(SarahFrench) - add_terraform_attribution_label + // TODO(SarahFrench) - terraform_attribution_label_addition_strategy + + // Warn users against using this data source + resp.Diagnostics.Append(diag.NewWarningDiagnostic( + "Data source google_provider_config_plugin_framework should not be used", + "Data source google_provider_config_plugin_framework is intended to be used only in acceptance tests for the provider. Instead, please use the google_client_config data source to access provider configuration details, or open a GitHub issue requesting new features in that datasource. Please go to: https://github.com/hashicorp/terraform-provider-google/issues/new/choose", + )) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/google/fwprovider/framework_provider_credentials_test.go b/google/fwprovider/framework_provider_credentials_test.go new file mode 100644 index 00000000000..1fa7641e761 --- /dev/null +++ b/google/fwprovider/framework_provider_credentials_test.go @@ -0,0 +1,272 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package fwprovider_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// TestAccFwProvider_credentials is a series of acc tests asserting how the plugin-framework provider handles credentials arguments +// It is PF specific because the HCL used uses a PF-implemented data source +// It is a counterpart to TestAccSdkProvider_credentials +func TestAccFwProvider_credentials(t *testing.T) { + testCases := map[string]func(t *testing.T){ + "credentials can be configured as a path to a credentials JSON file": testAccFwProvider_credentials_validJsonFilePath, + "configuring credentials as a path to a non-existent file results in an error": testAccFwProvider_credentials_badJsonFilepathCausesError, + "config takes precedence over environment variables": testAccFwProvider_credentials_configPrecedenceOverEnvironmentVariables, + "when credentials is unset in the config, environment variables are used in a given order": testAccFwProvider_credentials_precedenceOrderEnvironmentVariables, // GOOGLE_CREDENTIALS, GOOGLE_CLOUD_KEYFILE_JSON, GCLOUD_KEYFILE_JSON, GOOGLE_APPLICATION_CREDENTIALS + "when credentials is set to an empty string in the config the value isn't ignored and results in an error": testAccFwProvider_credentials_emptyStringValidation, + } + + for name, tc := range testCases { + // shadow the tc variable into scope so that when + // the loop continues, if t.Run hasn't executed tc(t) + // yet, we don't have a race condition + // see https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } +} + +func testAccFwProvider_credentials_validJsonFilePath(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + // unset all credentials env vars + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, "") + } + + credentials := transport_tpg.TestFakeCredentialsPath + + context := map[string]interface{}{ + "credentials": credentials, + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Credentials set as what we expect + Config: testAccFwProvider_credentialsInProviderBlock(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "credentials", credentials), + ), + }, + }, + }) +} + +func testAccFwProvider_credentials_badJsonFilepathCausesError(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + // unset all credentials env vars + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, "") + } + + pathToMissingFile := "./this/path/does/not/exist.json" // Doesn't exist + + context := map[string]interface{}{ + "credentials": pathToMissingFile, + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Apply-time error due to the file not existing + Config: testAccFwProvider_credentialsInProviderBlock(context), + PlanOnly: true, + ExpectError: regexp.MustCompile("JSON credentials are not valid"), + }, + }, + }) +} + +func testAccFwProvider_credentials_configPrecedenceOverEnvironmentVariables(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + credentials := envvar.GetTestCredsFromEnv() + + // ensure all possible credentials env vars set; show they aren't used + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, credentials) + } + + pathToMissingFile := "./this/path/does/not/exist.json" // Doesn't exist + + context := map[string]interface{}{ + "credentials": pathToMissingFile, + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Apply-time error; bad value in config is used over of good values in ENVs + Config: testAccFwProvider_credentialsInProviderBlock(context), + PlanOnly: true, + ExpectError: regexp.MustCompile("JSON credentials are not valid"), + }, + }, + }) +} + +func testAccFwProvider_credentials_precedenceOrderEnvironmentVariables(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + /* + These are all the ENVs for credentials, and they are in order of precedence. + GOOGLE_CREDENTIALS + GOOGLE_CLOUD_KEYFILE_JSON + GCLOUD_KEYFILE_JSON + GOOGLE_APPLICATION_CREDENTIALS + GOOGLE_USE_DEFAULT_CREDENTIALS + */ + + GOOGLE_CREDENTIALS := acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS") + GOOGLE_CLOUD_KEYFILE_JSON := acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON") + GCLOUD_KEYFILE_JSON := acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON") + GOOGLE_APPLICATION_CREDENTIALS := "./fake/file/path/nonexistent/a/credentials.json" // GOOGLE_APPLICATION_CREDENTIALS needs to be a path, not JSON + + context := map[string]interface{}{} + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // GOOGLE_CREDENTIALS is used 1st if set + PreConfig: func() { + t.Setenv("GOOGLE_CREDENTIALS", GOOGLE_CREDENTIALS) //used + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", GOOGLE_CLOUD_KEYFILE_JSON) + t.Setenv("GCLOUD_KEYFILE_JSON", GCLOUD_KEYFILE_JSON) + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) + }, + Config: testAccFwProvider_credentialsInEnvsOnly(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "credentials", GOOGLE_CREDENTIALS), + ), + }, + { + // GOOGLE_CLOUD_KEYFILE_JSON is used 2nd + PreConfig: func() { + // unset + t.Setenv("GOOGLE_CREDENTIALS", "") + // set + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", GOOGLE_CLOUD_KEYFILE_JSON) //used + t.Setenv("GCLOUD_KEYFILE_JSON", GCLOUD_KEYFILE_JSON) + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) + + }, + Config: testAccFwProvider_credentialsInEnvsOnly(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "credentials", GOOGLE_CLOUD_KEYFILE_JSON), + ), + }, + { + // GOOGLE_CLOUD_KEYFILE_JSON is used 3rd + PreConfig: func() { + // unset + t.Setenv("GOOGLE_CREDENTIALS", "") + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", "") + // set + t.Setenv("GCLOUD_KEYFILE_JSON", GCLOUD_KEYFILE_JSON) //used + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) + }, + Config: testAccFwProvider_credentialsInEnvsOnly(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "credentials", GCLOUD_KEYFILE_JSON), + ), + }, + { + // GOOGLE_APPLICATION_CREDENTIALS is used 4th + PreConfig: func() { + // unset + t.Setenv("GOOGLE_CREDENTIALS", "") + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", "") + t.Setenv("GCLOUD_KEYFILE_JSON", "") + // set + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) //used + }, + Config: testAccFwProvider_credentialsInEnvsOnly(context), + ExpectError: regexp.MustCompile("no such file or directory"), + }, + // Need a step to help post-test destroy run without error from GOOGLE_APPLICATION_CREDENTIALS + { + PreConfig: func() { + t.Setenv("GOOGLE_CREDENTIALS", GOOGLE_CREDENTIALS) + }, + Config: "// Need a step to help post-test destroy run without error", + }, + }, + }) +} + +func testAccFwProvider_credentials_emptyStringValidation(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + validValue := acctest.GenerateFakeCredentialsJson("usable-json-for-this-test") + + // ensure all credentials env vars set + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, validValue) + } + + context := map[string]interface{}{ + "credentials": "", // empty string used + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccFwProvider_credentialsInProviderBlock(context), + PlanOnly: true, + ExpectError: regexp.MustCompile("expected a non-empty string"), + }, + }, + }) +} + +// testAccFwProvider_credentialsInProviderBlock allows setting the credentials argument in a provider block. +// This function uses data.google_provider_config_plugin_framework because it is implemented with the plugin-framework, +// and it should be replaced with another plugin framework-implemented datasource or resource in future +func testAccFwProvider_credentialsInProviderBlock(context map[string]interface{}) string { + return acctest.Nprintf(` +provider "google" { + credentials = "%{credentials}" +} + +data "google_provider_config_plugin_framework" "default" {} + +output "credentials" { + value = data.google_provider_config_plugin_framework.default.credentials + sensitive = true +} +`, context) +} + +// testAccFwProvider_credentialsInEnvsOnly allows testing when the credentials argument +// is only supplied via ENVs +func testAccFwProvider_credentialsInEnvsOnly(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_provider_config_plugin_framework" "default" {} + +output "credentials" { + value = data.google_provider_config_plugin_framework.default.credentials + sensitive = true +} +`, context) +} diff --git a/google/fwtransport/framework_config.go b/google/fwtransport/framework_config.go index d0d4ea0a77f..b260b750871 100644 --- a/google/fwtransport/framework_config.go +++ b/google/fwtransport/framework_config.go @@ -34,6 +34,11 @@ import ( ) type FrameworkProviderConfig struct { + // Temporary, as we'll replace use of FrameworkProviderConfig with transport_tpg.Config soon + // transport_tpg.Config has a Credentials field, hence this change is needed + Credentials types.String + // End temporary + BillingProject types.String Client *http.Client Context context.Context @@ -332,6 +337,12 @@ func (p *FrameworkProviderConfig) LoadAndValidateFramework(ctx context.Context, p.WorkbenchBasePath = data.WorkbenchCustomEndpoint.ValueString() p.WorkflowsBasePath = data.WorkflowsCustomEndpoint.ValueString() + // Temporary + p.Credentials = data.Credentials + // End temporary + + // Copy values from the ProviderModel struct containing data about the provider configuration (present only when responsing to ConfigureProvider rpc calls) + // to the FrameworkProviderConfig struct that will be passed and available to all resources/data sources p.Context = ctx p.BillingProject = data.BillingProject p.DefaultLabels = data.DefaultLabels diff --git a/google/fwtransport/framework_config_test.go b/google/fwtransport/framework_config_test.go index c0ed0ff89c5..5afcf9b500c 100644 --- a/google/fwtransport/framework_config_test.go +++ b/google/fwtransport/framework_config_test.go @@ -178,167 +178,6 @@ func TestFrameworkProvider_LoadAndValidateFramework_project(t *testing.T) { } } -func TestFrameworkProvider_LoadAndValidateFramework_credentials(t *testing.T) { - - // Note: In the test function we need to set the below fields in test case's fwmodels.ProviderModel value - // this is to stop the code under test experiencing errors, and could be addressed in future refactoring. - // - ImpersonateServiceAccountDelegates: If we don't set this, we get a nil pointer exception ¯\_(ツ)_/¯ - - const pathToMissingFile string = "./this/path/doesnt/exist.json" // Doesn't exist - - cases := map[string]struct { - ConfigValues fwmodels.ProviderModel - EnvVariables map[string]string - ExpectedDataModelValue basetypes.StringValue - // ExpectedConfigStructValue not used here, as credentials info isn't stored in the config struct - ExpectError bool - }{ - "credentials can be configured as a path to a credentials JSON file": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringValue(transport_tpg.TestFakeCredentialsPath), - }, - ExpectedDataModelValue: types.StringValue(transport_tpg.TestFakeCredentialsPath), - }, - "configuring credentials as a path to a non-existent file results in an error": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringValue(pathToMissingFile), - }, - ExpectError: true, - }, - "credentials set in the config are not overridden by environment variables": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringValue(acctest.GenerateFakeCredentialsJson("test")), - }, - EnvVariables: map[string]string{ - "GOOGLE_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS"), - "GOOGLE_CLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedDataModelValue: types.StringValue(acctest.GenerateFakeCredentialsJson("test")), - }, - "when credentials is unset in the config, environment variables are used: GOOGLE_CREDENTIALS used first": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringNull(), // unset - }, - EnvVariables: map[string]string{ - "GOOGLE_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS"), - "GOOGLE_CLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedDataModelValue: types.StringValue(acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS")), - }, - "when credentials is unset in the config, environment variables are used: GOOGLE_CLOUD_KEYFILE_JSON used second": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringNull(), // unset - }, - EnvVariables: map[string]string{ - // GOOGLE_CREDENTIALS not set - "GOOGLE_CLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedDataModelValue: types.StringValue(acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON")), - }, - "when credentials is unset in the config, environment variables are used: GCLOUD_KEYFILE_JSON used third": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringNull(), // unset - }, - EnvVariables: map[string]string{ - // GOOGLE_CREDENTIALS not set - // GOOGLE_CLOUD_KEYFILE_JSON not set - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedDataModelValue: types.StringValue(acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON")), - }, - "when credentials is unset in the config (and access_token unset), GOOGLE_APPLICATION_CREDENTIALS is used for auth but not to set values in the config": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringNull(), // unset - }, - EnvVariables: map[string]string{ - // GOOGLE_CREDENTIALS not set - // GOOGLE_CLOUD_KEYFILE_JSON not set - // GCLOUD_KEYFILE_JSON not set - "GOOGLE_APPLICATION_CREDENTIALS": transport_tpg.TestFakeCredentialsPath, // needs to be a path to a file when used by code - }, - ExpectedDataModelValue: types.StringNull(), - }, - // Error states - "when credentials is set to an empty string in the config the value isn't ignored and results in an error": { - ConfigValues: fwmodels.ProviderModel{ - Credentials: types.StringValue(""), - }, - EnvVariables: map[string]string{ - "GOOGLE_APPLICATION_CREDENTIALS": transport_tpg.TestFakeCredentialsPath, // needs to be a path to a file when used by code - }, - ExpectError: true, - }, - // NOTE: these tests can't run in Cloud Build due to ADC locating credentials despite `GOOGLE_APPLICATION_CREDENTIALS` being unset - // See https://cloud.google.com/docs/authentication/application-default-credentials#search_order - // Also, when running these tests locally you need to run `gcloud auth application-default revoke` to ensure your machine isn't supplying ADCs - // "error returned if credentials is set as an empty string and GOOGLE_APPLICATION_CREDENTIALS is unset": { - // ConfigValues: fwmodels.ProviderModel{ - // Credentials: types.StringValue(""), - // }, - // EnvVariables: map[string]string{ - // "GOOGLE_APPLICATION_CREDENTIALS": "", - // }, - // ExpectError: true, - // }, - // "error returned if neither credentials nor access_token set in the provider config, and GOOGLE_APPLICATION_CREDENTIALS is unset": { - // EnvVariables: map[string]string{ - // "GOOGLE_APPLICATION_CREDENTIALS": "", - // }, - // ExpectError: true, - // }, - // Handling unknown values - see separate `TestFrameworkProvider_LoadAndValidateFramework_credentials_unknown` test - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - - // Arrange - acctest.UnsetTestProviderConfigEnvs(t) - acctest.SetupTestEnvs(t, tc.EnvVariables) - - ctx := context.Background() - tfVersion := "foobar" - providerversion := "999" - diags := diag.Diagnostics{} - - data := tc.ConfigValues - impersonateServiceAccountDelegates, _ := types.ListValue(types.StringType, []attr.Value{}) // empty list - data.ImpersonateServiceAccountDelegates = impersonateServiceAccountDelegates - - p := fwtransport.FrameworkProviderConfig{} - - // Act - p.LoadAndValidateFramework(ctx, &data, tfVersion, &diags, providerversion) - - // Assert - if diags.HasError() && tc.ExpectError { - return - } - if diags.HasError() && !tc.ExpectError { - for i, err := range diags.Errors() { - num := i + 1 - t.Logf("unexpected error #%d : %s : %s", num, err.Summary(), err.Detail()) - } - t.Fatalf("did not expect error, but [%d] error(s) occurred", diags.ErrorsCount()) - } - if tc.ExpectError && !diags.HasError() { - t.Fatalf("expected error, but no errors occurred") - } - if !data.Credentials.Equal(tc.ExpectedDataModelValue) { - t.Fatalf("want credentials to be `%s`, but got the value `%s`", tc.ExpectedDataModelValue, data.Credentials.String()) - } - // fwtransport.FrameworkProviderConfig does not store the credentials info, so test does not make assertions on config struct - }) - } -} - // NOTE: these tests can't run in Cloud Build due to ADC locating credentials despite `GOOGLE_APPLICATION_CREDENTIALS` being unset // See https://cloud.google.com/docs/authentication/application-default-credentials#search_order // Also, when running these tests locally you need to run `gcloud auth application-default revoke` to ensure your machine isn't supplying ADCs diff --git a/google/provider/data_source_provider_config_sdk.go b/google/provider/data_source_provider_config_sdk.go new file mode 100644 index 00000000000..e97b084c858 --- /dev/null +++ b/google/provider/data_source_provider_config_sdk.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package provider + +import ( + "crypto/sha1" + "encoding/base64" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceGoogleProviderConfigSdk() *schema.Resource { + return &schema.Resource{ + DeprecationMessage: "Data source google_provider_config_sdk is intended to be used only in acceptance tests for the provider. Instead, please use the google_client_config data source to access provider configuration details, or open a GitHub issue requesting new features in that datasource. Please go to: https://github.com/hashicorp/terraform-provider-google/issues/new/choose", + Read: dataSourceClientConfigRead, + Schema: map[string]*schema.Schema{ + // Start of user inputs + "access_token": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "credentials": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "impersonate_service_account": { + Type: schema.TypeString, + Computed: true, + }, + "impersonate_service_account_delegates": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "project": { + Type: schema.TypeString, + Computed: true, + }, + "region": { + Type: schema.TypeString, + Computed: true, + }, + "billing_project": { + Type: schema.TypeString, + Computed: true, + }, + "zone": { + Type: schema.TypeString, + Computed: true, + }, + "universe_domain": { + Type: schema.TypeString, + Computed: true, + }, + "scopes": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "user_project_override": { + Type: schema.TypeBool, + Computed: true, + }, + "request_reason": { + Type: schema.TypeString, + Computed: true, + }, + "request_timeout": { + Type: schema.TypeString, + Computed: true, + }, + "default_labels": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "add_terraform_attribution_label": { + Type: schema.TypeBool, + Computed: true, + }, + "terraform_attribution_label_addition_strategy": { + Type: schema.TypeString, + Computed: true, + }, + // End of user inputs + + // Note - this data source excludes the default and custom endpoints for individual services + + // Start of values set during provider configuration + "user_agent": { + Type: schema.TypeString, + Computed: true, + }, + // End of values set during provider configuration + }, + } +} + +func dataSourceClientConfigRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + + if err := d.Set("access_token", config.AccessToken); err != nil { + return fmt.Errorf("error setting access_token: %s", err) + } + if err := d.Set("credentials", config.Credentials); err != nil { + return fmt.Errorf("error setting credentials: %s", err) + } + if err := d.Set("impersonate_service_account", config.ImpersonateServiceAccount); err != nil { + return fmt.Errorf("error setting impersonate_service_account: %s", err) + } + if err := d.Set("impersonate_service_account_delegates", config.ImpersonateServiceAccountDelegates); err != nil { + return fmt.Errorf("error setting impersonate_service_account_delegates: %s", err) + } + if err := d.Set("project", config.Project); err != nil { + return fmt.Errorf("error setting project: %s", err) + } + if err := d.Set("region", config.Region); err != nil { + return fmt.Errorf("error setting region: %s", err) + } + if err := d.Set("billing_project", config.BillingProject); err != nil { + return fmt.Errorf("error setting billing_project: %s", err) + } + if err := d.Set("zone", config.Zone); err != nil { + return fmt.Errorf("error setting zone: %s", err) + } + if err := d.Set("universe_domain", config.UniverseDomain); err != nil { + return fmt.Errorf("error setting universe_domain: %s", err) + } + if err := d.Set("scopes", config.Scopes); err != nil { + return fmt.Errorf("error setting scopes: %s", err) + } + if err := d.Set("user_project_override", config.UserProjectOverride); err != nil { + return fmt.Errorf("error setting user_project_override: %s", err) + } + if err := d.Set("request_reason", config.RequestReason); err != nil { + return fmt.Errorf("error setting request_reason: %s", err) + } + if err := d.Set("request_timeout", config.RequestTimeout.String()); err != nil { + return fmt.Errorf("error setting request_timeout: %s", err) + } + if err := d.Set("default_labels", config.DefaultLabels); err != nil { + return fmt.Errorf("error setting default_labels: %s", err) + } + if err := d.Set("add_terraform_attribution_label", config.AddTerraformAttributionLabel); err != nil { + return fmt.Errorf("error setting add_terraform_attribution_label: %s", err) + } + if err := d.Set("terraform_attribution_label_addition_strategy", config.TerraformAttributionLabelAdditionStrategy); err != nil { + return fmt.Errorf("error setting terraform_attribution_label_addition_strategy: %s", err) + } + if err := d.Set("user_agent", config.UserAgent); err != nil { + return fmt.Errorf("error setting user_agent: %s", err) + } + + // Id is a hash of the total transport.Config struct + configString := []byte(fmt.Sprintf("%#v", config)) + hasher := sha1.New() + hasher.Write(configString) + sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + d.SetId(string(sha)) + + return nil +} diff --git a/google/provider/provider_credentials_test.go b/google/provider/provider_credentials_test.go new file mode 100644 index 00000000000..58704f3bdb5 --- /dev/null +++ b/google/provider/provider_credentials_test.go @@ -0,0 +1,271 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package provider_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// TestAccSdkProvider_credentials is a series of acc tests asserting how the SDK provider handles credentials arguments +// It is SDK specific because the HCL used provisions SDK-implemented resources +// It is a counterpart to TestAccFwProvider_credentials +func TestAccSdkProvider_credentials(t *testing.T) { + testCases := map[string]func(t *testing.T){ + "credentials can be configured as a path to a credentials JSON file": testAccSdkProvider_credentials_validJsonFilePath, + "configuring credentials as a path to a non-existent file results in an error": testAccSdkProvider_credentials_badJsonFilepathCausesError, + "config takes precedence over environment variables": testAccSdkProvider_credentials_configPrecedenceOverEnvironmentVariables, + "when credentials is unset in the config, environment variables are used in a given order": testAccSdkProvider_credentials_precedenceOrderEnvironmentVariables, // GOOGLE_CREDENTIALS, GOOGLE_CLOUD_KEYFILE_JSON, GCLOUD_KEYFILE_JSON, GOOGLE_APPLICATION_CREDENTIALS + "when credentials is set to an empty string in the config the value isn't ignored and results in an error": testAccSdkProvider_credentials_emptyStringValidation, + } + + for name, tc := range testCases { + // shadow the tc variable into scope so that when + // the loop continues, if t.Run hasn't executed tc(t) + // yet, we don't have a race condition + // see https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } +} + +func testAccSdkProvider_credentials_validJsonFilePath(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + // unset all credentials env vars + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, "") + } + + credentials := transport_tpg.TestFakeCredentialsPath + + context := map[string]interface{}{ + "credentials": credentials, + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Credentials set as what we expect + Config: testAccSdkProvider_credentialsInProviderBlock(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "credentials", credentials), + ), + }, + }, + }) +} + +func testAccSdkProvider_credentials_badJsonFilepathCausesError(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + // unset all credentials env vars + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, "") + } + + pathToMissingFile := "./this/path/does/not/exist.json" // Doesn't exist + + context := map[string]interface{}{ + "credentials": pathToMissingFile, + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Apply-time error due to the file not existing + Config: testAccSdkProvider_credentialsInProviderBlock(context), + PlanOnly: true, + ExpectError: regexp.MustCompile("JSON credentials are not valid"), + }, + }, + }) +} + +func testAccSdkProvider_credentials_configPrecedenceOverEnvironmentVariables(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + credentials := envvar.GetTestCredsFromEnv() + + // ensure all possible credentials env vars set; show they aren't used + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, credentials) + } + + pathToMissingFile := "./this/path/does/not/exist.json" // Doesn't exist + + context := map[string]interface{}{ + "credentials": pathToMissingFile, + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // Apply-time error; bad value in config is used over of good values in ENVs + Config: testAccSdkProvider_credentialsInProviderBlock(context), + PlanOnly: true, + ExpectError: regexp.MustCompile("JSON credentials are not valid"), + }, + }, + }) +} + +func testAccSdkProvider_credentials_precedenceOrderEnvironmentVariables(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + /* + These are all the ENVs for credentials, and they are in order of precedence. + GOOGLE_CREDENTIALS + GOOGLE_CLOUD_KEYFILE_JSON + GCLOUD_KEYFILE_JSON + GOOGLE_APPLICATION_CREDENTIALS + GOOGLE_USE_DEFAULT_CREDENTIALS + */ + + GOOGLE_CREDENTIALS := acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS") + GOOGLE_CLOUD_KEYFILE_JSON := acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON") + GCLOUD_KEYFILE_JSON := acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON") + GOOGLE_APPLICATION_CREDENTIALS := "./fake/file/path/nonexistent/a/credentials.json" // GOOGLE_APPLICATION_CREDENTIALS needs to be a path, not JSON + + context := map[string]interface{}{} + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + // GOOGLE_CREDENTIALS is used 1st if set + PreConfig: func() { + t.Setenv("GOOGLE_CREDENTIALS", GOOGLE_CREDENTIALS) //used + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", GOOGLE_CLOUD_KEYFILE_JSON) + t.Setenv("GCLOUD_KEYFILE_JSON", GCLOUD_KEYFILE_JSON) + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) + }, + Config: testAccSdkProvider_credentialsInEnvsOnly(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "credentials", GOOGLE_CREDENTIALS), + ), + }, + { + // GOOGLE_CLOUD_KEYFILE_JSON is used 2nd + PreConfig: func() { + // unset + t.Setenv("GOOGLE_CREDENTIALS", "") + // set + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", GOOGLE_CLOUD_KEYFILE_JSON) //used + t.Setenv("GCLOUD_KEYFILE_JSON", GCLOUD_KEYFILE_JSON) + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) + + }, + Config: testAccSdkProvider_credentialsInEnvsOnly(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "credentials", GOOGLE_CLOUD_KEYFILE_JSON), + ), + }, + { + // GOOGLE_CLOUD_KEYFILE_JSON is used 3rd + PreConfig: func() { + // unset + t.Setenv("GOOGLE_CREDENTIALS", "") + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", "") + // set + t.Setenv("GCLOUD_KEYFILE_JSON", GCLOUD_KEYFILE_JSON) //used + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) + }, + Config: testAccSdkProvider_credentialsInEnvsOnly(context), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "credentials", GCLOUD_KEYFILE_JSON), + ), + }, + { + // GOOGLE_APPLICATION_CREDENTIALS is used 4th + PreConfig: func() { + // unset + t.Setenv("GOOGLE_CREDENTIALS", "") + t.Setenv("GOOGLE_CLOUD_KEYFILE_JSON", "") + t.Setenv("GCLOUD_KEYFILE_JSON", "") + // set + t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", GOOGLE_APPLICATION_CREDENTIALS) //used + }, + Config: testAccSdkProvider_credentialsInEnvsOnly(context), + ExpectError: regexp.MustCompile("no such file or directory"), + }, + // Need a step to help post-test destroy run without error from GOOGLE_APPLICATION_CREDENTIALS + { + PreConfig: func() { + t.Setenv("GOOGLE_CREDENTIALS", GOOGLE_CREDENTIALS) + }, + Config: "// Need a step to help post-test destroy run without error", + }, + }, + }) +} + +func testAccSdkProvider_credentials_emptyStringValidation(t *testing.T) { + acctest.SkipIfVcr(t) // Test doesn't interact with API + + credentials := envvar.GetTestCredsFromEnv() + + // ensure all credentials env vars set + for _, v := range envvar.CredsEnvVars { + t.Setenv(v, credentials) + } + + context := map[string]interface{}{ + "credentials": "", // empty string used + } + + acctest.VcrTest(t, resource.TestCase{ + // No PreCheck for checking ENVs + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccSdkProvider_credentialsInProviderBlock(context), + PlanOnly: true, + ExpectError: regexp.MustCompile("expected a non-empty string"), + }, + }, + }) +} + +// testAccSdkProvider_credentialsInProviderBlock allows setting the credentials argument in a provider block. +// This function uses data.google_provider_config_sdk because it is implemented with the SDKv2 +func testAccSdkProvider_credentialsInProviderBlock(context map[string]interface{}) string { + return acctest.Nprintf(` +provider "google" { + credentials = "%{credentials}" +} + +data "google_provider_config_sdk" "default" {} + +output "credentials" { + value = data.google_provider_config_sdk.default.credentials + sensitive = true +} +`, context) +} + +// testAccSdkProvider_credentialsInEnvsOnly allows testing when the credentials argument +// is only supplied via ENVs +func testAccSdkProvider_credentialsInEnvsOnly(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_provider_config_sdk" "default" {} + +output "credentials" { + value = data.google_provider_config_sdk.default.credentials + sensitive = true +} +`, context) +} diff --git a/google/provider/provider_internal_test.go b/google/provider/provider_internal_test.go index 3dde62846fc..dbf148432e6 100644 --- a/google/provider/provider_internal_test.go +++ b/google/provider/provider_internal_test.go @@ -137,168 +137,6 @@ func TestProvider_ValidateEmptyStrings(t *testing.T) { } } -func TestProvider_ProviderConfigure_credentials(t *testing.T) { - - const pathToMissingFile string = "./this/path/doesnt/exist.json" // Doesn't exist - - cases := map[string]struct { - ConfigValues map[string]interface{} - EnvVariables map[string]string - ExpectError bool - ExpectFieldUnset bool - ExpectedSchemaValue string - ExpectedConfigValue string - }{ - "credentials can be configured as a path to a credentials JSON file": { - ConfigValues: map[string]interface{}{ - "credentials": transport_tpg.TestFakeCredentialsPath, - }, - EnvVariables: map[string]string{}, - ExpectedSchemaValue: transport_tpg.TestFakeCredentialsPath, - ExpectedConfigValue: transport_tpg.TestFakeCredentialsPath, - }, - "configuring credentials as a path to a non-existant file results in an error": { - ConfigValues: map[string]interface{}{ - "credentials": pathToMissingFile, - }, - ExpectError: true, - ExpectedSchemaValue: pathToMissingFile, - ExpectedConfigValue: pathToMissingFile, - }, - "credentials set in the config are not overridden by environment variables": { - ConfigValues: map[string]interface{}{ - "credentials": acctest.GenerateFakeCredentialsJson("test"), - }, - EnvVariables: map[string]string{ - "GOOGLE_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS"), - "GOOGLE_CLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedSchemaValue: acctest.GenerateFakeCredentialsJson("test"), - ExpectedConfigValue: acctest.GenerateFakeCredentialsJson("test"), - }, - "when credentials is unset in the config, environment variables are used: GOOGLE_CREDENTIALS used first": { - EnvVariables: map[string]string{ - "GOOGLE_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS"), - "GOOGLE_CLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedSchemaValue: "", - ExpectedConfigValue: acctest.GenerateFakeCredentialsJson("GOOGLE_CREDENTIALS"), - }, - "when credentials is unset in the config, environment variables are used: GOOGLE_CLOUD_KEYFILE_JSON used second": { - EnvVariables: map[string]string{ - // GOOGLE_CREDENTIALS not set - "GOOGLE_CLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedSchemaValue: "", - ExpectedConfigValue: acctest.GenerateFakeCredentialsJson("GOOGLE_CLOUD_KEYFILE_JSON"), - }, - "when credentials is unset in the config, environment variables are used: GCLOUD_KEYFILE_JSON used third": { - EnvVariables: map[string]string{ - // GOOGLE_CREDENTIALS not set - // GOOGLE_CLOUD_KEYFILE_JSON not set - "GCLOUD_KEYFILE_JSON": acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - "GOOGLE_APPLICATION_CREDENTIALS": acctest.GenerateFakeCredentialsJson("GOOGLE_APPLICATION_CREDENTIALS"), - }, - ExpectedSchemaValue: "", - ExpectedConfigValue: acctest.GenerateFakeCredentialsJson("GCLOUD_KEYFILE_JSON"), - }, - "when credentials is unset in the config (and access_token unset), GOOGLE_APPLICATION_CREDENTIALS is used for auth but not to set values in the config": { - EnvVariables: map[string]string{ - "GOOGLE_APPLICATION_CREDENTIALS": transport_tpg.TestFakeCredentialsPath, // needs to be a path to a file when used - }, - ExpectFieldUnset: true, - ExpectedSchemaValue: "", - }, - // Handling empty strings in config - "when credentials is set to an empty string in the config (and access_token unset), GOOGLE_APPLICATION_CREDENTIALS is used": { - ConfigValues: map[string]interface{}{ - "credentials": "", - }, - EnvVariables: map[string]string{ - "GOOGLE_APPLICATION_CREDENTIALS": transport_tpg.TestFakeCredentialsPath, // needs to be a path to a file when used - }, - ExpectFieldUnset: true, - ExpectedSchemaValue: "", - }, - // Error states - // NOTE: these tests can't run in Cloud Build due to ADC locating credentials despite `GOOGLE_APPLICATION_CREDENTIALS` being unset - // See https://cloud.google.com/docs/authentication/application-default-credentials#search_order - // Also, when running these tests locally you need to run `gcloud auth application-default revoke` to ensure your machine isn't supplying ADCs - // "error returned if credentials is set as an empty string and GOOGLE_APPLICATION_CREDENTIALS is unset": { - // ConfigValues: map[string]interface{}{ - // "credentials": "", - // }, - // EnvVariables: map[string]string{ - // "GOOGLE_APPLICATION_CREDENTIALS": "", - // }, - // ExpectError: true, - // }, - // "error returned if neither credentials nor access_token set in the provider config, and GOOGLE_APPLICATION_CREDENTIALS is unset": { - // EnvVariables: map[string]string{ - // "GOOGLE_APPLICATION_CREDENTIALS": "", - // }, - // ExpectError: true, - // }, - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - - // Arrange - ctx := context.Background() - acctest.UnsetTestProviderConfigEnvs(t) - acctest.SetupTestEnvs(t, tc.EnvVariables) - p := provider.Provider() - d := tpgresource.SetupTestResourceDataFromConfigMap(t, p.Schema, tc.ConfigValues) - - // Act - c, diags := provider.ProviderConfigure(ctx, d, p) - - // Assert - if diags.HasError() && !tc.ExpectError { - t.Fatalf("unexpected error(s): %#v", diags) - } - if !diags.HasError() && tc.ExpectError { - t.Fatal("expected error(s) but got none") - } - if diags.HasError() && tc.ExpectError { - v, ok := d.GetOk("credentials") - if ok { - val := v.(string) - if val != tc.ExpectedSchemaValue { - t.Fatalf("expected credentials value set in provider config data to be %s, got %s", tc.ExpectedSchemaValue, val) - } - if tc.ExpectFieldUnset { - t.Fatalf("expected credentials value to not be set in provider config data, got %s", val) - } - } - // Return early in tests where errors expected - return - } - - config := c.(*transport_tpg.Config) // Should be non-nil value, as test cases reaching this point experienced no errors - - v, ok := d.GetOk("credentials") - val := v.(string) - if ok && tc.ExpectFieldUnset { - t.Fatal("expected credentials value to be unset in provider config data") - } - if v != tc.ExpectedSchemaValue { - t.Fatalf("expected credentials value set in provider config data to be %s, got %s", tc.ExpectedSchemaValue, val) - } - if config.Credentials != tc.ExpectedConfigValue { - t.Fatalf("expected credentials value set in Config struct to be to be %s, got %s", tc.ExpectedConfigValue, config.Credentials) - } - }) - } -} - func TestProvider_ProviderConfigure_accessToken(t *testing.T) { cases := map[string]struct { diff --git a/google/provider/provider_mmv1_resources.go b/google/provider/provider_mmv1_resources.go index 6860547ee30..1ee760606af 100644 --- a/google/provider/provider_mmv1_resources.go +++ b/google/provider/provider_mmv1_resources.go @@ -315,7 +315,6 @@ var handwrittenDatasources = map[string]*schema.Resource{ "google_vmwareengine_private_cloud": vmwareengine.DataSourceVmwareenginePrivateCloud(), "google_vmwareengine_subnet": vmwareengine.DataSourceVmwareengineSubnet(), "google_vmwareengine_vcenter_credentials": vmwareengine.DataSourceVmwareengineVcenterCredentials(), - // ####### END handwritten datasources ########### } diff --git a/google/provider/provider_test.go b/google/provider/provider_test.go index 93f5b3c5ba3..0b34691422e 100644 --- a/google/provider/provider_test.go +++ b/google/provider/provider_test.go @@ -182,75 +182,6 @@ func TestAccProviderIndirectUserProjectOverride(t *testing.T) { }) } -func TestAccProviderCredentialsEmptyString(t *testing.T) { - // Test is not parallel because ENVs are set. - // Need to skip VCR as this test downloads providers from the Terraform Registry - acctest.SkipIfVcr(t) - - creds := envvar.GetTestCredsFromEnv() - project := envvar.GetTestProjectFromEnv() - t.Setenv("GOOGLE_CREDENTIALS", creds) - t.Setenv("GOOGLE_PROJECT", project) - - pid := "tf-test-" + acctest.RandString(t, 10) - - acctest.VcrTest(t, resource.TestCase{ - PreCheck: func() { acctest.AccTestPreCheck(t) }, - // No TestDestroy since that's not really the point of this test - Steps: []resource.TestStep{ - { - // This is a control for the other test steps; the provider block doesn't contain `credentials = ""` - Config: testAccProviderCredentials_actWithCredsFromEnv(pid), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), - PlanOnly: true, - ExpectNonEmptyPlan: true, - }, - { - // Assert that errors are expected with credentials when - // - GOOGLE_CREDENTIALS is set - // - provider block has credentials = "" - // - TPG v4.60.2 is used - // Context: this was an addidental breaking change introduced with muxing - Config: testAccProviderCredentials_actWithCredsFromEnv_emptyString(pid), - ExternalProviders: map[string]resource.ExternalProvider{ - "google": { - VersionConstraint: "4.60.2", - Source: "hashicorp/google", - }, - }, - PlanOnly: true, - ExpectNonEmptyPlan: true, - ExpectError: regexp.MustCompile(`unexpected end of JSON input`), - }, - { - // Assert that errors are NOT expected with credentials when - // - GOOGLE_CREDENTIALS is set - // - provider block has credentials = "" - // - TPG v4.84.0 is used - // Context: this was the fix for the unintended breaking change in 4.60.2 - Config: testAccProviderCredentials_actWithCredsFromEnv_emptyString(pid), - ExternalProviders: map[string]resource.ExternalProvider{ - "google": { - VersionConstraint: "4.84.0", - Source: "hashicorp/google", - }, - }, - PlanOnly: true, - ExpectNonEmptyPlan: true, - }, - { - // Validation errors are expected in 5.0.0+ - // Context: we intentionally introduced the breaking change again in 5.0.0+ - Config: testAccProviderCredentials_actWithCredsFromEnv_emptyString(pid), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), - PlanOnly: true, - ExpectNonEmptyPlan: true, - ExpectError: regexp.MustCompile(`expected a non-empty string`), - }, - }, - }) -} - func TestAccProviderEmptyStrings(t *testing.T) { t.Parallel()