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

Add acceptance tests for how provider handles impersonate_service_account argument #11641

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (d *GoogleProviderConfigPluginFrameworkDataSource) Read(ctx context.Context

data.Credentials = d.providerConfig.Credentials
data.AccessToken = d.providerConfig.AccessToken
// TODO(SarahFrench) - impersonate_service_account
data.ImpersonateServiceAccount = d.providerConfig.ImpersonateServiceAccount
// TODO(SarahFrench) - impersonate_service_account_delegates
data.Project = d.providerConfig.Project
data.Region = d.providerConfig.Region
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ func testAccFwProvider_access_token_configPrecedenceOverEnvironmentVariables(t *
Config: testAccFwProvider_access_tokenInProviderBlock(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "access_token", providerAccessToken),
)},
),
},
},
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package fwprovider_test

import (
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-google/google/acctest"
)

// TestAccFwProvider_impersonate_service_account is a series of acc tests asserting how the plugin-framework provider handles impersonate_service_account arguments
// It is plugin-framework specific because the HCL used provisions plugin-framework-implemented resources
// It is a counterpart to TestAccSdkProvider_impersonate_service_account
func TestAccFwProvider_impersonate_service_account(t *testing.T) {
testCases := map[string]func(t *testing.T){
// Configuring the provider using inputs
"config takes precedence over environment variables": testAccFwProvider_impersonate_service_account_configPrecedenceOverEnvironmentVariables,
"when impersonate_service_account is unset in the config, environment variables are used in a given order": testAccFwProvider_impersonate_service_account_precedenceOrderEnvironmentVariables, // GOOGLE_IMPERSONATE_SERVICE_ACCOUNT

// Schema-level validation
"when impersonate_service_account is set to an empty string in the config the value isn't ignored and results in an error": testAccFwProvider_impersonate_service_account_emptyStringValidation,

// Usage
// We need to wait for a non-Firebase resource to be migrated to the plugin-framework to enable writing this test
// "impersonate_service_account controls which service account is used for actions"
Comment on lines +23 to +25
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think waiting for an 'easier' resource to use would be best - given that Firebase is the only PF-impemented resources/data source available to us and it's Beta-only.

}

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_impersonate_service_account_configPrecedenceOverEnvironmentVariables(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API

impersonateServiceAccountEnvironment := "value-from-envs@example.com"
impersonateServiceAccountProviderBlock := "value-from-provider-block@example.com"

// ensure all possible impersonate_service_account env vars set; show they aren't used
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", impersonateServiceAccountEnvironment)

context := map[string]interface{}{
"impersonate_service_account": impersonateServiceAccountProviderBlock,
}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccFwProvider_impersonate_service_account_inProviderBlock(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "impersonate_service_account", impersonateServiceAccountProviderBlock),
),
},
},
})
}

func testAccFwProvider_impersonate_service_account_precedenceOrderEnvironmentVariables(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API
/*
These are all the ENVs for impersonate_service_account, and they are in order of precedence.
GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
*/

impersonateServiceAccount := "foobar@example.com"

context := map[string]interface{}{}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
PreConfig: func() {
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", impersonateServiceAccount)
},
Config: testAccFwProvider_impersonate_service_account_inEnvsOnly(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "impersonate_service_account", impersonateServiceAccount),
),
},
},
})
}

func testAccFwProvider_impersonate_service_account_emptyStringValidation(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API

impersonateServiceAccountEnvironment := "value-from-envs@example.com"

// ensure all possible impersonate_service_account env vars set; show they aren't used
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", impersonateServiceAccountEnvironment)

context := map[string]interface{}{
"impersonate_service_account": "", // empty string used
}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccFwProvider_impersonate_service_account_inProviderBlock(context),
PlanOnly: true,
ExpectError: regexp.MustCompile("expected a non-empty string"),
},
},
})
}

// testAccFwProvider_impersonate_service_account_inProviderBlock allows setting the impersonate_service_account argument in a provider block.
func testAccFwProvider_impersonate_service_account_inProviderBlock(context map[string]interface{}) string {
return acctest.Nprintf(`
provider "google" {
impersonate_service_account = "%{impersonate_service_account}"
}

data "google_provider_config_plugin_framework" "default" {}

`, context)
}

// testAccFwProvider_impersonate_service_account_inEnvsOnly allows testing when the impersonate_service_account argument
// is only supplied via ENVs
func testAccFwProvider_impersonate_service_account_inEnvsOnly(context map[string]interface{}) string {
return acctest.Nprintf(`
data "google_provider_config_plugin_framework" "default" {}

`, context)
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ func testAccFwProvider_access_token_configPrecedenceOverEnvironmentVariables(t *
Config: testAccFwProvider_access_tokenInProviderBlock(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_plugin_framework.default", "access_token", providerAccessToken),
)},
),
},
},
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ 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
// transport_tpg.Config has a the fields below, hence these changes are needed
Credentials types.String
AccessToken types.String
ImpersonateServiceAccount types.String
// End temporary

BillingProject types.String
Expand Down Expand Up @@ -106,6 +107,7 @@ func (p *FrameworkProviderConfig) LoadAndValidateFramework(ctx context.Context,
// Temporary
p.Credentials = data.Credentials
p.AccessToken = data.AccessToken
p.ImpersonateServiceAccount = data.ImpersonateServiceAccount
// End temporary

// Copy values from the ProviderModel struct containing data about the provider configuration (present only when responsing to ConfigureProvider rpc calls)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ func testAccSdkProvider_access_token_configPrecedenceOverEnvironmentVariables(t
Config: testAccSdkProvider_access_tokenInProviderBlock(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "access_token", providerAccessToken),
)},
),
},
},
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package provider_test

