Skip to content
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

Adds support for creating KMS CryptoKeys resources #692

Merged
merged 29 commits into from
Nov 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
eb497da
Adds support for creating KMS CryptoKeys resources
amfarrell Nov 3, 2017
b493bac
Destroy extant CryptoKeyVersions on CryptoKey destroy
Nov 7, 2017
9de9eff
Inherit project, location etc from KeyRing in CryptoKey
Nov 7, 2017
7ac65dc
Add function to calculate next rotation
Nov 8, 2017
0aa931e
Implement RotationPeriod parameter on CryptoKey
Nov 8, 2017
1450129
Import CryptoKey state
Nov 8, 2017
58ada43
Uncommit my local acceptance test hacks
Nov 8, 2017
5da8276
Docs for google_kms_crypto_key
Nov 8, 2017
4d5f9bf
Clear id at the end of CryptoKey deletion
Nov 9, 2017
42f6f24
Fix parseCryptoKeyId error messages
Nov 9, 2017
e21d734
Use correct naming in CryptoKeyIdParsing test
Nov 9, 2017
1ff44c3
Check RotationPeriod is present in acceptance test
Nov 9, 2017
7faf003
Rename variable in test function for consistency
Nov 9, 2017
d9ebafa
Fix wrong resource name in cryptokey docs
Nov 9, 2017
cde0e24
Add KeyRing to CryptoKey doc example
Nov 9, 2017
0b49730
Run test CryptoKey configs through terraform fmt
Nov 9, 2017
41eca4b
Don't set CryptoKey purpose in terraform state on import
Nov 9, 2017
3ea4b8a
Fix indentation in CryptoKey test
Nov 9, 2017
44ec6d6
Parallelise CryptoKey tests
Nov 10, 2017
d361401
Set rotation_key on CryptoKey read
Nov 10, 2017
adb4d54
Move RotationPeriod validation to planning phase
Nov 10, 2017
56bb79d
Use import state passthrough for CryptoKey
Nov 10, 2017
f78800b
Correct casing issues in test case names
Nov 10, 2017
dfe7413
Remove redundant CheckDestroy calls in CryptoKey tests
Nov 10, 2017
9f1857a
Add explanatory comment about extra test steps
Nov 10, 2017
18b4791
More explicit error handling in CryptoKey tests
Nov 10, 2017
90a04a4
Explicit dependency on project services in test keyring configs
Nov 10, 2017
345e299
Clean up comments in cryptokey resource
Nov 14, 2017
d2e13b2
Do not repeat in cryptokey id regexes
Nov 14, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add t.Parallel() (and then a newline) to the beginning of all tests?

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this comment has mismatched tabs/spaces for indentation, mind choosing one or the other?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

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