Skip to content

Commit 0ba6b75

Browse files
compute: Add scheduling.termination_time field to compute_instance resources (#12791) (#9479)
[upstream:fc3eeaab049d020371a96176262f2495ee9f7121] Signed-off-by: Modular Magician <[email protected]>
1 parent 23b956b commit 0ba6b75

14 files changed

+567
-1
lines changed

.changelog/12791.txt

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
```release-note:enhancement
2+
compute: added `scheduling.termination_time` field to `google_compute_instance` resource
3+
```
4+
```release-note:enhancement
5+
compute: added `scheduling.termination_time` field to `google_compute_instance_from_machine_image` resource
6+
```
7+
```release-note:enhancement
8+
compute: added `scheduling.termination_time` field to `google_compute_instance_from_template` resource
9+
```
10+
```release-note:enhancement
11+
compute: added `scheduling.termination_time` field to `google_compute_instance_template` resource
12+
```
13+
```release-note:enhancement
14+
compute: added `scheduling.termination_time` field to `google_compute_region_instance_template` resource
15+
```

google-beta/services/compute/compute_instance_helpers.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ func expandScheduling(v interface{}) (*compute.Scheduling, error) {
195195
scheduling.LocalSsdRecoveryTimeout = transformedLocalSsdRecoveryTimeout
196196
scheduling.ForceSendFields = append(scheduling.ForceSendFields, "LocalSsdRecoveryTimeout")
197197
}
198+
if v, ok := original["termination_time"]; ok {
199+
scheduling.TerminationTime = v.(string)
200+
}
198201
return scheduling, nil
199202
}
200203

@@ -337,6 +340,7 @@ func flattenScheduling(resp *compute.Scheduling) []map[string]interface{} {
337340
"provisioning_model": resp.ProvisioningModel,
338341
"instance_termination_action": resp.InstanceTerminationAction,
339342
"availability_domain": resp.AvailabilityDomain,
343+
"termination_time": resp.TerminationTime,
340344
}
341345