import (
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-google/google/acctest"
)

// TestAccSdkProvider_impersonate_service_account is a series of acc tests asserting how the SDK provider handles impersonate_service_account arguments
// It is SDK specific because the HCL used provisions SDK-implemented resources
// It is a counterpart to TestAccFwProvider_impersonate_service_account
func TestAccSdkProvider_impersonate_service_account(t *testing.T) {
testCases := map[string]func(t *testing.T){
// Configuring the provider using inputs
"config takes precedence over environment variables": testAccSdkProvider_impersonate_service_account_configPrecedenceOverEnvironmentVariables,
"when impersonate_service_account is unset in the config, environment variables are used in a given order": testAccSdkProvider_impersonate_service_account_precedenceOrderEnvironmentVariables, // GOOGLE_IMPERSONATE_SERVICE_ACCOUNT

// Schema-level validation
"when impersonate_service_account is set to an empty string in the config the value isn't ignored and results in an error": testAccSdkProvider_impersonate_service_account_emptyStringValidation,

// Usage
"impersonate_service_account controls which service account is used for actions": testAccSdkProvider_impersonate_service_account_usage,
}

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_impersonate_service_account_configPrecedenceOverEnvironmentVariables(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API

impersonateServiceAccountEnvironment := "value-from-envs@example.com"
impersonateServiceAccountProviderBlock := "value-from-provider-block@example.com"

// ensure all possible impersonate_service_account env vars set; show they aren't used
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", impersonateServiceAccountEnvironment)

context := map[string]interface{}{
"impersonate_service_account": impersonateServiceAccountProviderBlock,
}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccSdkProvider_impersonate_service_account_inProviderBlock(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "impersonate_service_account", impersonateServiceAccountProviderBlock),
)},
},
})
}

func testAccSdkProvider_impersonate_service_account_precedenceOrderEnvironmentVariables(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API
/*
These are all the ENVs for impersonate_service_account, and they are in order of precedence.
GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
*/

impersonateServiceAccount := "foobar@example.com"

context := map[string]interface{}{}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
PreConfig: func() {
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", impersonateServiceAccount)
},
Config: testAccSdkProvider_impersonate_service_account_inEnvsOnly(context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_provider_config_sdk.default", "impersonate_service_account", impersonateServiceAccount),
)},
},
})
}

func testAccSdkProvider_impersonate_service_account_emptyStringValidation(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API

impersonateServiceAccountEnvironment := "value-from-envs@example.com"

// ensure all possible impersonate_service_account env vars set; show they aren't used
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", impersonateServiceAccountEnvironment)

context := map[string]interface{}{
"impersonate_service_account": "", // empty string used
}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccSdkProvider_impersonate_service_account_inProviderBlock(context),
PlanOnly: true,
ExpectError: regexp.MustCompile("expected a non-empty string"),
},
},
})
}

func testAccSdkProvider_impersonate_service_account_usage(t *testing.T) {
acctest.SkipIfVcr(t) // Test doesn't interact with API

// ensure env vars unset
t.Setenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", "")

context := map[string]interface{}{
"random_suffix": acctest.RandString(t, 10),
}

acctest.VcrTest(t, resource.TestCase{
// No PreCheck for checking ENVs
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccSdkProvider_impersonate_service_account_testViaFailure(context),
ExpectError: regexp.MustCompile("Error creating Topic: googleapi: Error 403: User not authorized"),
Comment on lines +132 to +133
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error can only arise if Terraform is using the impersonated service account that has no permissions

},
},
})
}

// testAccSdkProvider_impersonate_service_account_inProviderBlock allows setting the impersonate_service_account argument in a provider block.
// This function uses data.google_provider_config_sdk because it is implemented with the SDKv2
func testAccSdkProvider_impersonate_service_account_inProviderBlock(context map[string]interface{}) string {
return acctest.Nprintf(`
provider "google" {
impersonate_service_account = "%{impersonate_service_account}"
}

data "google_provider_config_sdk" "default" {}

`, context)
}

// testAccSdkProvider_impersonate_service_account_inEnvsOnly allows testing when the impersonate_service_account argument
// is only supplied via ENVs
func testAccSdkProvider_impersonate_service_account_inEnvsOnly(context map[string]interface{}) string {
return acctest.Nprintf(`
data "google_provider_config_sdk" "default" {}

`, context)
}

func testAccSdkProvider_impersonate_service_account_testViaFailure(context map[string]interface{}) string {
return acctest.Nprintf(`
// This will succeed due to the Terraform identity having necessary permissions
resource "google_pubsub_topic" "ok" {
name = "tf-test-%{random_suffix}-ok"
}

// Create a service account and ensure the Terraform identity can make tokens for it
resource "google_service_account" "default" {
account_id = "tf-test-%{random_suffix}"
display_name = "Acceptance test impersonated service account"
}

data "google_client_openid_userinfo" "me" {
}

resource "google_service_account_iam_member" "token" {
service_account_id = google_service_account.default.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${data.google_client_openid_userinfo.me.email}"
}

// Impersonate the created service account
provider "google" {
alias = "impersonation"
impersonate_service_account = google_service_account.default.email
}

// This will fail due to the impersonated service account not having any permissions
resource "google_pubsub_topic" "fail" {
provider = google.impersonation
name = "tf-test-%{random_suffix}-fail"
}
`, context)
}