terraform-provider-google/google/resource_kms_crypto_key.go
Andrew Farrell 56d633a8d4 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
2017-11-14 13:21:25 -08:00

249 lines
6.3 KiB
Go

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}.`")
}