Skip to content

Commit

Permalink
Adds support for creating KMS CryptoKeys resources (#692)
Browse files Browse the repository at this point in the history
* 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
amfarrell authored and danawillow committed Nov 14, 2017
1 parent a6d50b0 commit 56d633a
Show file tree
Hide file tree
Showing 7 changed files with 763 additions and 0 deletions.
Empty file added google/encryptor-pod.yml
Empty file.
45 changes: 45 additions & 0 deletions google/import_kms_crypto_key_test.go
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,
},
},
})
}
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func Provider() terraform.ResourceProvider {
"google_logging_folder_sink": resourceLoggingFolderSink(),
"google_logging_project_sink": resourceLoggingProjectSink(),
"google_kms_key_ring": resourceKmsKeyRing(),
"google_kms_crypto_key": resourceKmsCryptoKey(),
"google_sourcerepo_repository": resourceSourceRepoRepository(),
"google_spanner_instance": resourceSpannerInstance(),
"google_spanner_database": resourceSpannerDatabase(),
Expand Down
248 changes: 248 additions & 0 deletions google/resource_kms_crypto_key.go
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}.`")
}
Loading

0 comments on commit 56d633a

Please sign in to comment.