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

Google Cloud impersonate fixes #2679

Merged
merged 19 commits into from
Aug 21, 2023
Merged
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 @@ -4061,6 +4062,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 @@ -4366,7 +4370,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 @@ -4379,6 +4382,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