Skip to content

Commit

Permalink
Google Cloud impersonate fixes (#2679)
Browse files Browse the repository at this point in the history
* Migration of impersonate changes

* Add test for bucket creation

* Add replacement of email

* acl item

* add logging of acl

* Testing gcp fix

* object fetching test

* Testing iam printing

* testing ProjectTeam

* Notifications

* add log printing

* Add test output value

* Add function to read objects metadata

* acl email

* Add check for gcp acl

* Tests cleanup

* Tests cleanup

* Removed debug prints

* Added loop termination
  • Loading branch information
denis256 authored Aug 21, 2023
1 parent 19efb19 commit c4a76a1
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 4 deletions.
14 changes: 11 additions & 3 deletions remote/remote_state_gcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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...)
Expand Down
7 changes: 7 additions & 0 deletions test/fixture-gcs-impersonate/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
backend "gcs" {}
}

output "value" {
value = "42"
}
16 changes: 16 additions & 0 deletions test/fixture-gcs-impersonate/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
36 changes: 36 additions & 0 deletions test/integration_serial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
25 changes: 24 additions & 1 deletion test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit c4a76a1

Please sign in to comment.