342346
if resp.AutomaticRestart != nil {
@@ -778,7 +782,8 @@ func schedulingHasChangeRequiringReboot(d *schema.ResourceData) bool {
778782

779783
return hasNodeAffinitiesChanged(oScheduling, newScheduling) ||
780784
hasMaxRunDurationChanged(oScheduling, newScheduling) ||
781-
hasGracefulShutdownChangedWithReboot(d, oScheduling, newScheduling)
785+
hasGracefulShutdownChangedWithReboot(d, oScheduling, newScheduling) ||
786+
hasTerminationTimeChanged(oScheduling, newScheduling)
782787
}
783788

784789
// Terraform doesn't correctly calculate changes on schema.Set, so we do it manually
@@ -830,6 +835,24 @@ func schedulingHasChangeWithoutReboot(d *schema.ResourceData) bool {
830835
return false
831836
}
832837

838+
func hasTerminationTimeChanged(oScheduling, nScheduling map[string]interface{}) bool {
839+
oTerminationTime := oScheduling["termination_time"].(string)
840+
nTerminationTime := nScheduling["termination_time"].(string)
841+
842+
if len(oTerminationTime) == 0 && len(nTerminationTime) == 0 {
843+
return false
844+
}
845+
if len(oTerminationTime) == 0 || len(nTerminationTime) == 0 {
846+
return true
847+
}
848+
849+
if oTerminationTime != nTerminationTime {
850+
return true
851+
}
852+
853+
return false
854+
}
855+
833856
func hasGracefulShutdownChangedWithReboot(d *schema.ResourceData, oScheduling, nScheduling map[string]interface{}) bool {
834857
allow_stopping_for_update := d.Get("allow_stopping_for_update").(bool)
835858
if !allow_stopping_for_update {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
package compute
4+
5+
import (
6+
"testing"
7+
)
8+
9+
func TestHasTerminationTimeChanged(t *testing.T) {
10+
t.Parallel()
11+
cases := map[string]struct {
12+
Old, New map[string]interface{}
13+
Expect bool
14+
}{
15+
"empty": {
16+
Old: map[string]interface{}{"termination_time": ""},
17+
New: map[string]interface{}{"termination_time": ""},
18+
Expect: false,
19+
},
20+
"new": {
21+
Old: map[string]interface{}{"termination_time": ""},
22+
New: map[string]interface{}{"termination_time": "2025-01-31T15:04:05Z"},
23+
Expect: true,
24+
},
25+
"changed": {
26+
Old: map[string]interface{}{"termination_time": "2025-01-30T15:04:05Z"},
27+
New: map[string]interface{}{"termination_time": "2025-01-31T15:04:05Z"},
28+
Expect: true,
29+
},
30+
"same": {
31+
Old: map[string]interface{}{"termination_time": "2025-01-30T15:04:05Z"},
32+
New: map[string]interface{}{"termination_time": "2025-01-30T15:04:05Z"},
33+
Expect: false,
34+
},
35+
}
36+
for tn, tc := range cases {
37+
if hasTerminationTimeChanged(tc.Old, tc.New) != tc.Expect {
38+
t.Errorf("%s: expected %t for whether termination time matched for old = %q, new = %q", tn, tc.Expect, tc.Old, tc.New)
39+
}
40+
}
41+
}

google-beta/services/compute/resource_compute_instance.go

+10
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ var (
9292
"scheduling.0.min_node_cpus",
9393
"scheduling.0.provisioning_model",
9494
"scheduling.0.instance_termination_action",
95+
"scheduling.0.termination_time",
9596
"scheduling.0.availability_domain",
9697
"scheduling.0.max_run_duration",
9798
"scheduling.0.on_instance_stop_action",
@@ -884,6 +885,15 @@ func ResourceComputeInstance() *schema.Resource {
884885
AtLeastOneOf: schedulingKeys,
885886
Description: `Specifies the action GCE should take when SPOT VM is preempted.`,
886887
},
888+
"termination_time": {
889+
Type: schema.TypeString,
890+
Optional: true,
891+
ForceNew: true,
892+
AtLeastOneOf: schedulingKeys,
893+
Description: `Specifies the timestamp, when the instance will be terminated,
894+
in RFC3339 text format. If specified, the instance termination action
895+
will be performed at the termination time.`,
896+
},
887897
"availability_domain": {
888898
Type: schema.TypeInt,
889899
Optional: true,

google-beta/services/compute/resource_compute_instance_from_machine_image_test.go

+89
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package compute_test
55
import (
66
"fmt"
77
"testing"
8+
"time"
89

910
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1011
"github.com/hashicorp/terraform-plugin-testing/terraform"
@@ -76,6 +77,35 @@ func TestAccComputeInstanceFromMachineImage_maxRunDuration(t *testing.T) {
7677
})
7778
}
7879

80+
func TestAccComputeInstanceFromMachineImage_terminationTime(t *testing.T) {
81+
t.Parallel()
82+
83+
var instance compute.Instance
84+
instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
85+
generatedInstanceName := fmt.Sprintf("tf-test-generated-%s", acctest.RandString(t, 10))
86+
resourceName := "google_compute_instance_from_machine_image.foobar"
87+
now := time.Now().UTC()
88+
terminationTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 9999, now.Location()).Format(time.RFC3339)
89+
90+
acctest.VcrTest(t, resource.TestCase{
91+
PreCheck: func() { acctest.AccTestPreCheck(t) },
92+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t),
93+
CheckDestroy: testAccCheckComputeInstanceFromMachineImageDestroyProducer(t),
94+
Steps: []resource.TestStep{
95+
{
96+
Config: testAccComputeInstanceFromMachineImage_terminationTime(instanceName, generatedInstanceName, terminationTime),
97+
Check: resource.ComposeTestCheckFunc(
98+
testAccCheckComputeInstanceExists(t, resourceName, &instance),
99+
100+
// Check that fields were set based on the template
101+
resource.TestCheckResourceAttr(resourceName, "scheduling.0.automatic_restart", "false"),
102+
resource.TestCheckResourceAttr(resourceName, "scheduling.0.termination_time", terminationTime),
103+
),
104+
},
105+
},
106+
})
107+
}
108+
79109
func TestAccComputeInstanceFromMachineImage_localSsdRecoveryTimeout(t *testing.T) {
80110
t.Parallel()
81111

@@ -699,6 +729,65 @@ resource "google_compute_instance_from_machine_image" "foobar" {
699729
`, instance, instance, newInstance)
700730
}
701731

732+
func testAccComputeInstanceFromMachineImage_terminationTime(instance, newInstance, terminationTime string) string {
733+
return fmt.Sprintf(`
734+
resource "google_compute_instance" "vm" {
735+
provider = google-beta
736+
737+
boot_disk {
738+
initialize_params {
739+
image = "debian-cloud/debian-12"
740+
}
741+
}
742+
743+
name = "%s"
744+
machine_type = "n1-standard-1"
745+
746+
network_interface {
747+
network = "default"
748+
}
749+
750+
metadata = {
751+
foo = "bar"
752+
}
753+
754+
scheduling {
755+
automatic_restart = false
756+
instance_termination_action = "STOP"
757+
termination_time = "%s"
758+
}
759+
760+
}
761+
762+
resource "google_compute_machine_image" "foobar" {
763+
provider = google-beta
764+
name = "%s"
765+
source_instance = google_compute_instance.vm.self_link
766+
}
767+
768+
resource "google_compute_instance_from_machine_image" "foobar" {
769+
provider = google-beta
770+
name = "%s"
771+
zone = "us-central1-a"
772+
773+
source_machine_image = google_compute_machine_image.foobar.self_link
774+
775+
labels = {
776+
my_key = "my_value"
777+
}
778+
scheduling {
779+
automatic_restart = false
780+
provisioning_model = "STANDARD"
781+
instance_termination_action = "STOP"
782+
termination_time = "%s"
783+
on_instance_stop_action {
784+
discard_local_ssd = true
785+
}
786+
}
787+
}
788+
`, instance, terminationTime, instance, newInstance, terminationTime)
789+
}
790+
702791
func testAccComputeInstanceFromMachineImage_localSsdRecoveryTimeout(instance, newInstance string) string {
703792
return fmt.Sprintf(`
704793
resource "google_compute_instance" "vm" {

google-beta/services/compute/resource_compute_instance_from_template_test.go

+82
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"regexp"
88
"testing"
9+
"time"
910

1011
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1112
"github.com/hashicorp/terraform-plugin-testing/terraform"
@@ -397,6 +398,32 @@ func TestAccComputeInstanceFromTemplate_overrideScheduling(t *testing.T) {
397398
})
398399
}
399400

401+
func TestAccComputeInstanceFromTemplate_TerminationTime(t *testing.T) {
402+
t.Parallel()
403+
404+
var instance compute.Instance
405+
instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
406+
templateName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
407+
templateDisk := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
408+
resourceName := "google_compute_instance_from_template.inst"
409+
now := time.Now().UTC()
410+
terminationTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 9999, now.Location()).Format(time.RFC3339)
411+
412+
acctest.VcrTest(t, resource.TestCase{
413+
PreCheck: func() { acctest.AccTestPreCheck(t) },
414+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
415+
CheckDestroy: testAccCheckComputeInstanceFromTemplateDestroyProducer(t),
416+
Steps: []resource.TestStep{
417+
{
418+
Config: testAccComputeInstanceFromTemplate_terminationTime(templateDisk, templateName, terminationTime, instanceName),
419+
Check: resource.ComposeTestCheckFunc(
420+
testAccCheckComputeInstanceExists(t, resourceName, &instance),
421+
),
422+
},
423+
},
424+
})
425+
}
426+
400427
func TestAccComputeInstanceFromTemplate_overrideMetadataDotStartupScript(t *testing.T) {
401428
var instance compute.Instance
402429
instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
@@ -1462,6 +1489,61 @@ resource "google_compute_instance_from_template" "inst" {
14621489
`, templateDisk, template, instance)
14631490
}
14641491

1492+
func testAccComputeInstanceFromTemplate_terminationTime(templateDisk, template, termination_time, instance string) string {
1493+
return fmt.Sprintf(`
1494+
data "google_compute_image" "my_image" {
1495+
family = "debian-11"
1496+
project = "debian-cloud"
1497+
}
1498+
1499+
resource "google_compute_disk" "foobar" {
1500+
name = "%s"
1501+
image = data.google_compute_image.my_image.self_link
1502+
size = 10
1503+
type = "pd-ssd"
1504+
zone = "us-central1-a"
1505+
}
1506+
1507+
resource "google_compute_instance_template" "foobar" {
1508+
name = "%s"
1509+
machine_type = "e2-medium"
1510+
1511+
disk {
1512+
source = google_compute_disk.foobar.name
1513+
auto_delete = false
1514+
boot = true
1515+
}
1516+
1517+
network_interface {
1518+
network = "default"
1519+
}
1520+
1521+
metadata = {
1522+
foo = "bar"
1523+
}
1524+
1525+
scheduling {
1526+
instance_termination_action = "STOP"
1527+
termination_time = "%s"
1528+
}
1529+
1530+
can_ip_forward = true
1531+
}
1532+
1533+
resource "google_compute_instance_from_template" "inst" {
1534+
name = "%s"
1535+
zone = "us-central1-a"
1536+
1537+
source_instance_template = google_compute_instance_template.foobar.self_link
1538+
1539+
scheduling {
1540+
instance_termination_action = "STOP"
1541+
termination_time = "%s"
1542+
}
1543+
}
1544+
`, templateDisk, template, termination_time, instance, termination_time)
1545+
}
1546+
14651547
func testAccComputeInstanceFromTemplate_overrideMetadataDotStartupScript(instance, template string) string {
14661548
return fmt.Sprintf(`
14671549
data "google_compute_image" "my_image" {

google-beta/services/compute/resource_compute_instance_template.go

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var (
3434
"scheduling.0.availability_domain",
3535
"scheduling.0.max_run_duration",
3636
"scheduling.0.on_instance_stop_action",
37+
"scheduling.0.termination_time",
3738
"scheduling.0.maintenance_interval",
3839
"scheduling.0.host_error_timeout_seconds",
3940
"scheduling.0.graceful_shutdown",
@@ -766,6 +767,15 @@ be from 0 to 999,999,999 inclusive.`,
766767
},
767768
},
768769
},
770+
"termination_time": {
771+
Type: schema.TypeString,
772+
Optional: true,
773+
ForceNew: true,
774+
AtLeastOneOf: schedulingKeys,
775+
Description: `Specifies the timestamp, when the instance will be terminated,
776+
in RFC3339 text format. If specified, the instance termination action
777+
will be performed at the termination time.`,
778+
},
769779
"host_error_timeout_seconds": {
770780
Type: schema.TypeInt,
771781
Optional: true,

0 commit comments

Comments
 (0)