Skip to content

Commit

Permalink
Make GCE Instance node affinities updatable (#4402) (#8927)
Browse files Browse the repository at this point in the history
Co-authored-by: upodroid <[email protected]>
Co-authored-by: Cameron Thornton <[email protected]>
Signed-off-by: Modular Magician <[email protected]>

Co-authored-by: upodroid <[email protected]>
Co-authored-by: Cameron Thornton <[email protected]>
  • Loading branch information
3 people authored Apr 15, 2021
1 parent 38e2913 commit 5e79b44
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .changelog/4402.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
compute: marked scheduling.0.node_affinities as updatable in `google_compute_instance`
```
48 changes: 41 additions & 7 deletions google/compute_instance_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@ func instanceSchedulingNodeAffinitiesElemSchema() *schema.Resource {
"key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"operator": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"IN", "NOT_IN"}, false),
},
"values": {
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
Expand Down Expand Up @@ -378,9 +375,18 @@ func flattenEnableDisplay(displayDevice *computeBeta.DisplayDevice) interface{}
return displayDevice.EnableDisplay
}

// Node affinity updates require a reboot
func schedulingHasChangeRequiringReboot(d *schema.ResourceData) bool {
o, n := d.GetChange("scheduling")
oScheduling := o.([]interface{})[0].(map[string]interface{})
newScheduling := n.([]interface{})[0].(map[string]interface{})

return hasNodeAffinitiesChanged(oScheduling, newScheduling)
}

// Terraform doesn't correctly calculate changes on schema.Set, so we do it manually
// https://github.com/hashicorp/terraform-plugin-sdk/issues/98
func schedulingHasChange(d *schema.ResourceData) bool {
func schedulingHasChangeWithoutReboot(d *schema.ResourceData) bool {
if !d.HasChange("scheduling") {
// This doesn't work correctly, which is why this method exists
// But it is here for posterity
Expand All @@ -389,8 +395,11 @@ func schedulingHasChange(d *schema.ResourceData) bool {
o, n := d.GetChange("scheduling")
oScheduling := o.([]interface{})[0].(map[string]interface{})
newScheduling := n.([]interface{})[0].(map[string]interface{})
originalNa := oScheduling["node_affinities"].(*schema.Set)
newNa := newScheduling["node_affinities"].(*schema.Set)

if schedulingHasChangeRequiringReboot(d) {
return false
}

if oScheduling["automatic_restart"] != newScheduling["automatic_restart"] {
return true
}
Expand All @@ -407,5 +416,30 @@ func schedulingHasChange(d *schema.ResourceData) bool {
return true
}

return reflect.DeepEqual(newNa, originalNa)
return false
}

func hasNodeAffinitiesChanged(oScheduling, newScheduling map[string]interface{}) bool {
oldNAs := oScheduling["node_affinities"].(*schema.Set).List()
newNAs := newScheduling["node_affinities"].(*schema.Set).List()
if len(oldNAs) != len(newNAs) {
return true
}
for i := range oldNAs {
oldNodeAffinity := oldNAs[i].(map[string]interface{})
newNodeAffinity := newNAs[i].(map[string]interface{})
if oldNodeAffinity["key"] != newNodeAffinity["key"] {
return true
}
if oldNodeAffinity["operator"] != newNodeAffinity["operator"] {
return true
}

// convertStringSet will sort the set into a slice, allowing DeepEqual
if !reflect.DeepEqual(convertStringSet(oldNodeAffinity["values"].(*schema.Set)), convertStringSet(newNodeAffinity["values"].(*schema.Set))) {
return true
}
}

return false
}
31 changes: 26 additions & 5 deletions google/resource_compute_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ func resourceComputeInstance() *schema.Resource {
// !!! IMPORTANT !!!
// We have a custom diff function for the scheduling block due to issues with Terraform's
// diff on schema.Set. If changes are made to this block, they must be reflected in that
// method. See schedulingHasChange in compute_instance_helpers.go
// method. See schedulingHasChangeWithoutReboot in compute_instance_helpers.go
Schema: map[string]*schema.Schema{
"on_host_maintenance": {
Type: schema.TypeString,
Expand Down Expand Up @@ -543,7 +543,6 @@ func resourceComputeInstance() *schema.Resource {
Type: schema.TypeSet,
Optional: true,
AtLeastOneOf: schedulingKeys,
ForceNew: true,
Elem: instanceSchedulingNodeAffinitiesElemSchema(),
DiffSuppressFunc: emptyOrDefaultStringSuppress(""),
Description: `Specifies node affinities or anti-affinities to determine which sole-tenant nodes your instances and managed instance groups will use as host systems.`,
Expand Down Expand Up @@ -1344,7 +1343,9 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
}
}

if schedulingHasChange(d) {
bootRequiredSchedulingChange := schedulingHasChangeRequiringReboot(d)
bootNotRequiredSchedulingChange := schedulingHasChangeWithoutReboot(d)
if bootNotRequiredSchedulingChange {
scheduling, err := expandScheduling(d.Get("scheduling"))
if err != nil {
return fmt.Errorf("Error creating request data to update scheduling: %s", err)
Expand Down Expand Up @@ -1629,7 +1630,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
}
}

needToStopInstanceBeforeUpdating := scopesChange || d.HasChange("service_account.0.email") || d.HasChange("machine_type") || d.HasChange("min_cpu_platform") || d.HasChange("enable_display") || d.HasChange("shielded_instance_config") || len(updatesToNIWhileStopped) > 0
needToStopInstanceBeforeUpdating := scopesChange || d.HasChange("service_account.0.email") || d.HasChange("machine_type") || d.HasChange("min_cpu_platform") || d.HasChange("enable_display") || d.HasChange("shielded_instance_config") || len(updatesToNIWhileStopped) > 0 || bootRequiredSchedulingChange

if d.HasChange("desired_status") && !needToStopInstanceBeforeUpdating {
desiredStatus := d.Get("desired_status").(string)
Expand Down Expand Up @@ -1663,7 +1664,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
desiredStatus := d.Get("desired_status").(string)

if statusBeforeUpdate == "RUNNING" && desiredStatus != "TERMINATED" && !d.Get("allow_stopping_for_update").(bool) {
return fmt.Errorf("Changing the machine_type, min_cpu_platform, service_account, enable_display, shielded_instance_config, " +
return fmt.Errorf("Changing the machine_type, min_cpu_platform, service_account, enable_display, shielded_instance_config, scheduling.node_affinities " +
"or network_interface.[#d].(network/subnetwork/subnetwork_project) on a started instance requires stopping it. " +
"To acknowledge this, please set allow_stopping_for_update = true in your config. " +
"You can also stop it by setting desired_status = \"TERMINATED\", but the instance will not be restarted after the update.")
Expand Down Expand Up @@ -1768,6 +1769,26 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
}
}

if bootRequiredSchedulingChange {
scheduling, err := expandScheduling(d.Get("scheduling"))
if err != nil {
return fmt.Errorf("Error creating request data to update scheduling: %s", err)
}

op, err := config.NewComputeBetaClient(userAgent).Instances.SetScheduling(
project, zone, instance.Name, scheduling).Do()
if err != nil {
return fmt.Errorf("Error updating scheduling policy: %s", err)
}

opErr := computeOperationWaitTime(
config, op, project, "scheduling policy update", userAgent,
d.Timeout(schema.TimeoutUpdate))
if opErr != nil {
return opErr
}
}

// If the instance stops it can invalidate the fingerprint for network interface.
// refresh the instance to get a new fingerprint
if len(updatesToNIWhileStopped) > 0 {
Expand Down
126 changes: 123 additions & 3 deletions google/resource_compute_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,14 +843,22 @@ func TestAccComputeInstance_soleTenantNodeAffinities(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeInstanceDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeInstance_withoutNodeAffinities(instanceName, templateName, groupName),
},
computeInstanceImportStep("us-central1-a", instanceName, []string{"allow_stopping_for_update"}),
{
Config: testAccComputeInstance_soleTenantNodeAffinities(instanceName, templateName, groupName),
},
computeInstanceImportStep("us-central1-a", instanceName, []string{}),
computeInstanceImportStep("us-central1-a", instanceName, []string{"allow_stopping_for_update"}),
{
Config: testAccComputeInstance_soleTenantNodeAffinitiesUpdated(instanceName, templateName, groupName),
},
computeInstanceImportStep("us-central1-a", instanceName, []string{}),
computeInstanceImportStep("us-central1-a", instanceName, []string{"allow_stopping_for_update"}),
{
Config: testAccComputeInstance_soleTenantNodeAffinitiesReduced(instanceName, templateName, groupName),
},
computeInstanceImportStep("us-central1-a", instanceName, []string{"allow_stopping_for_update"}),
},
})
}
Expand Down Expand Up @@ -1515,7 +1523,7 @@ func TestAccComputeInstance_updateRunning_desiredStatusRunning_allowStoppingForU
})
}

const errorAllowStoppingMsg = "Changing the machine_type, min_cpu_platform, service_account, enable_display, shielded_instance_config, or network_interface.\\[#d\\].\\(network/subnetwork/subnetwork_project\\) on a started instance requires stopping it. To acknowledge this, please set allow_stopping_for_update = true in your config. You can also stop it by setting desired_status = \"TERMINATED\", but the instance will not be restarted after the update."
const errorAllowStoppingMsg = "Changing the machine_type, min_cpu_platform, service_account, enable_display, shielded_instance_config, scheduling.node_affinities or network_interface.\\[#d\\].\\(network/subnetwork/subnetwork_project\\) on a started instance requires stopping it. To acknowledge this, please set allow_stopping_for_update = true in your config. You can also stop it by setting desired_status = \"TERMINATED\", but the instance will not be restarted after the update."

func TestAccComputeInstance_updateRunning_desiredStatusNotSet_notAllowStoppingForUpdate(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -4544,6 +4552,53 @@ resource "google_compute_instance" "foobar" {
`, instance)
}

