Adds support for creating KMS KeyRing resources (#518)

* Instantiate the cloudkms client

* Implement Create and Read for the kms key ring resource

* Expose the kms key ring resource

* Create acceptance test for creating a KeyRing, fix read to use KeyRing ID

* Add cloudkms library to vendor

* Address style comments

* Use fully-qualified keyring name in read operation

* Remove call to SetId during read operation

* Set ID as entire resource string

* Spin up a new project for acceptance test

* Use Getenv for billing and org environment variables

* And test and logs around removal from state

* Add comments

* Fixes formatting

* Log warning instead of info

* Use a single line for cloudkms client actions

* Add resource import test

* Add ability to import resource, update helper functions to use keyRingId struct

* Use shorter terraform ID for easier import

* Update import test to use the same config as the basic test

* Update KeyRing name regex to be consistent with API docs

* Add documentation page for resource

* Add KeyRing documentation to sidebar

* Adds unit tests around parsing the KeyRing import id

* Allow for project in id to be autopopulated from config

* Throw error in import if project provider is not provided for location/name format

* Consistent variable names

* Use tabs in resource config instead of spaces

* Remove "-x" suffix for docs

* Set project attribute on import if different from the project config
This commit is contained in:
Michael Parker 2017-10-27 11:40:01 -05:00 committed by Dana Hoffman
parent 548d572bc3
commit f2fc78d082
10 changed files with 6696 additions and 0 deletions

View File

@ -18,6 +18,7 @@ import (
"golang.org/x/oauth2/jwt"
"google.golang.org/api/bigquery/v2"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudkms/v1"
"google.golang.org/api/cloudresourcemanager/v1"
resourceManagerV2Beta1 "google.golang.org/api/cloudresourcemanager/v2beta1"
computeBeta "google.golang.org/api/compute/v0.beta"
@ -47,6 +48,7 @@ type Config struct {
clientComputeBeta *computeBeta.Service
clientContainer *container.Service
clientDns *dns.Service
clientKms *cloudkms.Service
clientLogging *cloudlogging.Service
clientPubsub *pubsub.Service
clientResourceManager *cloudresourcemanager.Service
@ -155,6 +157,13 @@ func (c *Config) loadAndValidate() error {
}
c.clientDns.UserAgent = userAgent
log.Printf("[INFO] Instantiating Google Cloud KMS Client...")
c.clientKms, err = cloudkms.New(client)
if err != nil {
return err
}
c.clientKms.UserAgent = userAgent
log.Printf("[INFO] Instantiating Google Stackdriver Logging client...")
c.clientLogging, err = cloudlogging.New(client)
if err != nil {

View File

@ -0,0 +1,42 @@
package google
import (
"testing"
"fmt"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"os"
)
func TestAccGoogleKmsKeyRing_importBasic(t *testing.T) {
skipIfEnvNotSet(t,
[]string{
"GOOGLE_ORG",
"GOOGLE_BILLING_ACCOUNT",
}...,
)
resourceName := "google_kms_key_ring.key_ring"
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))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testGoogleKmsKeyRing_basic(projectId, projectOrg, projectBillingAccount, keyRingName),
},
resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

View File

@ -116,6 +116,7 @@ func Provider() terraform.ResourceProvider {
"google_logging_billing_account_sink": resourceLoggingBillingAccountSink(),
"google_logging_folder_sink": resourceLoggingFolderSink(),
"google_logging_project_sink": resourceLoggingProjectSink(),
"google_kms_key_ring": resourceKmsKeyRing(),
"google_sourcerepo_repository": resourceSourceRepoRepository(),
"google_spanner_instance": resourceSpannerInstance(),
"google_spanner_database": resourceSpannerDatabase(),

View File

@ -0,0 +1,172 @@
package google
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/cloudkms/v1"
"log"
"regexp"
"strings"
)
func resourceKmsKeyRing() *schema.Resource {
return &schema.Resource{
Create: resourceKmsKeyRingCreate,
Read: resourceKmsKeyRingRead,
Delete: resourceKmsKeyRingDelete,
Importer: &schema.ResourceImporter{
State: resourceKmsKeyRingImportState,
},
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"location": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}
type kmsKeyRingId struct {
Project string
Location string
Name string
}
func (s *kmsKeyRingId) keyRingId() string {
return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", s.Project, s.Location, s.Name)
}
func (s *kmsKeyRingId) parentId() string {
return fmt.Sprintf("projects/%s/locations/%s", s.Project, s.Location)
}
func (s *kmsKeyRingId) terraformId() string {
return fmt.Sprintf("%s/%s/%s", s.Project, s.Location, s.Name)
}
func resourceKmsKeyRingCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
keyRingId := &kmsKeyRingId{
Project: project,
Location: d.Get("location").(string),
Name: d.Get("name").(string),
}
keyRing, err := config.clientKms.Projects.Locations.KeyRings.Create(keyRingId.parentId(), &cloudkms.KeyRing{}).KeyRingId(keyRingId.Name).Do()
if err != nil {
return fmt.Errorf("Error creating KeyRing: %s", err)
}
log.Printf("[DEBUG] Created KeyRing %s", keyRing.Name)
d.SetId(keyRingId.terraformId())
return resourceKmsKeyRingRead(d, meta)
}
func resourceKmsKeyRingRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
keyRingId, err := parseKmsKeyRingId(d.Id(), config)
if err != nil {
return err
}
log.Printf("[DEBUG] Executing read for KMS KeyRing %s", keyRingId.keyRingId())
_, err = config.clientKms.Projects.Locations.KeyRings.Get(keyRingId.keyRingId()).Do()
if err != nil {
return fmt.Errorf("Error reading KeyRing: %s", err)
}
return nil
}
/*
Because KMS KeyRing resources cannot be deleted on GCP, we are only going to remove it from state.
Re-creation of this resource through Terraform will produce an error.
*/
func resourceKmsKeyRingDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
keyRingId, err := parseKmsKeyRingId(d.Id(), config)
if err != nil {
return err
}
log.Printf("[WARNING] KMS KeyRing resources cannot be deleted from GCP. This KeyRing %s will be removed from Terraform state, but will still be present on the server.", keyRingId.keyRingId())
d.SetId("")
return nil
}
func parseKmsKeyRingId(id string, config *Config) (*kmsKeyRingId, error) {
parts := strings.Split(id, "/")
keyRingIdRegex := regexp.MustCompile("^([a-z0-9-]+)/([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})$")
keyRingIdWithoutProjectRegex := regexp.MustCompile("^([a-z0-9-])+/([a-zA-Z0-9_-]{1,63})$")
if keyRingIdRegex.MatchString(id) {
return &kmsKeyRingId{
Project: parts[0],
Location: parts[1],
Name: parts[2],
}, nil
}
if keyRingIdWithoutProjectRegex.MatchString(id) {
if config.Project == "" {
return nil, fmt.Errorf("The default project for the provider must be set when using the `{location}/{keyRingName}` id format.")
}
return &kmsKeyRingId{
Project: config.Project,
Location: parts[0],
Name: parts[1],
}, nil
}
return nil, fmt.Errorf("Invalid KeyRing id format, expecting `{projectId}/{locationId}/{keyRingName}` or `{locationId}/{keyRingName}.`")
}
func resourceKmsKeyRingImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
config := meta.(*Config)
keyRingId, err := parseKmsKeyRingId(d.Id(), config)
if err != nil {
return nil, err
}
d.Set("name", keyRingId.Name)
d.Set("location", keyRingId.Location)
if config.Project != keyRingId.Project {
d.Set("project", keyRingId.Project)
}
d.SetId(keyRingId.terraformId())
return []*schema.ResourceData{d}, nil
}

