-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Add support for google_service_account_key #472
Changes from 5 commits
f330886
2b33016
0168ffa
cff02b2
4c08aa3
4e4140d
631e88a
f30f857
0a7181f
0da0fbc
9f2f87c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/terraform/helper/encryption" | ||
"github.com/hashicorp/terraform/helper/schema" | ||
"google.golang.org/api/iam/v1" | ||
) | ||
|
||
func resourceGoogleServiceAccountKey() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceGoogleServiceAccountKeyCreate, | ||
Read: resourceGoogleServiceAccountKeyRead, | ||
Delete: resourceGoogleServiceAccountKeyDelete, | ||
Schema: map[string]*schema.Schema{ | ||
// Required | ||
"service_account_id": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
}, | ||
"public_key_type": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For enum types, would you mind adding a ValidateFunc that checks the values?
|
||
}, | ||
// Optional | ||
"private_key_type": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Default: "TYPE_GOOGLE_CREDENTIALS_FILE", | ||
Optional: true, | ||
ForceNew: true, | ||
}, | ||
|
||
"key_algorithm": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Default: "KEY_ALG_RSA_2048", | ||
Optional: true, | ||
ForceNew: true, | ||
}, | ||
|
||
"pgp_key": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
ForceNew: true, | ||
}, | ||
"private_key": &schema.Schema{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be down in the |
||
Type: schema.TypeString, | ||
Computed: true, | ||
Sensitive: true, | ||
}, | ||
"public_key": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Optional: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this |
||
ForceNew: true, | ||
}, | ||
"name": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Computed: true, | ||
Optional: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is marked |
||
ForceNew: true, | ||
}, | ||
// Computed | ||
"valid_after": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"valid_before": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"private_key_encrypted": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
"private_key_fingerprint": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceGoogleServiceAccountKeyCreate(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*Config) | ||
|
||
serviceAccount := d.Get("service_account_id").(string) | ||
|
||
r := &iam.CreateServiceAccountKeyRequest{} | ||
|
||
if v, ok := d.GetOk("key_algorithm"); ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this has a default value,
|
||
r.KeyAlgorithm = v.(string) | ||
} | ||
|
||
var pubKey string | ||
var err error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't have to declare this here since you initialize it just a few lines down with the |
||
if pubkeyInterface, ok := d.GetOk("public_key"); ok { | ||
pubKey = pubkeyInterface.(string) | ||
} | ||
|
||
if pubKey == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this variable never gets used, it might make sense to just not set it at all and instead do an if statement like:
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: extra newline |
||
if v, ok := d.GetOk("private_key_type"); ok { | ||
r.PrivateKeyType = v.(string) | ||
} | ||
|
||
sak, err := config.clientIAM.Projects.ServiceAccounts.Keys.Create(serviceAccount, r).Do() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused, why do we only call the create function if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes of course. |
||
if err != nil || sak == nil { | ||
return fmt.Errorf("Error creating service account key: %s", err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it might be nice to differentiate in the message the case where it returned an error vs returning nothing |
||
} | ||
|
||
d.SetId(sak.Name) | ||
// Data only available on create. | ||
d.Set("valid_after", sak.ValidAfterTime) | ||
d.Set("valid_before", sak.ValidBeforeTime) | ||
if v, ok := d.GetOk("pgp_key"); ok { | ||
encryptionKey, err := encryption.RetrieveGPGKey(v.(string)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fingerprint, encrypted, err := encryption.EncryptValue(encryptionKey, sak.PrivateKeyData, "Google Service Account Key") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
d.Set("private_key_encrypted", encrypted) | ||
d.Set("private_key_fingerprint", fingerprint) | ||
} else { | ||
d.Set("private_key", sak.PrivateKeyData) | ||
} | ||
} | ||
|
||
err = serviceAccountKeyWaitTime(config.clientIAM.Projects.ServiceAccounts.Keys, d.Id(), d.Get("public_key_type").(string), "Creating Service account key", 4) | ||
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's an error creating it, we probably want to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actualiy a bug on GCE IAM, the key is is well created but not downloadable just after creation. it is necessary to wait a few second or minut to download it. The setId is good because key well exist |
||
return err | ||
} | ||
resourceGoogleServiceAccountKeyRead(d, meta) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this can just be |
||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func resourceGoogleServiceAccountKeyRead(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*Config) | ||
|
||
publicKeyType := d.Get("public_key_type").(string) | ||
|
||
// Confirm the service account key exists | ||
sak, err := config.clientIAM.Projects.ServiceAccounts.Keys.Get(d.Id()).PublicKeyType(publicKeyType).Do() | ||
if err != nil { | ||
return handleNotFoundError(err, d, fmt.Sprintf("Service Account Key %q", d.Id())) | ||
} | ||
|
||
d.Set("name", sak.Name) | ||
d.Set("key_algorithm", sak.KeyAlgorithm) | ||
d.Set("public_key", sak.PublicKeyData) | ||
return nil | ||
} | ||
|
||
func resourceGoogleServiceAccountKeyDelete(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*Config) | ||
|
||
_, err := config.clientIAM.Projects.ServiceAccounts.Keys.Delete(d.Id()).Do() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
d.SetId("") | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform/helper/acctest" | ||
"github.com/hashicorp/terraform/helper/resource" | ||
"github.com/hashicorp/terraform/terraform" | ||
) | ||
|
||
// Test that a service account key can be created and destroyed | ||
func TestAccGoogleServiceAccountKey_basic(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add t.Parallel() (and then a new line) to the beginning of each of these tests? |
||
resourceName := "google_service_account_key.acceptance" | ||
accountID := "a" + acctest.RandString(10) | ||
displayName := "Terraform Test" | ||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
Steps: []resource.TestStep{ | ||
resource.TestStep{ | ||
Config: testAccGoogleServiceAccountKey(accountID, displayName), | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckGoogleServiceAccountKeyExists(resourceName), | ||
resource.TestCheckResourceAttrSet(resourceName, "public_key"), | ||
resource.TestCheckResourceAttrSet(resourceName, "valid_after"), | ||
resource.TestCheckResourceAttrSet(resourceName, "valid_before"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func testAccCheckGoogleServiceAccountKeyExists(r string) resource.TestCheckFunc { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. usually our exists tests also read from the API to confirm the resource exists remotely. If there's a good reason why that shouldn't happen in this test, mind writing a comment as to why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
return func(s *terraform.State) error { | ||
rs, ok := s.RootModule().Resources[r] | ||
if !ok { | ||
return fmt.Errorf("Not found: %s", r) | ||
} | ||
|
||
if rs.Primary.ID == "" { | ||
return fmt.Errorf("No ID is set") | ||
} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
func testAccGoogleServiceAccountKey(account, name string) string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like a fair number of the fields in the schema are untested. Mind making sure there's coverage of the rest of the fields? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
return fmt.Sprintf(`resource "google_service_account" "acceptance" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for consistency with the other resources' tests, can you move everything after |
||
account_id = "%v" | ||
display_name = "%v" | ||
|
||
} | ||
|
||
resource "google_service_account_key" "acceptance" { | ||
service_account_id = "${google_service_account.acceptance.id}" | ||
public_key_type = "TYPE_X509_PEM_FILE" | ||
}`, account, name) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform/helper/resource" | ||
"google.golang.org/api/googleapi" | ||
iam "google.golang.org/api/iam/v1" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does this import need to be named? |
||
) | ||
|
||
type ServiceAccountKeyWaiter struct { | ||
Service *iam.ProjectsServiceAccountsKeysService | ||
PublicKeyType string | ||
KeyName string | ||
} | ||
|
||
func (w *ServiceAccountKeyWaiter) RefreshFunc() resource.StateRefreshFunc { | ||
return func() (interface{}, string, error) { | ||
var err error | ||
var sak *iam.ServiceAccountKey | ||
sak, err = w.Service.Get(w.KeyName).PublicKeyType(w.PublicKeyType).Do() | ||
|
||
if err != nil { | ||
if err.(*googleapi.Error).Code == 404 { | ||
return nil, "PENDING", nil | ||
} else { | ||
return nil, "", err | ||
} | ||
} else { | ||
return sak, "DONE", nil | ||
} | ||
} | ||
} | ||
|
||
func (w *ServiceAccountKeyWaiter) Conf() *resource.StateChangeConf { | ||
return &resource.StateChangeConf{ | ||
Pending: []string{"PENDING"}, | ||
Target: []string{"DONE"}, | ||
Refresh: w.RefreshFunc(), | ||
} | ||
} | ||
|
||
func serviceAccountKeyWaitTime(client *iam.ProjectsServiceAccountsKeysService, keyName, publicKeyType, activity string, timeoutMin int) error { | ||
w := &ServiceAccountKeyWaiter{ | ||
Service: client, | ||
PublicKeyType: publicKeyType, | ||
KeyName: keyName, | ||
} | ||
|
||
state := w.Conf() | ||
state.Delay = 10 * time.Second | ||
state.Timeout = time.Duration(timeoutMin) * time.Minute | ||
state.MinTimeout = 2 * time.Second | ||
_, err := state.WaitForState() | ||
if err != nil { | ||
return fmt.Errorf("Error waiting for %s: %s", activity, err) | ||
} | ||
|
||
return nil | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this value has a reasonable default: https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts.keys/get. Any reason why it's
Required
vsprivate_key_type
andkey_algorithm
being Optional?