Skip to content

Commit

Permalink
New datasource: service account and service account key (#1535)
Browse files Browse the repository at this point in the history
  • Loading branch information
vishen authored and nat-henderson committed Jun 1, 2018
1 parent c71ab3f commit 8f31fec
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 8 deletions.
70 changes: 70 additions & 0 deletions google/data_source_google_service_account.go
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 74 additions & 0 deletions google/data_source_google_service_account_key.go
Original file line number Diff line number Diff line change
@@ -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
}
56 changes: 56 additions & 0 deletions google/data_source_google_service_account_key_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
48 changes: 48 additions & 0 deletions google/data_source_google_service_account_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
13 changes: 8 additions & 5 deletions google/resource_google_service_account_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package google

import (
"fmt"
"strings"

"github.com/hashicorp/terraform/helper/encryption"
"github.com/hashicorp/terraform/helper/schema"
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions google/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/(-|<project_name>)/serviceAccounts/<service_account_id>@<project_name>.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
}
27 changes: 27 additions & 0 deletions google/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
5 changes: 3 additions & 2 deletions google/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading

0 comments on commit 8f31fec

Please sign in to comment.