Skip to content

Commit a1e15b7

Browse files
authored
Unblock EXTERNAL/EXTERNAL_VPC Cloud KMS key creation. (#9931)
1 parent 90972e3 commit a1e15b7

File tree

4 files changed

+267
-0
lines changed

4 files changed

+267
-0
lines changed

mmv1/products/kms/CryptoKey.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,10 @@ properties:
156156
description: |
157157
Whether this key may contain imported versions only.
158158
default_from_api: true
159+
- !ruby/object:Api::Type::String
160+
name: 'cryptoKeyBackend'
161+
immutable: true
162+
description: |
163+
The resource name of the backend environment associated with all CryptoKeyVersions within this CryptoKey.
164+
The resource name is in the format "projects/*/locations/*/ekmConnections/*" and only applies to "EXTERNAL_VPC" keys.
165+
default_from_api: true

mmv1/products/kms/CryptoKeyVersion.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ examples:
3939
custom_code: !ruby/object:Provider::Terraform::CustomCode
4040
custom_delete: templates/terraform/custom_delete/kms_crypto_key_version.erb
4141
custom_import: templates/terraform/custom_import/kms_crypto_key_version.go.erb
42+
pre_update: templates/terraform/pre_update/kms_crypto_key_version.go.erb
4243
parameters:
4344
- !ruby/object:Api::Type::String
4445
name: 'cryptoKey'
@@ -123,6 +124,9 @@ properties:
123124
name: 'externalProtectionLevelOptions'
124125
description: |
125126
ExternalProtectionLevelOptions stores a group of additional fields for configuring a CryptoKeyVersion that are specific to the EXTERNAL protection level and EXTERNAL_VPC protection levels.
127+
deprecation_message: >-
128+
`externalProtectionLevelOptions` is being un-nested from the `attestation` field.
129+
Please use the top level `externalProtectionLevelOptions` field instead.
126130
properties:
127131
- !ruby/object:Api::Type::String
128132
name: 'externalKeyUri'
@@ -132,3 +136,16 @@ properties:
132136
name: 'ekmConnectionKeyPath'
133137
description: |
134138
The path to the external key material on the EKM when using EkmConnection e.g., "v0/my/key". Set this field instead of externalKeyUri when using an EkmConnection.
139+
- !ruby/object:Api::Type::NestedObject
140+
name: 'externalProtectionLevelOptions'
141+
description: |
142+
ExternalProtectionLevelOptions stores a group of additional fields for configuring a CryptoKeyVersion that are specific to the EXTERNAL protection level and EXTERNAL_VPC protection levels.
143+
properties:
144+
- !ruby/object:Api::Type::String
145+
name: 'externalKeyUri'
146+
description: |
147+
The URI for an external resource that this CryptoKeyVersion represents.
148+
- !ruby/object:Api::Type::String
149+
name: 'ekmConnectionKeyPath'
150+
description: |
151+
The path to the external key material on the EKM when using EkmConnection e.g., "v0/my/key". Set this field instead of externalKeyUri when using an EkmConnection.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// The generated code does not support conditional update masks.
2+
newUpdateMask := []string{}
3+
if d.HasChange("state") {
4+
newUpdateMask = append(newUpdateMask, "state")
5+
}
6+
7+
// Validate updated fields based on protection level (EXTERNAL vs EXTERNAL_VPC)
8+
if d.HasChange("external_protection_level_options") {
9+
if d.Get("protection_level") == "EXTERNAL" {
10+
newUpdateMask = append(newUpdateMask, "externalProtectionLevelOptions.externalKeyUri")
11+
} else if d.Get("protection_level") == "EXTERNAL_VPC" {
12+
newUpdateMask = append(newUpdateMask, "externalProtectionLevelOptions.ekmConnectionKeyPath")
13+
}
14+
}
15+
// updateMask is a URL parameter but not present in the schema, so ReplaceVars
16+
// won't set it
17+
url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(newUpdateMask, ",")})
18+
if err != nil {
19+
return err
20+
}

mmv1/third_party/terraform/services/kms/resource_kms_crypto_key_test.go

+223
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,85 @@ func TestAccKmsCryptoKeyVersion_patch(t *testing.T) {
538538
})
539539
}
540540

