Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compute: Add scheduling.termination_time field to compute_instance resources #21717

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changelog/12791.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
```release-note:enhancement
compute: added `scheduling.termination_time` field to `google_compute_instance` resource
```
```release-note:enhancement
compute: added `scheduling.termination_time` field to `google_compute_instance_from_machine_image` resource
```
```release-note:enhancement
compute: added `scheduling.termination_time` field to `google_compute_instance_from_template` resource
```
```release-note:enhancement
compute: added `scheduling.termination_time` field to `google_compute_instance_template` resource
```
```release-note:enhancement
compute: added `scheduling.termination_time` field to `google_compute_region_instance_template` resource
```
26 changes: 25 additions & 1 deletion google/services/compute/compute_instance_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ func expandScheduling(v interface{}) (*compute.Scheduling, error) {
scheduling.LocalSsdRecoveryTimeout = transformedLocalSsdRecoveryTimeout
scheduling.ForceSendFields = append(scheduling.ForceSendFields, "LocalSsdRecoveryTimeout")
}
if v, ok := original["termination_time"]; ok {
scheduling.TerminationTime = v.(string)
}
return scheduling, nil
}

Expand Down Expand Up @@ -268,6 +271,7 @@ func flattenScheduling(resp *compute.Scheduling) []map[string]interface{} {
"provisioning_model": resp.ProvisioningModel,
"instance_termination_action": resp.InstanceTerminationAction,
"availability_domain": resp.AvailabilityDomain,
"termination_time": resp.TerminationTime,
}

if resp.AutomaticRestart != nil {
Expand Down Expand Up @@ -663,7 +667,9 @@ func schedulingHasChangeRequiringReboot(d *schema.ResourceData) bool {
oScheduling := o.([]interface{})[0].(map[string]interface{})
newScheduling := n.([]interface{})[0].(map[string]interface{})

return hasNodeAffinitiesChanged(oScheduling, newScheduling) || hasMaxRunDurationChanged(oScheduling, newScheduling)
return (hasNodeAffinitiesChanged(oScheduling, newScheduling) ||
hasMaxRunDurationChanged(oScheduling, newScheduling) ||
hasTerminationTimeChanged(oScheduling, newScheduling))
}

// Terraform doesn't correctly calculate changes on schema.Set, so we do it manually
Expand Down Expand Up @@ -708,6 +714,24 @@ func schedulingHasChangeWithoutReboot(d *schema.ResourceData) bool {
return false
}

func hasTerminationTimeChanged(oScheduling, nScheduling map[string]interface{}) bool {
oTerminationTime := oScheduling["termination_time"].(string)
nTerminationTime := nScheduling["termination_time"].(string)

if len(oTerminationTime) == 0 && len(nTerminationTime) == 0 {
return false
}
if len(oTerminationTime) == 0 || len(nTerminationTime) == 0 {
return true
}

if oTerminationTime != nTerminationTime {
return true
}

return false
}

func hasMaxRunDurationChanged(oScheduling, nScheduling map[string]interface{}) bool {
oMrd := oScheduling["max_run_duration"].([]interface{})
nMrd := nScheduling["max_run_duration"].([]interface{})
Expand Down
41 changes: 41 additions & 0 deletions google/services/compute/compute_instance_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package compute

import (
"testing"
)

func TestHasTerminationTimeChanged(t *testing.T) {
t.Parallel()
cases := map[string]struct {
Old, New map[string]interface{}
Expect bool
}{
"empty": {
Old: map[string]interface{}{"termination_time": ""},
New: map[string]interface{}{"termination_time": ""},
Expect: false,
},
"new": {
Old: map[string]interface{}{"termination_time": ""},
New: map[string]interface{}{"termination_time": "2025-01-31T15:04:05Z"},
Expect: true,
},
"changed": {
Old: map[string]interface{}{"termination_time": "2025-01-30T15:04:05Z"},
New: map[string]interface{}{"termination_time": "2025-01-31T15:04:05Z"},
Expect: true,
},
"same": {
Old: map[string]interface{}{"termination_time": "2025-01-30T15:04:05Z"},
New: map[string]interface{}{"termination_time": "2025-01-30T15:04:05Z"},
Expect: false,
},
}
for tn, tc := range cases {
if hasTerminationTimeChanged(tc.Old, tc.New) != tc.Expect {
t.Errorf("%s: expected %t for whether termination time matched for old = %q, new = %q", tn, tc.Expect, tc.Old, tc.New)
}
}
}
10 changes: 10 additions & 0 deletions google/services/compute/resource_compute_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ var (
"scheduling.0.min_node_cpus",
"scheduling.0.provisioning_model",
"scheduling.0.instance_termination_action",
"scheduling.0.termination_time",
"scheduling.0.availability_domain",
"scheduling.0.max_run_duration",
"scheduling.0.on_instance_stop_action",
Expand Down Expand Up @@ -856,6 +857,15 @@ func ResourceComputeInstance() *schema.Resource {
AtLeastOneOf: schedulingKeys,
Description: `Specifies the action GCE should take when SPOT VM is preempted.`,
},
"termination_time": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
AtLeastOneOf: schedulingKeys,
Description: `Specifies the timestamp, when the instance will be terminated,
in RFC3339 text format. If specified, the instance termination action
will be performed at the termination time.`,
},
"availability_domain": {
Type: schema.TypeInt,
Optional: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"regexp"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
Expand Down Expand Up @@ -275,6 +276,32 @@ func TestAccComputeInstanceFromTemplate_overrideScheduling(t *testing.T) {
})
}

func TestAccComputeInstanceFromTemplate_TerminationTime(t *testing.T) {
t.Parallel()

var instance compute.Instance
instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
templateName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
templateDisk := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
resourceName := "google_compute_instance_from_template.inst"
now := time.Now().UTC()
terminationTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 9999, now.Location()).Format(time.RFC3339)

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckComputeInstanceFromTemplateDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeInstanceFromTemplate_terminationTime(templateDisk, templateName, terminationTime, instanceName),
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeInstanceExists(t, resourceName, &instance),
),
},
},
})
}