func testAccComputeInstance_withoutNodeAffinities(instance, nodeTemplate, nodeGroup 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-8" // can't be e2 because of sole tenancy
zone = "us-central1-a"
allow_stopping_for_update = true
boot_disk {
initialize_params {
image = data.google_compute_image.my_image.self_link
}
}
network_interface {
network = "default"
}
}
resource "google_compute_node_template" "nodetmpl" {
name = "%s"
region = "us-central1"
node_affinity_labels = {
tfacc = "test"
}
node_type = "n1-node-96-624"
cpu_overcommit_type = "ENABLED"
}
resource "google_compute_node_group" "nodes" {
name = "%s"
zone = "us-central1-a"
size = 1
node_template = google_compute_node_template.nodetmpl.self_link
}
`, instance, nodeTemplate, nodeGroup)
}

func testAccComputeInstance_soleTenantNodeAffinities(instance, nodeTemplate, nodeGroup string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
Expand All @@ -4555,6 +4610,7 @@ resource "google_compute_instance" "foobar" {
name = "%s"
machine_type = "n1-standard-8" // can't be e2 because of sole tenancy
zone = "us-central1-a"
allow_stopping_for_update = true
boot_disk {
initialize_params {
Expand Down Expand Up @@ -4623,6 +4679,7 @@ resource "google_compute_instance" "foobar" {
name = "%s"
machine_type = "n1-standard-8" // can't be e2 because of sole tenancy
zone = "us-central1-a"
allow_stopping_for_update = true
boot_disk {
initialize_params {
Expand Down Expand Up @@ -4680,6 +4737,69 @@ resource "google_compute_node_group" "nodes" {
`, instance, nodeTemplate, nodeGroup)
}