541+
func TestAccKmsCryptoKeyVersion_externalProtectionLevelOptions(t *testing.T) {
542+
t.Parallel()
543+
544+
projectId := fmt.Sprintf("tf-test-%d", acctest.RandInt(t))
545+
projectOrg := envvar.GetTestOrgFromEnv(t)
546+
projectBillingAccount := envvar.GetTestBillingAccountFromEnv(t)
547+
keyRingName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
548+
cryptoKeyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
549+
keyUri := "data.google_secret_manager_secret_version.key_uri.secret_data"
550+
updatedKeyUri := "data.google_secret_manager_secret_version.key_uri_updated.secret_data"
551+
552+
acctest.VcrTest(t, resource.TestCase{
553+
PreCheck: func() { acctest.AccTestPreCheck(t) },
554+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
555+
Steps: []resource.TestStep{
556+
{
557+
Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptions(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, keyUri),
558+
},
559+
{
560+
ResourceName: "google_kms_crypto_key_version.crypto_key_version",
561+
ImportState: true,
562+
ImportStateVerify: true,
563+
ImportStateVerifyIgnore: []string{"labels", "terraform_labels"},
564+
},
565+
{
566+
Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptions(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, updatedKeyUri),
567+
},
568+
{
569+
ResourceName: "google_kms_crypto_key_version.crypto_key_version",
570+
ImportState: true,
571+
ImportStateVerify: true,
572+
ImportStateVerifyIgnore: []string{"labels", "terraform_labels"},
573+
},
574+
},
575+
})
576+
}
577+
578+
func TestAccKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(t *testing.T) {
579+
// This test relies on manual steps to set up the EkmConnection used for the
580+
// CryptoKeyVersion creation, which means we can't spin up a temporary project.
581+
// We also can't use bootstrapped keys because that would defeat the purpose of
582+
// this key creation test, so we skip this test for VCR to avoid KMS resource
583+
// accumulation in the TF test project (since KMS resources can't be deleted).
584+
acctest.SkipIfVcr(t)
585+
t.Parallel()
586+
587+
projectId := envvar.GetTestProjectFromEnv()
588+
keyRingName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
589+
cryptoKeyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
590+
ekmConnectionName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
591+
keyPath := "data.google_secret_manager_secret_version.key_path.secret_data"
592+
updatedKeyPath := "data.google_secret_manager_secret_version.key_path_updated.secret_data"
593+
594+
acctest.VcrTest(t, resource.TestCase{
595+
PreCheck: func() { acctest.AccTestPreCheck(t) },
596+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
597+
Steps: []resource.TestStep{
598+
{
599+
Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(projectId, keyRingName, cryptoKeyName, ekmConnectionName, keyPath),
600+
},
601+
{
602+
ResourceName: "google_kms_crypto_key_version.crypto_key_version",
603+
ImportState: true,
604+
ImportStateVerify: true,
605+
ImportStateVerifyIgnore: []string{"labels", "terraform_labels"},
606+
},
607+
{
608+
Config: testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(projectId, keyRingName, cryptoKeyName, ekmConnectionName, updatedKeyPath),
609+
},
610+
{
611+
ResourceName: "google_kms_crypto_key_version.crypto_key_version",
612+
ImportState: true,
613+
ImportStateVerify: true,
614+
ImportStateVerifyIgnore: []string{"labels", "terraform_labels"},
615+
},
616+
},
617+
})
618+
}
619+
541620
// This test runs in its own project, otherwise the test project would start to get filled
542621
// with undeletable resources
543622
func testGoogleKmsCryptoKey_basic(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName string) string {
@@ -953,3 +1032,147 @@ resource "google_kms_crypto_key_version" "crypto_key_version" {
9531032
}
9541033
`, projectId, projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, preventDestroy, state)
9551034
}
1035+
1036+
func testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptions(projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, keyUri string) string {
1037+
return fmt.Sprintf(`
1038+
resource "google_project" "acceptance" {
1039+
name = "%s"
1040+
project_id = "%s"
1041+
org_id = "%s"
1042+
billing_account = "%s"
1043+
}
1044+
1045+
resource "google_project_service" "acceptance" {
1046+
project = google_project.acceptance.project_id
1047+
service = "cloudkms.googleapis.com"
1048+
}
1049+
1050+
resource "google_kms_key_ring" "key_ring" {
1051+
project = google_project_service.acceptance.project
1052+
name = "%s"
1053+
location = "us-central1"
1054+
}
1055+
1056+
resource "google_kms_crypto_key" "crypto_key" {
1057+
name = "%s"
1058+
key_ring = google_kms_key_ring.key_ring.id
1059+
1060+
version_template {
1061+
algorithm = "EXTERNAL_SYMMETRIC_ENCRYPTION"
1062+
protection_level = "EXTERNAL"
1063+
}
1064+
1065+
labels = {
1066+
key = "value"
1067+
}
1068+
skip_initial_version_creation = true
1069+
}
1070+
1071+
data "google_secret_manager_secret_version" "key_uri" {
1072+
secret = "external-full-key-uri"
1073+
project = "315636579862"
1074+
}
1075+
data "google_secret_manager_secret_version" "key_uri_updated" {
1076+
secret = "external-full-key-uri-update-test"
1077+
project = "315636579862"
1078+
}
1079+
1080+
resource "google_kms_crypto_key_version" "crypto_key_version" {
1081+
crypto_key = google_kms_crypto_key.crypto_key.id
1082+
external_protection_level_options {
1083+
external_key_uri = %s
1084+
}
1085+
}
1086+
`, projectId, projectId, projectOrg, projectBillingAccount, keyRingName, cryptoKeyName, keyUri)
1087+
}
1088+
1089+
// EkmConnection setup and creation is based off of resource_kms_ekm_connection_test.go
1090+
func testGoogleKmsCryptoKeyVersion_externalProtectionLevelOptionsVpc(projectId, keyRingName, cryptoKeyName, ekmConnectionName, keyPath string) string {
1091+
return fmt.Sprintf(`
1092+
data "google_project" "vpc-project" {
1093+
project_id = "cloud-ekm-refekm-playground"
1094+
}
1095+
data "google_project" "project" {
1096+
project_id = "%s"
1097+
}
1098+
1099+
data "google_secret_manager_secret_version" "raw_der" {
1100+
secret = "playground-cert"
1101+
project = "315636579862"
1102+
}
1103+
data "google_secret_manager_secret_version" "hostname" {
1104+
secret = "external-uri"
1105+
project = "315636579862"
1106+
}
1107+
data "google_secret_manager_secret_version" "servicedirectoryservice" {
1108+
secret = "external-servicedirectoryservice"
1109+
project = "315636579862"
1110+
}
1111+
1112+
resource "google_project_iam_member" "add_sdviewer" {
1113+
project = data.google_project.vpc-project.number
1114+
role = "roles/servicedirectory.viewer"
1115+
member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-ekms.iam.gserviceaccount.com"
1116+
}
1117+
resource "google_project_iam_member" "add_pscAuthorizedService" {
1118+
project = data.google_project.vpc-project.number
1119+
role = "roles/servicedirectory.pscAuthorizedService"
1120+
member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-ekms.iam.gserviceaccount.com"
1121+
}
1122+
1123+
resource "google_kms_ekm_connection" "example-ekmconnection" {
1124+
name = "%s"
1125+
location = "us-central1"
1126+
key_management_mode = "MANUAL"
1127+
service_resolvers {
1128+
service_directory_service = data.google_secret_manager_secret_version.servicedirectoryservice.secret_data
1129+
hostname = data.google_secret_manager_secret_version.hostname.secret_data
1130+
server_certificates {
1131+
raw_der = data.google_secret_manager_secret_version.raw_der.secret_data
1132+
}
1133+
}
1134+
depends_on = [
1135+
google_project_iam_member.add_pscAuthorizedService,
1136+
google_project_iam_member.add_sdviewer
1137+
]
1138+
}
1139+
1140+
resource "google_kms_key_ring" "key_ring" {
1141+
project = data.google_project.project.project_id
1142+
name = "%s"
1143+
location = "us-central1"
1144+
}
1145+
1146+
resource "google_kms_crypto_key" "crypto_key" {
1147+
name = "%s"
1148+
key_ring = google_kms_key_ring.key_ring.id
1149+
1150+
version_template {
1151+
algorithm = "EXTERNAL_SYMMETRIC_ENCRYPTION"
1152+
protection_level = "EXTERNAL_VPC"
1153+
}
1154+
1155+
labels = {
1156+
key = "value"
1157+
}
1158+
crypto_key_backend = google_kms_ekm_connection.example-ekmconnection.id
1159+
skip_initial_version_creation = true
1160+
}
1161+
1162+
data "google_secret_manager_secret_version" "key_path" {
1163+
secret = "external-keypath"
1164+
project = "315636579862"
1165+
}
1166+
data "google_secret_manager_secret_version" "key_path_updated" {
1167+
secret = "external-keypath-update-test"
1168+
project = "315636579862"
1169+
}
1170+
1171+
resource "google_kms_crypto_key_version" "crypto_key_version" {
1172+
crypto_key = google_kms_crypto_key.crypto_key.id
1173+
external_protection_level_options {
1174+
ekm_connection_key_path = %s
1175+
}
1176+
}
1177+
`, projectId, ekmConnectionName, keyRingName, cryptoKeyName, keyPath)
1178+
}

0 commit comments

Comments
 (0)