func TestAccComputeInstanceFromTemplate_overrideMetadataDotStartupScript(t *testing.T) {
var instance compute.Instance
instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
Expand Down Expand Up @@ -978,6 +1005,61 @@ resource "google_compute_instance_from_template" "inst" {
`, templateDisk, template, instance)
}

func testAccComputeInstanceFromTemplate_terminationTime(templateDisk, template, termination_time, instance string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
family = "debian-11"
project = "debian-cloud"
}

resource "google_compute_disk" "foobar" {
name = "%s"
image = data.google_compute_image.my_image.self_link
size = 10
type = "pd-ssd"
zone = "us-central1-a"
}

resource "google_compute_instance_template" "foobar" {
name = "%s"
machine_type = "e2-medium"

disk {
source = google_compute_disk.foobar.name
auto_delete = false
boot = true
}

network_interface {
network = "default"
}

metadata = {
foo = "bar"
}

scheduling {
instance_termination_action = "STOP"
termination_time = "%s"
}

can_ip_forward = true
}

resource "google_compute_instance_from_template" "inst" {
name = "%s"
zone = "us-central1-a"

source_instance_template = google_compute_instance_template.foobar.self_link

scheduling {
instance_termination_action = "STOP"
termination_time = "%s"
}
}
`, templateDisk, template, termination_time, instance, termination_time)
}

func testAccComputeInstanceFromTemplate_overrideMetadataDotStartupScript(instance, template string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
Expand Down
10 changes: 10 additions & 0 deletions google/services/compute/resource_compute_instance_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var (
"scheduling.0.availability_domain",
"scheduling.0.max_run_duration",
"scheduling.0.on_instance_stop_action",
"scheduling.0.termination_time",
"scheduling.0.local_ssd_recovery_timeout",
}

Expand Down Expand Up @@ -739,6 +740,15 @@ be from 0 to 999,999,999 inclusive.`,
},
},
},
"termination_time": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
AtLeastOneOf: schedulingKeys,
Description: `Specifies the timestamp, when the instance will be terminated,
in RFC3339 text format. If specified, the instance termination action
will be performed at the termination time.`,
},
"local_ssd_recovery_timeout": {
Type: schema.TypeList,
Optional: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,37 @@ func TestAccComputeInstanceTemplate_maxRunDuration_onInstanceStopAction(t *testi
})
}

func TestAccComputeInstanceTemplate_instanceTerminationAction_terminationTime(t *testing.T) {
t.Parallel()

var instanceTemplate compute.InstanceTemplate
now := time.Now().UTC()
terminationTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 9999, now.Location()).Format(time.RFC3339)
var instanceTerminationAction = "STOP"

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckComputeInstanceTemplateDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeInstanceTemplate_onInstanceStopAction_terminationTime(acctest.RandString(t, 10), terminationTime),
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeInstanceTemplateExists(
t, "google_compute_instance_template.foobar", &instanceTemplate),
testAccCheckComputeInstanceTemplateInstanceTerminationAction(&instanceTemplate, instanceTerminationAction),
testAccCheckComputeInstanceTemplateInstanceTerminationTime(&instanceTemplate, terminationTime),
),
},
{
ResourceName: "google_compute_instance_template.foobar",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccComputeInstanceTemplate_spot_maxRunDuration(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -1857,6 +1888,15 @@ func testAccCheckComputeInstanceTemplateInstanceTerminationAction(instanceTempla
}
}

func testAccCheckComputeInstanceTemplateInstanceTerminationTime(instanceTemplate *compute.InstanceTemplate, termination_time string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if instanceTemplate.Properties.Scheduling.TerminationTime != termination_time {
return fmt.Errorf("Expected instance_termination_time %v, got %v", termination_time, instanceTemplate.Properties.Scheduling.TerminationTime)
}
return nil
}
}

func testAccCheckComputeInstanceTemplateMaxRunDuration(instanceTemplate *compute.InstanceTemplate, instance_max_run_duration_want compute.Duration) resource.TestCheckFunc {
return func(s *terraform.State) error {
if !reflect.DeepEqual(*instanceTemplate.Properties.Scheduling.MaxRunDuration, instance_max_run_duration_want) {
Expand Down Expand Up @@ -4071,6 +4111,46 @@ resource "google_compute_instance_template" "foobar" {
`, suffix)
}

func testAccComputeInstanceTemplate_onInstanceStopAction_terminationTime(suffix string, terminationTime string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
family = "debian-11"
project = "debian-cloud"
}

resource "google_compute_instance_template" "foobar" {
name = "tf-test-instance-template-%s"
machine_type = "e2-medium"
can_ip_forward = false
tags = ["foo", "bar"]

disk {
source_image = data.google_compute_image.my_image.self_link
auto_delete = true
boot = true
}

network_interface {
network = "default"
}

scheduling {
automatic_restart = false
instance_termination_action = "STOP"
termination_time = "%s"
}

metadata = {
foo = "bar"
}

service_account {
scopes = ["userinfo-email", "compute-ro", "storage-ro"]
}
}
`, suffix, terminationTime)
}

func testAccComputeInstanceTemplate_localSsdRecoveryTimeout(suffix string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
Expand Down
Loading