func testAccComputeInstance_soleTenantNodeAffinitiesReduced(instance, nodeTemplate, nodeGroup 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-8" // can't be e2 because of sole tenancy
zone = "us-central1-a"
allow_stopping_for_update = true
boot_disk {
initialize_params {
image = data.google_compute_image.my_image.self_link
}
}
network_interface {
network = "default"
}
scheduling {
node_affinities {
key = "tfacc"
operator = "IN"
values = ["test", "updatedlabel"]
}
node_affinities {
key = "compute.googleapis.com/node-group-name"
operator = "IN"
values = [google_compute_node_group.nodes.name]
}
min_node_cpus = 6
}
}
resource "google_compute_node_template" "nodetmpl" {
name = "%s"
region = "us-central1"
node_affinity_labels = {
tfacc = "test"
}
node_type = "n1-node-96-624"
cpu_overcommit_type = "ENABLED"
}
resource "google_compute_node_group" "nodes" {
name = "%s"
zone = "us-central1-a"
size = 1
node_template = google_compute_node_template.nodetmpl.self_link
}
`, instance, nodeTemplate, nodeGroup)
}

func testAccComputeInstance_shieldedVmConfig(instance string, enableSecureBoot bool, enableVtpm bool, enableIntegrityMonitoring bool) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
Expand Down

0 comments on commit 5e79b44

Please sign in to comment.