View File

@ -0,0 +1,196 @@
package google
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"log"
"os"
)
func TestKeyRingIdParsing(t *testing.T) {
cases := map[string]struct {
ImportId string
ExpectedError bool
ExpectedTerraformId string
ExpectedKeyRingId string
Config *Config
}{
"id is in project/location/keyRingName format": {
ImportId: "test-project/us-central1/test-key-ring",
ExpectedError: false,
ExpectedTerraformId: "test-project/us-central1/test-key-ring",
ExpectedKeyRingId: "projects/test-project/locations/us-central1/keyRings/test-key-ring",
},
"id contains name that is longer than 63 characters": {
ImportId: "test-project/us-central1/can-you-believe-that-this-key-ring-name-is-exactly-64-characters",
ExpectedError: true,
},
"id is in location/keyRingName format": {
ImportId: "us-central1/test-key-ring",
ExpectedError: false,
ExpectedTerraformId: "test-project/us-central1/test-key-ring",
ExpectedKeyRingId: "projects/test-project/locations/us-central1/keyRings/test-key-ring",
Config: &Config{Project: "test-project"},
},
"id is in location/keyRingName format without project in config": {
ImportId: "us-central1/test-key-ring",
ExpectedError: true,
Config: &Config{Project: ""},
},
}
for tn, tc := range cases {
keyRingId, err := parseKmsKeyRingId(tc.ImportId, tc.Config)
if tc.ExpectedError && err == nil {
t.Fatalf("bad: %s, expected an error", tn)
}
if err != nil {
if tc.ExpectedError {
continue
}
t.Fatalf("bad: %s, err: %#v", tn, err)
}
if keyRingId.terraformId() != tc.ExpectedTerraformId {
t.Fatalf("bad: %s, expected Terraform ID to be `%s` but is `%s`", tn, tc.ExpectedTerraformId, keyRingId.terraformId())
}
if keyRingId.keyRingId() != tc.ExpectedKeyRingId {
t.Fatalf("bad: %s, expected KeyRing ID to be `%s` but is `%s`", tn, tc.ExpectedKeyRingId, keyRingId.keyRingId())
}
}
}
func TestAccGoogleKmsKeyRing_basic(t *testing.T) {
skipIfEnvNotSet(t,
[]string{
"GOOGLE_ORG",
"GOOGLE_BILLING_ACCOUNT",
}...,
)
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))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckGoogleKmsKeyRingWasRemovedFromState("google_kms_key_ring.key_ring"),
Steps: []resource.TestStep{
resource.TestStep{
Config: testGoogleKmsKeyRing_basic(projectId, projectOrg, projectBillingAccount, keyRingName),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleKmsKeyRingExists("google_kms_key_ring.key_ring"),
),
},
resource.TestStep{
Config: testGoogleKmsKeyRing_removed(projectId, projectOrg, projectBillingAccount),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleKmsKeyRingWasRemovedFromState("google_kms_key_ring.key_ring"),
),
},
},
})
}
func testAccCheckGoogleKmsKeyRingExists(resourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("Resource not found: %s", resourceName)
}
keyRingId := &kmsKeyRingId{
Project: rs.Primary.Attributes["project"],
Location: rs.Primary.Attributes["location"],
Name: rs.Primary.Attributes["name"],
}
listKeyRingsResponse, err := config.clientKms.Projects.Locations.KeyRings.List(keyRingId.parentId()).Do()
if err != nil {
return fmt.Errorf("Error listing KeyRings: %s", err)
}
for _, keyRing := range listKeyRingsResponse.KeyRings {
log.Printf("[DEBUG] Found KeyRing: %s", keyRing.Name)
if keyRing.Name == keyRingId.keyRingId() {
return nil
}
}
return fmt.Errorf("KeyRing not found: %s", keyRingId.keyRingId())
}
}
/*
KMS KeyRings cannot be deleted. This ensures that the KeyRing resource was removed from state,
even though the server-side resource was not removed.
*/
func testAccCheckGoogleKmsKeyRingWasRemovedFromState(resourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[resourceName]
if ok {
return fmt.Errorf("Resource was not removed from state: %s", resourceName)
}
return nil
}
}
/*
This test runs in its own project, otherwise the test project would start to get filled
with undeletable resources
*/
func testGoogleKmsKeyRing_basic(projectId, projectOrg, projectBillingAccount, keyRingName string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
name = "%s"
project_id = "%s"
org_id = "%s"
billing_account = "%s"
}
resource "google_project_services" "acceptance" {
project = "${google_project.acceptance.project_id}"
services = [
"cloudkms.googleapis.com"
]
}
resource "google_kms_key_ring" "key_ring" {
project = "${google_project_services.acceptance.project}"
name = "%s"
location = "us-central1"
}
`, projectId, projectId, projectOrg, projectBillingAccount, keyRingName)
}
func testGoogleKmsKeyRing_removed(projectId, projectOrg, projectBillingAccount string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
name = "%s"
project_id = "%s"
org_id = "%s"
billing_account = "%s"
}
resource "google_project_services" "acceptance" {
project = "${google_project.acceptance.project_id}"
services = [
"cloudkms.googleapis.com"
]
}
`, projectId, projectId, projectOrg, projectBillingAccount)
}

