diff --git a/remote/remote_state_gcs.go b/remote/remote_state_gcs.go index be5cda40f..3b42295f9 100644 --- a/remote/remote_state_gcs.go +++ b/remote/remote_state_gcs.go @@ -9,6 +9,8 @@ import ( "strconv" "time" + "google.golang.org/api/impersonate" + "cloud.google.com/go/storage" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/errors" @@ -470,9 +472,15 @@ func CreateGCSClient(gcsConfigRemote RemoteStateConfigGCS) (*storage.Client, err } if gcsConfigRemote.ImpersonateServiceAccount != "" { - opts = append(opts, option.ImpersonateCredentials( - gcsConfigRemote.ImpersonateServiceAccount, - gcsConfigRemote.ImpersonateServiceAccountDelegates...)) + ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ + TargetPrincipal: gcsConfigRemote.ImpersonateServiceAccount, + Scopes: []string{storage.ScopeFullControl}, + Delegates: gcsConfigRemote.ImpersonateServiceAccountDelegates, + }) + if err != nil { + return nil, err + } + opts = append(opts, option.WithTokenSource(ts)) } client, err := storage.NewClient(ctx, opts...) diff --git a/test/fixture-gcs-impersonate/main.tf b/test/fixture-gcs-impersonate/main.tf new file mode 100644 index 000000000..f110804e3 --- /dev/null +++ b/test/fixture-gcs-impersonate/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "gcs" {} +} + +output "value" { + value = "42" +} diff --git a/test/fixture-gcs-impersonate/terragrunt.hcl b/test/fixture-gcs-impersonate/terragrunt.hcl new file mode 100644 index 000000000..0ab02651f --- /dev/null +++ b/test/fixture-gcs-impersonate/terragrunt.hcl @@ -0,0 +1,16 @@ +remote_state { + backend = "gcs" + + config = { + project = "__FILL_IN_PROJECT__" + location = "__FILL_IN_LOCATION__" + bucket = "__FILL_IN_BUCKET_NAME__" + impersonate_service_account = "__FILL_IN_GCP_EMAIL__" + prefix = "terraform.tfstate" + + gcs_bucket_labels = { + owner = "terragrunt_test" + name = "terraform_state_storage" + } + } +} diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go index b054dc018..058405007 100644 --- a/test/integration_serial_test.go +++ b/test/integration_serial_test.go @@ -352,3 +352,39 @@ func TestTerragruntParallelism(t *testing.T) { }) } } + +func TestTerragruntWorksWithImpersonateGCSBackend(t *testing.T) { + defaultCreds := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") + defer os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", defaultCreds) + + impersonatorKey := os.Getenv("GCLOUD_SERVICE_KEY_IMPERSONATOR") + if impersonatorKey == "" { + t.Fatalf("Required environment variable `%s` - not found", "GCLOUD_SERVICE_KEY_IMPERSONATOR") + } + tmpImpersonatorCreds := createTmpTerragruntConfigContent(t, impersonatorKey, "impersonator-key.json") + defer removeFile(t, tmpImpersonatorCreds) + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", tmpImpersonatorCreds) + + project := os.Getenv("GOOGLE_CLOUD_PROJECT") + gcsBucketName := fmt.Sprintf("terragrunt-test-bucket-%s", strings.ToLower(uniqueId())) + + // run with impersonation + tmpTerragruntImpersonateGCSConfigPath := createTmpTerragruntGCSConfig(t, TEST_FIXTURE_GCS_IMPERSONATE_PATH, project, TERRAFORM_REMOTE_STATE_GCP_REGION, gcsBucketName, config.DefaultTerragruntConfigPath) + runTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-config %s --terragrunt-working-dir %s", tmpTerragruntImpersonateGCSConfigPath, TEST_FIXTURE_GCS_IMPERSONATE_PATH)) + + var expectedGCSLabels = map[string]string{ + "owner": "terragrunt_test", + "name": "terraform_state_storage"} + validateGCSBucketExistsAndIsLabeled(t, TERRAFORM_REMOTE_STATE_GCP_REGION, gcsBucketName, expectedGCSLabels) + + email := os.Getenv("GOOGLE_IDENTITY_EMAIL") + attrs := gcsObjectAttrs(t, gcsBucketName, "terraform.tfstate/default.tfstate") + ownerEmail := false + for _, a := range attrs.ACL { + if (a.Role == "OWNER") && (a.Email == email) { + ownerEmail = true + break + } + } + assert.True(t, ownerEmail, "Identity email should match the impersonated account") +} diff --git a/test/integration_test.go b/test/integration_test.go index f4ad75a8f..9817131ad 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -162,6 +162,7 @@ const ( TEST_FIXTURE_STRCONTAINS = "fixture-strcontains" TEST_FIXTURE_INIT_CACHE = "fixture-init-cache" TEST_FIXTURE_NULL_VALUE = "fixture-null-values" + TEST_FIXTURE_GCS_IMPERSONATE_PATH = "fixture-gcs-impersonate/" TERRAFORM_BINARY = "terraform" TERRAFORM_FOLDER = ".terraform" TERRAFORM_STATE = "terraform.tfstate" @@ -4077,6 +4078,9 @@ func copyTerragruntGCSConfigAndFillPlaceholders(t *testing.T, configSrcPath stri contents = strings.Replace(contents, "__FILL_IN_LOCATION__", location, -1) contents = strings.Replace(contents, "__FILL_IN_BUCKET_NAME__", gcsBucketName, -1) + email := os.Getenv("GOOGLE_IDENTITY_EMAIL") + contents = strings.Replace(contents, "__FILL_IN_GCP_EMAIL__", email, -1) + if err := ioutil.WriteFile(configDestPath, []byte(contents), 0444); err != nil { t.Fatalf("Error writing temp Terragrunt config to %s: %v", configDestPath, err) } @@ -4382,7 +4386,6 @@ func validateGCSBucketExistsAndIsLabeled(t *testing.T, location string, bucketNa // verify the bucket location ctx := context.Background() bucket := gcsClient.Bucket(bucketName) - attrs, err := bucket.Attrs(ctx) if err != nil { t.Fatal(err) @@ -4395,6 +4398,26 @@ func validateGCSBucketExistsAndIsLabeled(t *testing.T, location string, bucketNa } } +// gcsObjectAttrs returns the attributes of the specified object in the bucket +func gcsObjectAttrs(t *testing.T, bucketName string, objectName string) *storage.ObjectAttrs { + remoteStateConfig := remote.RemoteStateConfigGCS{Bucket: bucketName} + + gcsClient, err := remote.CreateGCSClient(remoteStateConfig) + if err != nil { + t.Fatalf("Error creating GCS client: %v", err) + } + + ctx := context.Background() + bucket := gcsClient.Bucket(bucketName) + + handle := bucket.Object(objectName) + attrs, err := handle.Attrs(ctx) + if err != nil { + t.Fatalf("Error reading object attributes %s %v", objectName, err) + } + return attrs +} + func assertGCSLabels(t *testing.T, expectedLabels map[string]string, bucketName string, client *storage.Client) { ctx := context.Background() bucket := client.Bucket(bucketName)