-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds support for creating KMS CryptoKeys resources (#692)
* Adds support for creating KMS CryptoKeys resources * Destroy extant CryptoKeyVersions on CryptoKey destroy * Inherit project, location etc from KeyRing in CryptoKey * Add function to calculate next rotation * Implement RotationPeriod parameter on CryptoKey * Import CryptoKey state * Uncommit my local acceptance test hacks * Docs for google_kms_crypto_key * Clear id at the end of CryptoKey deletion Also add more detail to warning message. * Fix parseCryptoKeyId error messages * Use correct naming in CryptoKeyIdParsing test * Check RotationPeriod is present in acceptance test * Rename variable in test function for consistency * Fix wrong resource name in cryptokey docs * Add KeyRing to CryptoKey doc example * Run test CryptoKey configs through terraform fmt * Don't set CryptoKey purpose in terraform state on import * Fix indentation in CryptoKey test * Parallelise CryptoKey tests * Set rotation_key on CryptoKey read * Move RotationPeriod validation to planning phase * Use import state passthrough for CryptoKey * Correct casing issues in test case names * Remove redundant CheckDestroy calls in CryptoKey tests * Add explanatory comment about extra test steps * More explicit error handling in CryptoKey tests * Explicit dependency on project services in test keyring configs * Clean up comments in cryptokey resource * Do not repeat in cryptokey id regexes
- Loading branch information
1 parent
a6d50b0
commit 56d633a
Showing
7 changed files
with
763 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package google | ||
|
||
import ( | ||
"testing" | ||
|
||
"fmt" | ||
"github.com/hashicorp/terraform/helper/acctest" | ||
"github.com/hashicorp/terraform/helper/resource" | ||
"os" | ||
) | ||
|
||
func TestAccGoogleKmsCryptoKey_importBasic(t *testing.T) { | ||
t.Parallel() | ||
|
||
skipIfEnvNotSet(t, | ||
[]string{ | ||
"GOOGLE_ORG", | ||
"GOOGLE_BILLING_ACCOUNT", | ||
}..., | ||
) | ||
|
||
resourceName := "google_kms_crypto_key.crypto_key" | ||
|
||
projectId := "terraform-" + acctest.RandString(10) | ||
projectOrg := os.Getenv("GOOGLE_ORG") | ||
projectBillingAccount := os.Getenv("GOOGLE_BILLING_ACCOUNT") | ||
keyRingName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) | ||
cryptoKeyName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) | ||
|
||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
Steps: []resource.TestStep{ | ||
resource.TestStep{ | ||
Config: testGoogleKmsCryptoKey_basic(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName), | ||
}, | ||
|
||
resource.TestStep{ | ||
ResourceName: resourceName, | ||
ImportState: true, | ||
ImportStateVerify: true, | ||
}, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
package google | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform/helper/schema" | ||
"google.golang.org/api/cloudkms/v1" | ||
) | ||
|
||
func resourceKmsCryptoKey() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceKmsCryptoKeyCreate, | ||
Read: resourceKmsCryptoKeyRead, | ||
Delete: resourceKmsCryptoKeyDelete, | ||
Importer: &schema.ResourceImporter{ | ||
State: schema.ImportStatePassthrough, | ||
}, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"name": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
}, | ||
"key_ring": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
}, | ||
"rotation_period": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
ForceNew: true, | ||
ValidateFunc: validateKmsCryptoKeyRotationPeriod, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
type kmsCryptoKeyId struct { | ||
KeyRingId kmsKeyRingId | ||
Name string | ||
} | ||
|
||
func (s *kmsCryptoKeyId) cryptoKeyId() string { | ||
return fmt.Sprintf("%s/cryptoKeys/%s", s.KeyRingId.keyRingId(), s.Name) | ||
} | ||
|
||
func (s *kmsCryptoKeyId) parentId() string { | ||
return s.KeyRingId.keyRingId() | ||
} | ||
|
||
func (s *kmsCryptoKeyId) terraformId() string { | ||
return fmt.Sprintf("%s/%s", s.KeyRingId.terraformId(), s.Name) | ||
} | ||
|
||
func resourceKmsCryptoKeyCreate(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*Config) | ||
|
||
keyRingId, err := parseKmsKeyRingId(d.Get("key_ring").(string), config) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
cryptoKeyId := &kmsCryptoKeyId{ | ||
KeyRingId: *keyRingId, | ||
Name: d.Get("name").(string), | ||
} | ||
|
||
key := cloudkms.CryptoKey{Purpose: "ENCRYPT_DECRYPT"} | ||
|
||
if d.Get("rotation_period") != "" { | ||
rotationPeriod := d.Get("rotation_period").(string) | ||
nextRotation, err := kmsCryptoKeyNextRotation(time.Now(), rotationPeriod) | ||
|
||
if err != nil { | ||
return fmt.Errorf("Error setting CryptoKey rotation period: %s", err.Error()) | ||
} | ||
|
||
key.NextRotationTime = nextRotation | ||
key.RotationPeriod = rotationPeriod | ||
} | ||
|
||
cryptoKey, err := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.Create(cryptoKeyId.KeyRingId.keyRingId(), &key).CryptoKeyId(cryptoKeyId.Name).Do() | ||
|
||
if err != nil { | ||
return fmt.Errorf("Error creating CryptoKey: %s", err.Error()) | ||
} | ||
|
||
log.Printf("[DEBUG] Created CryptoKey %s", cryptoKey.Name) | ||
|
||
d.SetId(cryptoKeyId.terraformId()) | ||
|
||
return resourceKmsCryptoKeyRead(d, meta) | ||
} | ||
|
||
func resourceKmsCryptoKeyRead(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*Config) | ||
|
||
cryptoKeyId, err := parseKmsCryptoKeyId(d.Id(), config) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
log.Printf("[DEBUG] Executing read for KMS CryptoKey %s", cryptoKeyId.cryptoKeyId()) | ||
|
||
if err != nil { | ||
return fmt.Errorf("Error reading CryptoKey: %s", err) | ||
} | ||
d.Set("key_ring", cryptoKeyId.KeyRingId.terraformId()) | ||
d.Set("name", cryptoKeyId.Name) | ||
d.Set("rotation_period", d.Get("rotation_period")) | ||
|
||
d.SetId(cryptoKeyId.terraformId()) | ||
|
||
return nil | ||
} | ||
|
||
func clearCryptoKeyVersions(cryptoKeyId *kmsCryptoKeyId, config *Config) error { | ||
versionsClient := config.clientKms.Projects.Locations.KeyRings.CryptoKeys.CryptoKeyVersions | ||
|
||
versionsResponse, err := versionsClient.List(cryptoKeyId.cryptoKeyId()).Do() | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, version := range versionsResponse.CryptoKeyVersions { | ||
request := &cloudkms.DestroyCryptoKeyVersionRequest{} | ||
_, err = versionsClient.Destroy(version.Name, request).Do() | ||
|
||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
/* | ||
Because KMS CryptoKey resources cannot be deleted on GCP, we are only going to remove it from state | ||
and destroy all its versions, rendering the key useless for encryption and decryption of data. | ||
Re-creation of this resource through Terraform will produce an error. | ||
*/ | ||
|
||
func resourceKmsCryptoKeyDelete(d *schema.ResourceData, meta interface{}) error { | ||
config := meta.(*Config) | ||
|
||
cryptoKeyId, err := parseKmsCryptoKeyId(d.Id(), config) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
log.Printf(` | ||
[WARNING] KMS CryptoKey resources cannot be deleted from GCP. The CryptoKey %s will be removed from Terraform state, | ||
and all its CryptoKeyVersions will be destroyed, but it will still be present on the server.`, cryptoKeyId.cryptoKeyId()) | ||
|
||
err = clearCryptoKeyVersions(cryptoKeyId, config) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
d.SetId("") | ||
return nil | ||
} | ||
|
||
func validateKmsCryptoKeyRotationPeriod(value interface{}, _ string) (ws []string, errors []error) { | ||
period := value.(string) | ||
pattern := regexp.MustCompile("^([0-9.]*\\d)s$") | ||
match := pattern.FindStringSubmatch(period) | ||
|
||
if len(match) == 0 { | ||
errors = append(errors, fmt.Errorf("Invalid period format: %s", period)) | ||
} | ||
|
||
number := match[1] | ||
seconds, err := strconv.ParseFloat(number, 64) | ||
|
||
if err != nil { | ||
errors = append(errors, err) | ||
} else { | ||
if seconds < 86400.0 { | ||
errors = append(errors, fmt.Errorf("Rotation period must be greater than one day")) | ||
} | ||
|
||
parts := strings.Split(number, ".") | ||
|
||
if len(parts) > 1 && len(parts[1]) > 9 { | ||
errors = append(errors, fmt.Errorf("Rotation period cannot have more than 9 fractional digits")) | ||
} | ||
} | ||
|
||
return | ||
} | ||
|
||
func kmsCryptoKeyNextRotation(now time.Time, period string) (result string, err error) { | ||
var duration time.Duration | ||
|
||
duration, err = time.ParseDuration(period) | ||
|
||
if err == nil { | ||
result = now.UTC().Add(duration).Format(time.RFC3339Nano) | ||
} | ||
|
||
return | ||
} | ||
|
||
func parseKmsCryptoKeyId(id string, config *Config) (*kmsCryptoKeyId, error) { | ||
parts := strings.Split(id, "/") | ||
|
||
cryptoKeyIdRegex := regexp.MustCompile("^([a-z0-9-]+)/([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})/([a-zA-Z0-9_-]{1,63})$") | ||
cryptoKeyIdWithoutProjectRegex := regexp.MustCompile("^([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})/([a-zA-Z0-9_-]{1,63})$") | ||
|
||
if cryptoKeyIdRegex.MatchString(id) { | ||
return &kmsCryptoKeyId{ | ||
KeyRingId: kmsKeyRingId{ | ||
Project: parts[0], | ||
Location: parts[1], | ||
Name: parts[2], | ||
}, | ||
Name: parts[3], | ||
}, nil | ||
} | ||
|
||
if cryptoKeyIdWithoutProjectRegex.MatchString(id) { | ||
if config.Project == "" { | ||
return nil, fmt.Errorf("The default project for the provider must be set when using the `{location}/{keyRingName}/{cryptoKeyName}` id format.") | ||
} | ||
|
||
return &kmsCryptoKeyId{ | ||
KeyRingId: kmsKeyRingId{ | ||
Project: config.Project, | ||
Location: parts[0], | ||
Name: parts[1], | ||
}, | ||
Name: parts[2], | ||
}, nil | ||
} | ||
|
||
return nil, fmt.Errorf("Invalid CryptoKey id format, expecting `{projectId}/{locationId}/{KeyringName}/{cryptoKeyName}` or `{locationId}/{keyRingName}/{cryptoKeyName}.`") | ||
} |
Oops, something went wrong.