From c0a718822566788bcbc960866f27948e60647fa0 Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Thu, 27 May 2021 23:09:00 +0000 Subject: [PATCH] Compute: add support for instance reservation_affinity (#4335) Signed-off-by: Modular Magician --- .changelog/4335.txt | 3 + google-beta/compute_instance_helpers.go | 51 +++++++ google-beta/resource_compute_instance.go | 54 +++++++ .../resource_compute_instance_template.go | 58 +++++++ ...resource_compute_instance_template_test.go | 144 ++++++++++++++++++ google-beta/resource_compute_instance_test.go | 142 +++++++++++++++++ google-beta/resource_dataproc_cluster_test.go | 3 +- 7 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 .changelog/4335.txt diff --git a/.changelog/4335.txt b/.changelog/4335.txt new file mode 100644 index 0000000000..d1984af28b --- /dev/null +++ b/.changelog/4335.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +compute: added `reservation_affinity` to `google_compute_instance` and `google_compute_instance_template` +``` diff --git a/google-beta/compute_instance_helpers.go b/google-beta/compute_instance_helpers.go index 2c9588ea91..7e441962a3 100644 --- a/google-beta/compute_instance_helpers.go +++ b/google-beta/compute_instance_helpers.go @@ -443,3 +443,54 @@ func hasNodeAffinitiesChanged(oScheduling, newScheduling map[string]interface{}) return false } + +func expandReservationAffinity(d *schema.ResourceData) (*computeBeta.ReservationAffinity, error) { + _, ok := d.GetOk("reservation_affinity") + if !ok { + return nil, nil + } + + prefix := "reservation_affinity.0" + reservationAffinityType := d.Get(prefix + ".type").(string) + + affinity := computeBeta.ReservationAffinity{ + ConsumeReservationType: reservationAffinityType, + ForceSendFields: []string{"ConsumeReservationType"}, + } + + _, hasSpecificReservation := d.GetOk(prefix + ".specific_reservation") + if (reservationAffinityType == "SPECIFIC_RESERVATION") != hasSpecificReservation { + return nil, fmt.Errorf("specific_reservation must be set when reservation_affinity is SPECIFIC_RESERVATION, and not set otherwise") + } + + prefix = prefix + ".specific_reservation.0" + if hasSpecificReservation { + affinity.Key = d.Get(prefix + ".key").(string) + affinity.ForceSendFields = append(affinity.ForceSendFields, "Key", "Values") + + for _, v := range d.Get(prefix + ".values").([]interface{}) { + affinity.Values = append(affinity.Values, v.(string)) + } + } + + return &affinity, nil +} + +func flattenReservationAffinity(affinity *computeBeta.ReservationAffinity) []map[string]interface{} { + if affinity == nil { + return nil + } + + flattened := map[string]interface{}{ + "type": affinity.ConsumeReservationType, + } + + if affinity.ConsumeReservationType == "SPECIFIC_RESERVATION" { + flattened["specific_reservation"] = []map[string]interface{}{{ + "key": affinity.Key, + "values": affinity.Values, + }} + } + + return []map[string]interface{}{flattened} +} diff --git a/google-beta/resource_compute_instance.go b/google-beta/resource_compute_instance.go index 39a2ce19f0..920e6ca621 100644 --- a/google-beta/resource_compute_instance.go +++ b/google-beta/resource_compute_instance.go @@ -737,6 +737,51 @@ func resourceComputeInstance() *schema.Resource { MaxItems: 1, Description: `A list of short names or self_links of resource policies to attach to the instance. Modifying this list will cause the instance to recreate. Currently a max of 1 resource policy is supported.`, }, + + "reservation_affinity": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Description: `Specifies the reservations that this instance can consume from.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"ANY_RESERVATION", "SPECIFIC_RESERVATION", "NO_RESERVATION"}, false), + Description: `The type of reservation from which this instance can consume resources.`, + }, + + "specific_reservation": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Description: `Specifies the label selector for the reservation to use.`, + + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Corresponds to the label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify compute.googleapis.com/reservation-name as the key and specify the name of your reservation as the only value.`, + }, + "values": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + ForceNew: true, + Description: `Corresponds to the label values of a reservation resource.`, + }, + }, + }, + }, + }, + }, + }, }, CustomizeDiff: customdiff.All( customdiff.If( @@ -855,6 +900,11 @@ func expandComputeInstance(project string, d *schema.ResourceData, config *Confi return nil, fmt.Errorf("Error creating guest accelerators: %s", err) } + reservationAffinity, err := expandReservationAffinity(d) + if err != nil { + return nil, fmt.Errorf("Error creating reservation affinity: %s", err) + } + // Create the instance information return &computeBeta.Instance{ CanIpForward: d.Get("can_ip_forward").(bool), @@ -877,6 +927,7 @@ func expandComputeInstance(project string, d *schema.ResourceData, config *Confi ShieldedInstanceConfig: expandShieldedVmConfigs(d), DisplayDevice: expandDisplayDevice(d), ResourcePolicies: convertStringArr(d.Get("resource_policies").([]interface{})), + ReservationAffinity: reservationAffinity, }, nil } @@ -1235,6 +1286,9 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error setting desired_status: %s", err) } } + if err := d.Set("reservation_affinity", flattenReservationAffinity(instance.ReservationAffinity)); err != nil { + return fmt.Errorf("Error setting reservation_affinity: %s", err) + } d.SetId(fmt.Sprintf("projects/%s/zones/%s/instances/%s", project, zone, instance.Name)) diff --git a/google-beta/resource_compute_instance_template.go b/google-beta/resource_compute_instance_template.go index b55191d51d..06b0074a7a 100644 --- a/google-beta/resource_compute_instance_template.go +++ b/google-beta/resource_compute_instance_template.go @@ -614,6 +614,51 @@ func resourceComputeInstanceTemplate() *schema.Resource { Set: schema.HashString, Description: `A set of key/value label pairs to assign to instances created from this template,`, }, + + "reservation_affinity": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Description: `Specifies the reservations that this instance can consume from.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"ANY_RESERVATION", "SPECIFIC_RESERVATION", "NO_RESERVATION"}, false), + Description: `The type of reservation from which this instance can consume resources.`, + }, + + "specific_reservation": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Description: `Specifies the label selector for the reservation to use.`, + + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Corresponds to the label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify compute.googleapis.com/reservation-name as the key and specify the name of your reservation as the only value.`, + }, + "values": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + ForceNew: true, + Description: `Corresponds to the label values of a reservation resource.`, + }, + }, + }, + }, + }, + }, + }, }, UseJSONNumber: true, } @@ -871,6 +916,11 @@ func resourceComputeInstanceTemplateCreate(d *schema.ResourceData, meta interfac return err } + reservationAffinity, err := expandReservationAffinity(d) + if err != nil { + return err + } + instanceProperties := &computeBeta.InstanceProperties{ CanIpForward: d.Get("can_ip_forward").(bool), Description: d.Get("instance_description").(string), @@ -886,6 +936,7 @@ func resourceComputeInstanceTemplateCreate(d *schema.ResourceData, meta interfac ConfidentialInstanceConfig: expandConfidentialInstanceConfig(d), ShieldedInstanceConfig: expandShieldedVmConfigs(d), DisplayDevice: expandDisplayDevice(d), + ReservationAffinity: reservationAffinity, } if _, ok := d.GetOk("labels"); ok { @@ -1280,6 +1331,13 @@ func resourceComputeInstanceTemplateRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("Error setting enable_display: %s", err) } } + + if reservationAffinity := instanceTemplate.Properties.ReservationAffinity; reservationAffinity != nil { + if err = d.Set("reservation_affinity", flattenReservationAffinity(reservationAffinity)); err != nil { + return fmt.Errorf("Error setting reservation_affinity: %s", err) + } + } + return nil } diff --git a/google-beta/resource_compute_instance_template_test.go b/google-beta/resource_compute_instance_template_test.go index b36e14e5e3..cbdbdaec4e 100644 --- a/google-beta/resource_compute_instance_template_test.go +++ b/google-beta/resource_compute_instance_template_test.go @@ -734,6 +734,57 @@ func TestAccComputeInstanceTemplate_soleTenantNodeAffinities(t *testing.T) { }) } +func TestAccComputeInstanceTemplate_reservationAffinities(t *testing.T) { + t.Parallel() + + var template computeBeta.InstanceTemplate + var templateName = randString(t, 10) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceTemplateDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeInstanceTemplate_reservationAffinityInstanceTemplate_nonSpecificReservation(templateName, "NO_RESERVATION"), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists(t, "google_compute_instance_template.foobar", &template), + testAccCheckComputeInstanceTemplateHasReservationAffinity(&template, "NO_RESERVATION"), + ), + }, + { + ResourceName: "google_compute_instance_template.foobar", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccComputeInstanceTemplate_reservationAffinityInstanceTemplate_nonSpecificReservation(templateName, "ANY_RESERVATION"), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists(t, "google_compute_instance_template.foobar", &template), + testAccCheckComputeInstanceTemplateHasReservationAffinity(&template, "ANY_RESERVATION"), + ), + }, + { + ResourceName: "google_compute_instance_template.foobar", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccComputeInstanceTemplate_reservationAffinityInstanceTemplate_specificReservation(templateName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists(t, "google_compute_instance_template.foobar", &template), + testAccCheckComputeInstanceTemplateHasReservationAffinity(&template, "SPECIFIC_RESERVATION", templateName), + ), + }, + { + ResourceName: "google_compute_instance_template.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccComputeInstanceTemplate_shieldedVmConfig1(t *testing.T) { t.Parallel() @@ -1264,6 +1315,36 @@ func testAccCheckComputeInstanceTemplateHasMinCpuPlatform(instanceTemplate *comp } } +func testAccCheckComputeInstanceTemplateHasReservationAffinity(instanceTemplate *computeBeta.InstanceTemplate, consumeReservationType string, specificReservationNames ...string) resource.TestCheckFunc { + if len(specificReservationNames) > 1 { + panic("too many specificReservationNames in test") + } + + return func(*terraform.State) error { + if instanceTemplate.Properties.ReservationAffinity == nil { + return fmt.Errorf("expected template to have reservation affinity, but it was nil") + } + + if actualReservationType := instanceTemplate.Properties.ReservationAffinity.ConsumeReservationType; actualReservationType != consumeReservationType { + return fmt.Errorf("Wrong reservationAffinity consumeReservationType: expected %s, got, %s", consumeReservationType, actualReservationType) + } + + if len(specificReservationNames) > 0 { + const reservationNameKey = "compute.googleapis.com/reservation-name" + if actualKey := instanceTemplate.Properties.ReservationAffinity.Key; actualKey != reservationNameKey { + return fmt.Errorf("Wrong reservationAffinity key: expected %s, got, %s", reservationNameKey, actualKey) + } + + reservationAffinityValues := instanceTemplate.Properties.ReservationAffinity.Values + if len(reservationAffinityValues) != 1 || reservationAffinityValues[0] != specificReservationNames[0] { + return fmt.Errorf("Wrong reservationAffinity values: expected %s, got, %s", specificReservationNames, reservationAffinityValues) + } + } + + return nil + } +} + func testAccCheckComputeInstanceTemplateHasShieldedVmConfig(instanceTemplate *computeBeta.InstanceTemplate, enableSecureBoot bool, enableVtpm bool, enableIntegrityMonitoring bool) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -2139,6 +2220,69 @@ resource "google_compute_instance_template" "foobar" { `, suffix) } +func testAccComputeInstanceTemplate_reservationAffinityInstanceTemplate_nonSpecificReservation(templateName, consumeReservationType string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance_template" "foobar" { + name = "instancet-test-%s" + machine_type = "e2-medium" + can_ip_forward = false + + disk { + source_image = data.google_compute_image.my_image.self_link + auto_delete = true + boot = true + } + + network_interface { + network = "default" + } + + reservation_affinity { + type = "%s" + } +} +`, templateName, consumeReservationType) +} + +func testAccComputeInstanceTemplate_reservationAffinityInstanceTemplate_specificReservation(templateName string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance_template" "foobar" { + name = "instancet-test-%s" + machine_type = "e2-medium" + can_ip_forward = false + + disk { + source_image = data.google_compute_image.my_image.self_link + auto_delete = true + boot = true + } + + network_interface { + network = "default" + } + + reservation_affinity { + type = "SPECIFIC_RESERVATION" + + specific_reservation { + key = "compute.googleapis.com/reservation-name" + values = ["%s"] + } + } +} +`, templateName, templateName) +} + func testAccComputeInstanceTemplate_shieldedVmConfig(suffix string, enableSecureBoot bool, enableVtpm bool, enableIntegrityMonitoring bool) string { return fmt.Sprintf(` data "google_compute_image" "my_image" { diff --git a/google-beta/resource_compute_instance_test.go b/google-beta/resource_compute_instance_test.go index 4fef381570..78807940df 100644 --- a/google-beta/resource_compute_instance_test.go +++ b/google-beta/resource_compute_instance_test.go @@ -956,6 +956,45 @@ func TestAccComputeInstance_soleTenantNodeAffinities(t *testing.T) { }) } +func TestAccComputeInstance_reservationAffinities(t *testing.T) { + t.Parallel() + + var instance computeBeta.Instance + var instanceName = fmt.Sprintf("tf-test-resaffinity-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeInstance_reservationAffinity_nonSpecificReservationConfig(instanceName, "NO_RESERVATION"), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists(t, "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasReservationAffinity(&instance, "NO_RESERVATION"), + ), + }, + computeInstanceImportStep("us-central1-a", instanceName, []string{}), + { + Config: testAccComputeInstance_reservationAffinity_nonSpecificReservationConfig(instanceName, "ANY_RESERVATION"), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists(t, "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasReservationAffinity(&instance, "ANY_RESERVATION"), + ), + }, + computeInstanceImportStep("us-central1-a", instanceName, []string{}), + { + Config: testAccComputeInstance_reservationAffinity_specificReservationConfig(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists(t, "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasReservationAffinity(&instance, "SPECIFIC_RESERVATION", instanceName), + ), + }, + computeInstanceImportStep("us-central1-a", instanceName, []string{}), + }, + }) +} + func TestAccComputeInstance_subnet_auto(t *testing.T) { t.Parallel() @@ -2742,6 +2781,34 @@ func testAccCheckComputeInstanceHasConfiguredDeletionProtection(instance *comput } } +func testAccCheckComputeInstanceHasReservationAffinity(instance *computeBeta.Instance, reservationType string, specificReservationNames ...string) resource.TestCheckFunc { + if len(specificReservationNames) > 1 { + panic("too many specificReservationNames provided in test") + } + + return func(*terraform.State) error { + if instance.ReservationAffinity == nil { + return fmt.Errorf("expected instance to have reservation affinity, but it was nil") + } + + if instance.ReservationAffinity.ConsumeReservationType != reservationType { + return fmt.Errorf("Wrong reservationAffinity consumeReservationType: expected %s, got, %s", reservationType, instance.ReservationAffinity.ConsumeReservationType) + } + + if len(specificReservationNames) > 0 { + const reservationNameKey = "compute.googleapis.com/reservation-name" + if instance.ReservationAffinity.Key != reservationNameKey { + return fmt.Errorf("Wrong reservationAffinity key: expected %s, got, %s", reservationNameKey, instance.ReservationAffinity.Key) + } + if len(instance.ReservationAffinity.Values) != 1 || instance.ReservationAffinity.Values[0] != specificReservationNames[0] { + return fmt.Errorf("Wrong reservationAffinity values: expected %s, got, %s", specificReservationNames, instance.ReservationAffinity.Values) + } + } + + return nil + } +} + func testAccCheckComputeInstanceHasShieldedVmConfig(instance *computeBeta.Instance, enableSecureBoot bool, enableVtpm bool, enableIntegrityMonitoring bool) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -5072,6 +5139,81 @@ resource "google_compute_node_group" "nodes" { `, instance, nodeTemplate, nodeGroup) } +func testAccComputeInstance_reservationAffinity_nonSpecificReservationConfig(instanceName, reservationType string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } + + reservation_affinity { + type = "%s" + } +}`, instanceName, reservationType) +} + +func testAccComputeInstance_reservationAffinity_specificReservationConfig(instanceName string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_reservation" "reservation" { + name = "%s" + zone = "us-central1-a" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = true +} + +resource "google_compute_instance" "foobar" { + name = "%[1]s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } + + reservation_affinity { + type = "SPECIFIC_RESERVATION" + + specific_reservation { + key = "compute.googleapis.com/reservation-name" + values = ["%[1]s"] + } + } +}`, instanceName) +} + func testAccComputeInstance_shieldedVmConfig(instance string, enableSecureBoot bool, enableVtpm bool, enableIntegrityMonitoring bool) string { return fmt.Sprintf(` data "google_compute_image" "my_image" { diff --git a/google-beta/resource_dataproc_cluster_test.go b/google-beta/resource_dataproc_cluster_test.go index e2592b3a32..ab4878c979 100644 --- a/google-beta/resource_dataproc_cluster_test.go +++ b/google-beta/resource_dataproc_cluster_test.go @@ -13,8 +13,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - dataproc "google.golang.org/api/dataproc/v1beta2" "google.golang.org/api/googleapi" + + dataproc "google.golang.org/api/dataproc/v1beta2" ) func TestDataprocExtractInitTimeout(t *testing.T) {