Skip to content

Commit ffc3213

Browse files
Add fields to compute instance template, image (#6919) (#13253)
* Add the source_snapshot, source_snapshot_encyption_key, and source_image_encryption_key fields to compute instance template * Add test for compute image encryption key * Added website documentation * Make kms_key_self_link fields required Signed-off-by: Modular Magician <[email protected]> Signed-off-by: Modular Magician <[email protected]>
1 parent f486fd5 commit ffc3213

9 files changed

+543
-13
lines changed

.changelog/6919.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:enhancement
2+
compute: added support for `image_encryption_key` to `google_compute_image`
3+
```
4+
```release-note:enhancement
5+
compute: added support for `source_snapshot`, `source_snapshot_encyption_key`, and `source_image_encryption_key` to `google_compute_instance_template`
6+
```

google/resource_compute_image.go

+102
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"log"
2020
"reflect"
21+
"strings"
2122
"time"
2223

2324
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -87,6 +88,37 @@ Applicable only for bootable images.`,
8788
Elem: computeImageGuestOsFeaturesSchema(),
8889
// Default schema.HashSchema is used.
8990
},
91+
"image_encryption_key": {
92+
Type: schema.TypeList,
93+
Optional: true,
94+
ForceNew: true,
95+
Description: `Encrypts the image using a customer-supplied encryption key.
96+
97+
After you encrypt an image with a customer-supplied key, you must
98+
provide the same key if you use the image later (e.g. to create a
99+
disk from the image)`,
100+
MaxItems: 1,
101+
Elem: &schema.Resource{
102+
Schema: map[string]*schema.Schema{
103+
"kms_key_self_link": {
104+
Type: schema.TypeString,
105+
Optional: true,
106+
ForceNew: true,
107+
DiffSuppressFunc: compareSelfLinkRelativePaths,
108+
Description: `The self link of the encryption key that is stored in Google Cloud
109+
KMS.`,
110+
},
111+
"kms_key_service_account": {
112+
Type: schema.TypeString,
113+
Optional: true,
114+
ForceNew: true,
115+
Description: `The service account being used for the encryption request for the
116+
given KMS key. If absent, the Compute Engine default service
117+
account is used.`,
118+
},
119+
},
120+
},
121+
},
90122
"labels": {
91123
Type: schema.TypeMap,
92124
Optional: true,
@@ -256,6 +288,12 @@ func resourceComputeImageCreate(d *schema.ResourceData, meta interface{}) error
256288
} else if v, ok := d.GetOkExists("guest_os_features"); !isEmptyValue(reflect.ValueOf(guestOsFeaturesProp)) && (ok || !reflect.DeepEqual(v, guestOsFeaturesProp)) {
257289
obj["guestOsFeatures"] = guestOsFeaturesProp
258290
}
291+
imageEncryptionKeyProp, err := expandComputeImageImageEncryptionKey(d.Get("image_encryption_key"), d, config)
292+
if err != nil {
293+
return err
294+
} else if v, ok := d.GetOkExists("image_encryption_key"); !isEmptyValue(reflect.ValueOf(imageEncryptionKeyProp)) && (ok || !reflect.DeepEqual(v, imageEncryptionKeyProp)) {
295+
obj["imageEncryptionKey"] = imageEncryptionKeyProp
296+
}
259297
labelsProp, err := expandComputeImageLabels(d.Get("labels"), d, config)
260298
if err != nil {
261299
return err
@@ -403,6 +441,9 @@ func resourceComputeImageRead(d *schema.ResourceData, meta interface{}) error {
403441
if err := d.Set("guest_os_features", flattenComputeImageGuestOsFeatures(res["guestOsFeatures"], d, config)); err != nil {
404442
return fmt.Errorf("Error reading Image: %s", err)
405443
}
444+
if err := d.Set("image_encryption_key", flattenComputeImageImageEncryptionKey(res["imageEncryptionKey"], d, config)); err != nil {
445+
return fmt.Errorf("Error reading Image: %s", err)
446+
}
406447
if err := d.Set("labels", flattenComputeImageLabels(res["labels"], d, config)); err != nil {
407448
return fmt.Errorf("Error reading Image: %s", err)
408449
}
@@ -627,6 +668,33 @@ func flattenComputeImageGuestOsFeaturesType(v interface{}, d *schema.ResourceDat
627668
return v
628669
}
629670

671+
func flattenComputeImageImageEncryptionKey(v interface{}, d *schema.ResourceData, config *Config) interface{} {
672+
if v == nil {
673+
return nil
674+
}
675+
original := v.(map[string]interface{})
676+
if len(original) == 0 {
677+
return nil
678+
}
679+
transformed := make(map[string]interface{})
680+
transformed["kms_key_self_link"] =
681+
flattenComputeImageImageEncryptionKeyKmsKeySelfLink(original["kmsKeyName"], d, config)
682+
transformed["kms_key_service_account"] =
683+
flattenComputeImageImageEncryptionKeyKmsKeyServiceAccount(original["kmsKeyServiceAccount"], d, config)
684+
return []interface{}{transformed}
685+
}
686+
func flattenComputeImageImageEncryptionKeyKmsKeySelfLink(v interface{}, d *schema.ResourceData, config *Config) interface{} {
687+
if v == nil {
688+
return v
689+
}
690+
vStr := v.(string)
691+
return strings.Split(vStr, "/cryptoKeyVersions/")[0]
692+
}
693+
694+
func flattenComputeImageImageEncryptionKeyKmsKeyServiceAccount(v interface{}, d *schema.ResourceData, config *Config) interface{} {
695+
return v
696+
}
697+
630698
func flattenComputeImageLabels(v interface{}, d *schema.ResourceData, config *Config) interface{} {
631699
return v
632700
}
@@ -706,6 +774,40 @@ func expandComputeImageGuestOsFeaturesType(v interface{}, d TerraformResourceDat
706774
return v, nil
707775
}
708776

777+
func expandComputeImageImageEncryptionKey(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
778+
l := v.([]interface{})
779+
if len(l) == 0 || l[0] == nil {
780+
return nil, nil
781+
}
782+
raw := l[0]
783+
original := raw.(map[string]interface{})
784+
transformed := make(map[string]interface{})
785+
786+
transformedKmsKeySelfLink, err := expandComputeImageImageEncryptionKeyKmsKeySelfLink(original["kms_key_self_link"], d, config)
787+
if err != nil {
788+
return nil, err
789+
} else if val := reflect.ValueOf(transformedKmsKeySelfLink); val.IsValid() && !isEmptyValue(val) {
790+
transformed["kmsKeyName"] = transformedKmsKeySelfLink
791+
}
792+
793+
transformedKmsKeyServiceAccount, err := expandComputeImageImageEncryptionKeyKmsKeyServiceAccount(original["kms_key_service_account"], d, config)
794+
if err != nil {
795+
return nil, err
796+
} else if val := reflect.ValueOf(transformedKmsKeyServiceAccount); val.IsValid() && !isEmptyValue(val) {
797+
transformed["kmsKeyServiceAccount"] = transformedKmsKeyServiceAccount
798+
}
799+
800+
return transformed, nil
801+
}
802+
803+
func expandComputeImageImageEncryptionKeyKmsKeySelfLink(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
804+
return v, nil
805+
}
806+
807+
func expandComputeImageImageEncryptionKeyKmsKeyServiceAccount(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) {
808+
return v, nil
809+
}
810+
709811
func expandComputeImageLabels(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) {
710812
if v == nil {
711813
return map[string]string{}, nil

google/resource_compute_image_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,30 @@ func TestAccComputeImage_resolveImage(t *testing.T) {
211211
})
212212
}
213213

214+
func TestAccComputeImage_imageEncryptionKey(t *testing.T) {
215+
t.Parallel()
216+
217+
kmsKey := BootstrapKMSKeyInLocation(t, "us-central1")
218+
kmsKeyName := GetResourceNameFromSelfLink(kmsKey.CryptoKey.Name)
219+
kmsRingName := GetResourceNameFromSelfLink(kmsKey.KeyRing.Name)
220+
221+
vcrTest(t, resource.TestCase{
222+
PreCheck: func() { testAccPreCheck(t) },
223+
Providers: testAccProviders,
224+
CheckDestroy: testAccCheckComputeInstanceTemplateDestroyProducer(t),
225+
Steps: []resource.TestStep{
226+
{
227+
Config: testAccComputeImage_imageEncryptionKey(kmsRingName, kmsKeyName, randString(t, 10)),
228+
},
229+
{
230+
ResourceName: "google_compute_image.image",
231+
ImportState: true,
232+
ImportStateVerify: true,
233+
},
234+
},
235+
})
236+
}
237+
214238
func testAccCheckComputeImageResolution(t *testing.T, n string) resource.TestCheckFunc {
215239
return func(s *terraform.State) error {
216240
config := googleProviderConfig(t)
@@ -450,3 +474,42 @@ resource "google_compute_image" "foobar" {
450474
}
451475
`, diskName, snapshotName, imageName)
452476
}
477+
478+
func testAccComputeImage_imageEncryptionKey(kmsRingName, kmsKeyName, suffix string) string {
479+
return fmt.Sprintf(`
480+
data "google_kms_key_ring" "ring" {
481+
name = "%s"
482+
location = "us-central1"
483+
}
484+
485+
data "google_kms_crypto_key" "key" {
486+
name = "%s"
487+
key_ring = data.google_kms_key_ring.ring.id
488+
}
489+
490+
resource "google_service_account" "test" {
491+
account_id = "tf-test-sa-%s"
492+
display_name = "KMS Ops Account"
493+
}
494+
495+
resource "google_kms_crypto_key_iam_member" "crypto_key" {
496+
crypto_key_id = data.google_kms_crypto_key.key.id
497+
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
498+
member = "serviceAccount:${google_service_account.test.email}"
499+
}
500+
501+
data "google_compute_image" "debian" {
502+
family = "debian-11"
503+
project = "debian-cloud"
504+
}
505+
506+
resource "google_compute_image" "image" {
507+
name = "tf-test-image-%s"
508+
source_image = data.google_compute_image.debian.self_link
509+
image_encryption_key {
510+
kms_key_self_link = data.google_kms_crypto_key.key.id
511+
kms_key_service_account = google_service_account.test.email
512+
}
513+
}
514+
`, kmsRingName, kmsKeyName, suffix, suffix)
515+
}

google/resource_compute_instance_template.go

+104-5
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,74 @@ func resourceComputeInstanceTemplate() *schema.Resource {
160160
ForceNew: true,
161161
Description: `The image from which to initialize this disk. This can be one of: the image's self_link, projects/{project}/global/images/{image}, projects/{project}/global/images/family/{family}, global/images/{image}, global/images/family/{family}, family/{family}, {project}/{family}, {project}/{image}, {family}, or {image}. ~> Note: Either source or source_image is required when creating a new instance except for when creating a local SSD.`,
162162
},
163+
"source_image_encryption_key": {
164+
Type: schema.TypeList,
165+
Optional: true,
166+
ForceNew: true,
167+
Description: `The customer-supplied encryption key of the source
168+
image. Required if the source image is protected by a
169+
customer-supplied encryption key.
170+
171+
Instance templates do not store customer-supplied
172+
encryption keys, so you cannot create disks for
173+
instances in a managed instance group if the source
174+
images are encrypted with your own keys.`,
175+
MaxItems: 1,
176+
Elem: &schema.Resource{
177+
Schema: map[string]*schema.Schema{
178+
"kms_key_service_account": {
179+
Type: schema.TypeString,
180+
Optional: true,
181+
ForceNew: true,
182+
Description: `The service account being used for the encryption
183+
request for the given KMS key. If absent, the Compute
184+
Engine default service account is used.`,
185+
},
186+
"kms_key_self_link": {
187+
Type: schema.TypeString,
188+
Required: true,
189+
ForceNew: true,
190+
Description: `The self link of the encryption key that is stored in
191+
Google Cloud KMS.`,
192+
},
193+
},
194+
},
195+
},
196+
"source_snapshot": {
197+
Type: schema.TypeString,
198+
Optional: true,
199+
ForceNew: true,
200+
Description: `The source snapshot to create this disk. When creating
201+
a new instance, one of initializeParams.sourceSnapshot,
202+
initializeParams.sourceImage, or disks.source is
203+
required except for local SSD.`,
204+
},
205+
"source_snapshot_encryption_key": {
206+
Type: schema.TypeList,
207+
Optional: true,
208+
ForceNew: true,
209+
Description: `The customer-supplied encryption key of the source snapshot.`,
210+
MaxItems: 1,
211+
Elem: &schema.Resource{
212+
Schema: map[string]*schema.Schema{
213+
"kms_key_service_account": {
214+
Type: schema.TypeString,
215+
Optional: true,
216+
ForceNew: true,
217+
Description: `The service account being used for the encryption
218+
request for the given KMS key. If absent, the Compute
219+
Engine default service account is used.`,
220+
},
221+
"kms_key_self_link": {
222+
Type: schema.TypeString,
223+
Required: true,
224+
ForceNew: true,
225+
Description: `The self link of the encryption key that is stored in
226+
Google Cloud KMS.`,
227+
},
228+
},
229+
},
230+
},
163231

164232
"interface": {
165233
Type: schema.TypeString,
@@ -886,7 +954,7 @@ func buildDisks(d *schema.ResourceData, config *Config) ([]*compute.AttachedDisk
886954

887955
if v, ok := d.GetOk(prefix + ".source"); ok {
888956
disk.Source = v.(string)
889-
conflicts := []string{"disk_size_gb", "disk_name", "disk_type", "source_image", "labels"}
957+
conflicts := []string{"disk_size_gb", "disk_name", "disk_type", "source_image", "source_snapshot", "labels"}
890958
for _, conflict := range conflicts {
891959
if _, ok := d.GetOk(prefix + "." + conflict); ok {
892960
return nil, fmt.Errorf("Cannot use `source` with any of the fields in %s", conflicts)
@@ -906,6 +974,8 @@ func buildDisks(d *schema.ResourceData, config *Config) ([]*compute.AttachedDisk
906974
disk.InitializeParams.DiskType = v.(string)
907975
}
908976

977+
disk.InitializeParams.Labels = expandStringMap(d, prefix+".labels")
978+
909979
if v, ok := d.GetOk(prefix + ".source_image"); ok {
910980
imageName := v.(string)
911981
imageUrl, err := resolveImage(config, project, imageName, userAgent)
@@ -917,7 +987,29 @@ func buildDisks(d *schema.ResourceData, config *Config) ([]*compute.AttachedDisk
917987
disk.InitializeParams.SourceImage = imageUrl
918988
}
919989

920-
disk.InitializeParams.Labels = expandStringMap(d, prefix+".labels")
990+
if _, ok := d.GetOk(prefix + ".source_image_encryption_key"); ok {
991+
disk.InitializeParams.SourceImageEncryptionKey = &compute.CustomerEncryptionKey{}
992+
if v, ok := d.GetOk(prefix + ".source_image_encryption_key.0.kms_key_self_link"); ok {
993+
disk.InitializeParams.SourceImageEncryptionKey.KmsKeyName = v.(string)
994+
}
995+
if v, ok := d.GetOk(prefix + ".source_image_encryption_key.0.kms_key_service_account"); ok {
996+
disk.InitializeParams.SourceImageEncryptionKey.KmsKeyServiceAccount = v.(string)
997+
}
998+
}
999+
1000+
if v, ok := d.GetOk(prefix + ".source_snapshot"); ok {
1001+
disk.InitializeParams.SourceSnapshot = v.(string)
1002+
}
1003+
1004+
if _, ok := d.GetOk(prefix + ".source_snapshot_encryption_key"); ok {
1005+
disk.InitializeParams.SourceSnapshotEncryptionKey = &compute.CustomerEncryptionKey{}
1006+
if v, ok := d.GetOk(prefix + ".source_snapshot_encryption_key.0.kms_key_self_link"); ok {
1007+
disk.InitializeParams.SourceSnapshotEncryptionKey.KmsKeyName = v.(string)
1008+
}
1009+
if v, ok := d.GetOk(prefix + ".source_snapshot_encryption_key.0.kms_key_service_account"); ok {
1010+
disk.InitializeParams.SourceSnapshotEncryptionKey.KmsKeyServiceAccount = v.(string)
1011+
}
1012+
}
9211013

9221014
if _, ok := d.GetOk(prefix + ".resource_policies"); ok {
9231015
// instance template only supports a resource name here (not uri)
@@ -1100,8 +1192,14 @@ func diskCharacteristicsFromMap(m map[string]interface{}) diskCharacteristics {
11001192
return dc
11011193
}
11021194

1103-
func flattenDisk(disk *compute.AttachedDisk, defaultProject string) (map[string]interface{}, error) {
1195+
func flattenDisk(disk *compute.AttachedDisk, configDisk map[string]any, defaultProject string) (map[string]interface{}, error) {
11041196
diskMap := make(map[string]interface{})
1197+
1198+
// These values are not returned by the API, so we copy them from the config.
1199+
diskMap["source_image_encryption_key"] = configDisk["source_image_encryption_key"]
1200+
diskMap["source_snapshot"] = configDisk["source_snapshot"]
1201+
diskMap["source_snapshot_encryption_key"] = configDisk["source_snapshot_encryption_key"]
1202+
11051203
if disk.InitializeParams != nil {
11061204
if disk.InitializeParams.SourceImage != "" {
11071205
path, err := resolveImageRefToRelativeURI(defaultProject, disk.InitializeParams.SourceImage)
@@ -1266,11 +1364,12 @@ func flattenDisks(disks []*compute.AttachedDisk, d *schema.ResourceData, default
12661364
apiDisks := make([]map[string]interface{}, len(disks))
12671365

12681366
for i, disk := range disks {
1269-
d, err := flattenDisk(disk, defaultProject)
1367+
configDisk := d.Get(fmt.Sprintf("disk.%d", i)).(map[string]any)
1368+
apiDisk, err := flattenDisk(disk, configDisk, defaultProject)
12701369
if err != nil {
12711370
return nil, err
12721371
}
1273-
apiDisks[i] = d
1372+
apiDisks[i] = apiDisk
12741373
}
12751374

12761375
return reorderDisks(d.Get("disk").([]interface{}), apiDisks), nil

0 commit comments

Comments
 (0)