File diff suppressed because it is too large Load Diff

4853
vendor/google.golang.org/api/cloudkms/v1/cloudkms-gen.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

6
vendor/vendor.json vendored
View File

@ -975,6 +975,12 @@
"revision": "c8d75a8ec737f9b8b1ed2676c28feedbe21f543f",
"revisionTime": "2016-11-21T18:05:46Z"
},
{
"checksumSHA1": "/cYz9AiKwLEO61OdoLnKi2uiOeQ=",
"path": "google.golang.org/api/cloudkms/v1",
"revision": "654f863362977d69086620b5f72f13e911da2410",
"revisionTime": "2017-09-14T00:03:44Z"
},
{
"checksumSHA1": "9mUF072lBQeUk8vYCGZt/+KCRTI=",
"path": "google.golang.org/api/cloudresourcemanager/v1",

View File

@ -0,0 +1,61 @@
---
layout: "google"
page_title: "Google: google_kms_key_ring"
sidebar_current: "docs-google-kms-key-ring"
description: |-
Allows creation of a Google Cloud Platform KMS KeyRing.
---
# google\_kms\_key\_ring
Allows creation of a Google Cloud Platform KMS KeyRing. For more information see
[the official documentation](https://cloud.google.com/kms/docs/object-hierarchy#keyring)
and
[API](https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings).
A KeyRing is a grouping of CryptoKeys for organizational purposes. A KeyRing belongs to a Google Cloud Platform Project
and resides in a specific location.
~> Note: KeyRings cannot be deleted from Google Cloud Platform. Destroying a Terraform-managed KeyRing will remove it
from state but **will not delete the resource on the server**.
## Example Usage
```hcl
resource "google_kms_key_ring" "my_key_ring" {
name = "my-key-ring"
location = "us-central1"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The KeyRing's name.
A KeyRings name must be unique within a location and match the regular expression `[a-zA-Z0-9_-]{1,63}`
* `location` - (Required) The Google Cloud Platform location for the KeyRing.
A full list of valid locations can be found by running `gcloud kms locations list`.
- - -
* `project` - (Optional) The project in which the resource belongs. If it
is not provided, the provider project is used.
## Attributes Reference
In addition to the arguments listed above, the following computed attributes are
exported:
* `id` - The ID of the created KeyRing. Its format is `{projectId}/{location}/{keyRingName}`.
## Import
KeyRings can be imported using the KeyRing autogenerated `id`, e.g.
```
$ terraform import google_kms_key_ring.my_key_ring my-gcp-project/us-central1/my-key-ring
$ terraform import google_kms_key_ring.my_key_ring us-central1/my-key-ring
```

View File

@ -77,6 +77,9 @@
<li<%= sidebar_current("docs-google-folder-iam-policy") %>>
<a href="/docs/providers/google/r/google_folder_iam_policy.html">google_folder_iam_policy</a>
</li>
<li<%= sidebar_current("docs-google-kms-key-ring") %>>
<a href="/docs/providers/google/r/google_kms_key_ring.html">google_kms_key_ring</a>
</li>
<li<%= sidebar_current("docs-google-organization-policy") %>>
<a href="/docs/providers/google/r/google_organization_policy.html">google_organization_policy</a>
</li>