From 8f31fec85728636c07d177bbf8159e01622a5477 Mon Sep 17 00:00:00 2001 From: Jonathan Pentecost Date: Fri, 1 Jun 2018 01:31:45 +0100 Subject: [PATCH] New datasource: service account and service account key (#1535) --- google/data_source_google_service_account.go | 70 ++++++++++++++++++ .../data_source_google_service_account_key.go | 74 +++++++++++++++++++ ..._source_google_service_account_key_test.go | 56 ++++++++++++++ ...data_source_google_service_account_test.go | 48 ++++++++++++ google/provider.go | 2 + google/resource_google_service_account_key.go | 13 ++-- google/utils.go | 16 ++++ google/utils_test.go | 27 +++++++ google/validation.go | 5 +- ...ource_google_service_account.html.markdown | 63 ++++++++++++++++ ...e_google_service_account_key.html.markdown | 50 +++++++++++++ website/google.erb | 7 +- 12 files changed, 423 insertions(+), 8 deletions(-) create mode 100644 google/data_source_google_service_account.go create mode 100644 google/data_source_google_service_account_key.go create mode 100644 google/data_source_google_service_account_key_test.go create mode 100644 google/data_source_google_service_account_test.go create mode 100644 website/docs/d/datasource_google_service_account.html.markdown create mode 100644 website/docs/d/datasource_google_service_account_key.html.markdown diff --git a/google/data_source_google_service_account.go b/google/data_source_google_service_account.go new file mode 100644 index 00000000..6c75da0e --- /dev/null +++ b/google/data_source_google_service_account.go @@ -0,0 +1,70 @@ +package google + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceGoogleServiceAccount() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleServiceAccountRead, + Schema: map[string]*schema.Schema{ + "account_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRFC1035Name(6, 30), + }, + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "email": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "unique_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "display_name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceGoogleServiceAccountRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Get the project from the resource or fallback to the project + // in the provider configuration + project, err := getProject(d, config) + if err != nil { + return err + } + + // Get the service account as a fully qualified name + serviceAccountName := serviceAccountFQN(d.Get("account_id").(string), project) + + sa, err := config.clientIAM.Projects.ServiceAccounts.Get(serviceAccountName).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Service Account %q", serviceAccountName)) + } + + d.SetId(sa.Name) + d.Set("email", sa.Email) + d.Set("unique_id", sa.UniqueId) + d.Set("project", sa.ProjectId) + d.Set("account_id", strings.Split(sa.Email, "@")[0]) + d.Set("name", sa.Name) + d.Set("display_name", sa.DisplayName) + + return nil +} diff --git a/google/data_source_google_service_account_key.go b/google/data_source_google_service_account_key.go new file mode 100644 index 00000000..81d3050f --- /dev/null +++ b/google/data_source_google_service_account_key.go @@ -0,0 +1,74 @@ +package google + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func dataSourceGoogleServiceAccountKey() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleServiceAccountKeyRead, + + Schema: map[string]*schema.Schema{ + "service_account_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "public_key_type": &schema.Schema{ + Type: schema.TypeString, + Default: "TYPE_X509_PEM_FILE", + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"TYPE_NONE", "TYPE_X509_PEM_FILE", "TYPE_RAW_PUBLIC_KEY"}, false), + }, + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + // Computed + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "key_algorithm": { + Type: schema.TypeString, + Computed: true, + }, + "public_key": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceGoogleServiceAccountKeyRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Get the project from the resource or fallback to the project + // in the provider configuration + project, err := getProject(d, config) + if err != nil { + return err + } + + // Get the service account as the fully qualified name + serviceAccountName := serviceAccountFQN(d.Get("service_account_id").(string), project) + + publicKeyType := d.Get("public_key_type").(string) + + // Confirm the service account key exists + sak, err := config.clientIAM.Projects.ServiceAccounts.Keys.Get(serviceAccountName).PublicKeyType(publicKeyType).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Service Account Key %q", serviceAccountName)) + } + + d.SetId(sak.Name) + + d.Set("name", sak.Name) + d.Set("key_algorithm", sak.KeyAlgorithm) + d.Set("public_key", sak.PublicKeyData) + + return nil +} diff --git a/google/data_source_google_service_account_key_test.go b/google/data_source_google_service_account_key_test.go new file mode 100644 index 00000000..20acc5e7 --- /dev/null +++ b/google/data_source_google_service_account_key_test.go @@ -0,0 +1,56 @@ +package google + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDatasourceGoogleServiceAccountKey_basic(t *testing.T) { + t.Parallel() + + resourceName := "data.google_service_account_key.acceptance" + account := acctest.RandomWithPrefix("tf-test") + serviceAccountName := fmt.Sprintf( + "projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", + getTestProjectFromEnv(), + account, + getTestProjectFromEnv(), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccDatasourceGoogleServiceAccountKey(account), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleServiceAccountKeyExists(resourceName), + // Check that the 'name' starts with the service account name + resource.TestMatchResourceAttr(resourceName, "name", regexp.MustCompile(serviceAccountName)), + resource.TestCheckResourceAttrSet(resourceName, "key_algorithm"), + resource.TestCheckResourceAttrSet(resourceName, "public_key"), + ), + }, + }, + }) +} + +func testAccDatasourceGoogleServiceAccountKey(account string) string { + return fmt.Sprintf(` +resource "google_service_account" "acceptance" { + account_id = "%s" +} + +resource "google_service_account_key" "acceptance" { + service_account_id = "${google_service_account.acceptance.name}" + public_key_type = "TYPE_X509_PEM_FILE" +} + +data "google_service_account_key" "acceptance" { + service_account_id = "${google_service_account_key.acceptance.id}" +}`, account) +} diff --git a/google/data_source_google_service_account_test.go b/google/data_source_google_service_account_test.go new file mode 100644 index 00000000..f575f516 --- /dev/null +++ b/google/data_source_google_service_account_test.go @@ -0,0 +1,48 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDatasourceGoogleServiceAccount_basic(t *testing.T) { + t.Parallel() + + resourceName := "data.google_service_account.acceptance" + account := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleServiceAccount_basic(account), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleServiceAccountExists(resourceName), + resource.TestCheckResourceAttr( + resourceName, "id", fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", getTestProjectFromEnv(), account, getTestProjectFromEnv())), + resource.TestCheckResourceAttrSet(resourceName, "email"), + resource.TestCheckResourceAttrSet(resourceName, "unique_id"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "display_name"), + ), + }, + }, + }) +} + +func testAccCheckGoogleServiceAccount_basic(account string) string { + return fmt.Sprintf(` +resource "google_service_account" "acceptance" { + account_id = "%s" + display_name = "Testing Account" +} + +data "google_service_account" "acceptance" { + account_id = "${google_service_account.acceptance.account_id}" +} +`, account) +} diff --git a/google/provider.go b/google/provider.go index 5009fa8e..6cde7bce 100644 --- a/google/provider.go +++ b/google/provider.go @@ -88,6 +88,8 @@ func Provider() terraform.ResourceProvider { "google_kms_secret": dataSourceGoogleKmsSecret(), "google_folder": dataSourceGoogleFolder(), "google_organization": dataSourceGoogleOrganization(), + "google_service_account": dataSourceGoogleServiceAccount(), + "google_service_account_key": dataSourceGoogleServiceAccountKey(), "google_storage_object_signed_url": dataSourceGoogleSignedUrl(), "google_storage_project_service_account": dataSourceGoogleStorageProjectServiceAccount(), "google_compute_backend_service": dataSourceGoogleComputeBackendService(), diff --git a/google/resource_google_service_account_key.go b/google/resource_google_service_account_key.go index 75dfe23d..2a6a6b58 100644 --- a/google/resource_google_service_account_key.go +++ b/google/resource_google_service_account_key.go @@ -2,7 +2,6 @@ package google import ( "fmt" - "strings" "github.com/hashicorp/terraform/helper/encryption" "github.com/hashicorp/terraform/helper/schema" @@ -88,17 +87,21 @@ func resourceGoogleServiceAccountKey() *schema.Resource { func resourceGoogleServiceAccountKeyCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - serviceAccount := d.Get("service_account_id").(string) - if !strings.HasPrefix(serviceAccount, "projects/") { - serviceAccount = "projects/-/serviceAccounts/" + serviceAccount + // Get the project from the resource or fallback to the project + // in the provider configuration + project, err := getProject(d, config) + if err != nil { + return err } + serviceAccountName := serviceAccountFQN(d.Get("service_account_id").(string), project) + r := &iam.CreateServiceAccountKeyRequest{ KeyAlgorithm: d.Get("key_algorithm").(string), PrivateKeyType: d.Get("private_key_type").(string), } - sak, err := config.clientIAM.Projects.ServiceAccounts.Keys.Create(serviceAccount, r).Do() + sak, err := config.clientIAM.Projects.ServiceAccounts.Keys.Create(serviceAccountName, r).Do() if err != nil { return fmt.Errorf("Error creating service account key: %s", err) } diff --git a/google/utils.go b/google/utils.go index 05cf67ad..edca2317 100644 --- a/google/utils.go +++ b/google/utils.go @@ -360,3 +360,19 @@ func lockedCall(lockKey string, f func() error) error { return f() } + +// serviceAccountFQN will attempt to generate the fully qualified name in the format of: +// "projects/(-|)/serviceAccounts/@.iam.gserviceaccount.com" +func serviceAccountFQN(serviceAccount, project string) string { + // If the service account id isn't already the fully qualified name + if !strings.HasPrefix(serviceAccount, "projects/") { + // If the service account id is an email + if strings.Contains(serviceAccount, "@") { + serviceAccount = "projects/-/serviceAccounts/" + serviceAccount + } else { + // If the service account id doesn't contain the email, build it + serviceAccount = fmt.Sprintf("projects/-/serviceAccounts/%s@%s.iam.gserviceaccount.com", serviceAccount, project) + } + } + return serviceAccount +} diff --git a/google/utils_test.go b/google/utils_test.go index 509f95e4..d987b43c 100644 --- a/google/utils_test.go +++ b/google/utils_test.go @@ -444,3 +444,30 @@ func TestEmptyOrDefaultStringSuppress(t *testing.T) { } } } + +func TestServiceAccountFQN(t *testing.T) { + // Every test case should produce this fully qualified service account name + serviceAccountExpected := "projects/-/serviceAccounts/test-service-account@test-project.iam.gserviceaccount.com" + cases := map[string]struct { + serviceAccount string + project string + }{ + "service account fully qualified name from account id": { + serviceAccount: "test-service-account", + project: "test-project", + }, + "service account fully qualified name from account email": { + serviceAccount: "test-service-account@test-project.iam.gserviceaccount.com", + }, + "service account fully qualified name from account name": { + serviceAccount: "projects/-/serviceAccounts/test-service-account@test-project.iam.gserviceaccount.com", + }, + } + + for tn, tc := range cases { + serviceAccountName := serviceAccountFQN(tc.serviceAccount, tc.project) + if serviceAccountName != serviceAccountExpected { + t.Errorf("bad: %s, expected '%s' but returned '%s", tn, serviceAccountExpected, serviceAccountName) + } + } +} diff --git a/google/validation.go b/google/validation.go index aa72ae1c..15ea6bad 100644 --- a/google/validation.go +++ b/google/validation.go @@ -2,12 +2,13 @@ package google import ( "fmt" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/helper/validation" "net" "regexp" "strconv" "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) const ( diff --git a/website/docs/d/datasource_google_service_account.html.markdown b/website/docs/d/datasource_google_service_account.html.markdown new file mode 100644 index 00000000..36da7cd2 --- /dev/null +++ b/website/docs/d/datasource_google_service_account.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "google" +page_title: "Google: google_service_account" +sidebar_current: "docs-google-datasource-service-account" +description: |- + Get the service account from a project. +--- + +# google\_service\_account + +Get the service account from a project. For more information see +the official [API](https://cloud.google.com/compute/docs/access/service-accounts) documentation. + +## Example Usage + +```hcl +data "google_service_account" "object_viewer" { + account_id = "object-viewer" +} +``` + +## Example Usage, save key in Kubernetes secret +```hcl +data "google_service_account" "myaccount" { + account_id = "myaccount-id" +} + +resource "google_service_account_key" "mykey" { + service_account_id = "${data.google_service_account.myaccount.name}" +} + +resource "kubernetes_secret" "google-application-credentials" { + metadata { + name = "google-application-credentials" + } + data { + credentials.json = "${base64decode(google_service_account_key.mykey.private_key)}" + } +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Required) The Service account id. + +* `project` - (Optional) The ID of the project that the service account will be created in. + Defaults to the provider project configuration. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `email` - The e-mail address of the service account. This value + should be referenced from any `google_iam_policy` data sources + that would grant the service account privileges. + +* `unique_id` - The unique id of the service account. + +* `name` - The fully-qualified name of the service account. + +* `display_name` - The display name for the service account. diff --git a/website/docs/d/datasource_google_service_account_key.html.markdown b/website/docs/d/datasource_google_service_account_key.html.markdown new file mode 100644 index 00000000..2db10338 --- /dev/null +++ b/website/docs/d/datasource_google_service_account_key.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "google" +page_title: "Google: google_service_account_key" +sidebar_current: "docs-google-datasource-service-account-key" +description: |- + Get a Google Cloud Platform service account Public Key +--- + +# google\_service\_account\_key + +Get service account public key. For more information, see [the official documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) and [API](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts.keys/get). + + +## Example Usage + +```hcl +data "google_service_account" "myaccount" { + account_id = "myaccount" +} + +data "google_service_account_key" "mykey" { + service_account_id = "${data.google_service_account.myaccount.name}" + public_key_type = "TYPE_X509_PEM_FILE" +} + +output "mykey_public_key" { + value = "${data.google_service_account_key.mykey.public_key}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `service_account_id` - (Required) The Service account id of the Key Pair. This can be a string in the format +`{ACCOUNT}` or `projects/{PROJECT_ID}/serviceAccounts/{ACCOUNT}`, where `{ACCOUNT}` is the email address or +unique id of the service account. If the `{ACCOUNT}` syntax is used, the project will be inferred from the account. + +* `project` - (Optional) The ID of the project that the service account will be created in. + Defaults to the provider project configuration. + +* `public_key_type` (Optional) The output format of the public key requested. X509_PEM is the default output format. + +## Attributes Reference + +The following attributes are exported in addition to the arguments listed above: + +* `name` - The name used for this key pair + +* `public_key` - The public key, base64 encoded diff --git a/website/google.erb b/website/google.erb index f443a40d..e85c7a14 100644 --- a/website/google.erb +++ b/website/google.erb @@ -40,7 +40,6 @@ > google_compute_network - > google_project @@ -98,6 +97,12 @@ > google_folder + > + google_service_account + + > + google_service_account_key + > google_storage_object_signed_url