From ee732c90665405f48c04aa03a742c1e84145eb45 Mon Sep 17 00:00:00 2001 From: lornaluo Date: Mon, 27 Oct 2025 21:38:08 +0000 Subject: [PATCH 1/4] feat: Add support for reservation_affinity --- builder/googlecompute/config.go | 2 + builder/googlecompute/config_test.go | 42 +++++++++++++++++++ builder/googlecompute/step_create_instance.go | 1 + .../ReservationAffinity-not-required.mdx | 13 ++++++ .../lib/common/ReservationAffinity.mdx | 6 +++ lib/common/driver_gce.go | 5 +++ lib/common/instance.go | 1 + lib/common/reservation_affinity.go | 40 ++++++++++++++++++ lib/common/reservation_affinity.hcl2spec.go | 35 ++++++++++++++++ 9 files changed, 145 insertions(+) create mode 100644 docs-partials/lib/common/ReservationAffinity-not-required.mdx create mode 100644 docs-partials/lib/common/ReservationAffinity.mdx create mode 100644 lib/common/reservation_affinity.go create mode 100644 lib/common/reservation_affinity.hcl2spec.go diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index f1254ddc..023608f5 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -241,6 +241,8 @@ type Config struct { // // Refer to the [Node Affinity](#node-affinities) for more information on affinities. NodeAffinities []common.NodeAffinity `mapstructure:"node_affinity" required:"false"` + // ReservationAffinity: Specifies the reservations that this instance can consume from. + ReservationAffinity *common.ReservationAffinity `mapstructure:"reservation_affinity" required:"false"` // The time to wait for instance state changes. Defaults to "5m". StateTimeout time.Duration `mapstructure:"state_timeout" required:"false"` // The region in which to launch the instance. Defaults to the region diff --git a/builder/googlecompute/config_test.go b/builder/googlecompute/config_test.go index c9a2fa0e..c2534471 100644 --- a/builder/googlecompute/config_test.go +++ b/builder/googlecompute/config_test.go @@ -828,6 +828,48 @@ func TestLabelsValidity(t *testing.T) { } } +func TestConfig_ReservationAffinity(t *testing.T) { + // Get a standard, valid base configuration + raw, tempfile := testConfig(t) + defer os.Remove(tempfile) + + // Add the reservation_affinity block to the configuration + raw["reservation_affinity"] = map[string]interface{}{ + "consume_reservation_type": "SPECIFIC_RESERVATION", + "key": "compute.googleapis.com/reservation-name", + "values": []string{"test-reservation"}, + } + + // Prepare the configuration, which parses the raw map into the Config struct + var c Config + warns, errs := c.Prepare(raw) + + // Use the existing helper to assert that there were no errors + testConfigOk(t, warns, errs) + + // --- Verification --- + // Now, check that the ReservationAffinity struct was populated correctly. + + if c.ReservationAffinity == nil { + t.Fatal("ReservationAffinity should not be nil after preparing config") + } + + if c.ReservationAffinity.ConsumeReservationType != "SPECIFIC_RESERVATION" { + t.Errorf("expected ConsumeReservationType to be 'SPECIFIC_RESERVATION', but got '%s'", + c.ReservationAffinity.ConsumeReservationType) + } + + if c.ReservationAffinity.Key != "compute.googleapis.com/reservation-name" { + t.Errorf("expected Key to be 'compute.googleapis.com/reservation-name', but got '%s'", + c.ReservationAffinity.Key) + } + + if len(c.ReservationAffinity.Values) != 1 || c.ReservationAffinity.Values[0] != "test-reservation" { + t.Errorf("expected Values to be ['test-reservation'], but got '%v'", + c.ReservationAffinity.Values) + } +} + // Helper stuff below func testConfig(t *testing.T) (config map[string]interface{}, tempAccountFile string) { diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go index 2029a756..8a8de190 100644 --- a/builder/googlecompute/step_create_instance.go +++ b/builder/googlecompute/step_create_instance.go @@ -207,6 +207,7 @@ func (s *StepCreateInstance) Run(ctx context.Context, state multistep.StateBag) Preemptible: c.Preemptible, NodeAffinities: c.NodeAffinities, Region: c.Region, + ReservationAffinity: c.ReservationAffinity, ServiceAccountEmail: c.ServiceAccountEmail, Scopes: c.Scopes, Subnetwork: c.Subnetwork, diff --git a/docs-partials/lib/common/ReservationAffinity-not-required.mdx b/docs-partials/lib/common/ReservationAffinity-not-required.mdx new file mode 100644 index 00000000..a807376c --- /dev/null +++ b/docs-partials/lib/common/ReservationAffinity-not-required.mdx @@ -0,0 +1,13 @@ + + +- `consume_reservation_type` (string) - ConsumeReservationType: Specifies the type of reservation from which this + instance can consume resources. + See https://cloud.google.com/compute/docs/instances/consuming-reserved-instances for examples. + +- `key` (string) - Key: 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. + +- `values` ([]string) - Values: Corresponds to the label values of a reservation resource. + + diff --git a/docs-partials/lib/common/ReservationAffinity.mdx b/docs-partials/lib/common/ReservationAffinity.mdx new file mode 100644 index 00000000..f894ae3e --- /dev/null +++ b/docs-partials/lib/common/ReservationAffinity.mdx @@ -0,0 +1,6 @@ + + +ReservationAffinity is the configuration structure for instance reservation +affinity. It allows you to consume a specific reservation. + + diff --git a/lib/common/driver_gce.go b/lib/common/driver_gce.go index 51711141..db5b2dc9 100644 --- a/lib/common/driver_gce.go +++ b/lib/common/driver_gce.go @@ -741,6 +741,11 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { }, } + if c.ReservationAffinity != nil { + log.Printf("[DEBUG] setting reservation affinity to %s", c.ReservationAffinity) + instance.ReservationAffinity = c.ReservationAffinity.ComputeType() + } + if c.MaxRunDurationInSeconds > 0 { log.Printf("[DEBUG] setting max run duration to %d seconds", c.MaxRunDurationInSeconds) instance.Scheduling.MaxRunDuration = &compute.Duration{ diff --git a/lib/common/instance.go b/lib/common/instance.go index 2af0b7d8..50699575 100644 --- a/lib/common/instance.go +++ b/lib/common/instance.go @@ -32,6 +32,7 @@ type InstanceConfig struct { InstanceTerminationAction string Preemptible bool NodeAffinities []NodeAffinity + ReservationAffinity *ReservationAffinity Region string ServiceAccountEmail string Scopes []string diff --git a/lib/common/reservation_affinity.go b/lib/common/reservation_affinity.go new file mode 100644 index 00000000..ff7539d9 --- /dev/null +++ b/lib/common/reservation_affinity.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//go:generate packer-sdc struct-markdown +//go:generate packer-sdc mapstructure-to-hcl2 -type ReservationAffinity + +package common + +import compute "google.golang.org/api/compute/v1" + +// ReservationAffinity is the configuration structure for instance reservation +// affinity. It allows you to consume a specific reservation. +type ReservationAffinity struct { + // ConsumeReservationType: Specifies the type of reservation from which this + // instance can consume resources. + // See https://cloud.google.com/compute/docs/instances/consuming-reserved-instances for examples. + ConsumeReservationType string `mapstructure:"consume_reservation_type"` + + // Key: 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. + Key string `mapstructure:"key"` + + // Values: Corresponds to the label values of a reservation resource. + Values []string `mapstructure:"values"` +} + +// ComputeType converts the Packer-specific ReservationAffinity struct to the +// type required by the Google Cloud API client library. +func (r *ReservationAffinity) ComputeType() *compute.ReservationAffinity { + if r == nil { + return nil + } + return &compute.ReservationAffinity{ + ConsumeReservationType: r.ConsumeReservationType, + Key: r.Key, + Values: r.Values, + } +} + diff --git a/lib/common/reservation_affinity.hcl2spec.go b/lib/common/reservation_affinity.hcl2spec.go new file mode 100644 index 00000000..b1e3b2ea --- /dev/null +++ b/lib/common/reservation_affinity.hcl2spec.go @@ -0,0 +1,35 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package common + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatReservationAffinity is an auto-generated flat version of ReservationAffinity. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatReservationAffinity struct { + ConsumeReservationType *string `mapstructure:"consume_reservation_type" cty:"consume_reservation_type" hcl:"consume_reservation_type"` + Key *string `mapstructure:"key" cty:"key" hcl:"key"` + Values []string `mapstructure:"values" cty:"values" hcl:"values"` +} + +// FlatMapstructure returns a new FlatReservationAffinity. +// FlatReservationAffinity is an auto-generated flat version of ReservationAffinity. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*ReservationAffinity) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatReservationAffinity) +} + +// HCL2Spec returns the hcl spec of a ReservationAffinity. +// This spec is used by HCL to read the fields of ReservationAffinity. +// The decoded values from this spec will then be applied to a FlatReservationAffinity. +func (*FlatReservationAffinity) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "consume_reservation_type": &hcldec.AttrSpec{Name: "consume_reservation_type", Type: cty.String, Required: false}, + "key": &hcldec.AttrSpec{Name: "key", Type: cty.String, Required: false}, + "values": &hcldec.AttrSpec{Name: "values", Type: cty.List(cty.String), Required: false}, + } + return s +} From 6993bb28f146a682cc016d332e52b8b6d56f4de1 Mon Sep 17 00:00:00 2001 From: lornaluo Date: Tue, 28 Oct 2025 18:57:15 +0000 Subject: [PATCH 2/4] feat(builder/googlecompute): Add reservation affinity support This commit introduces support for the `reservation_affinity` block to the googlecompute builder. This allows users to target specific reservations when launching instances, enabling the use of pre-provisioned capacity. The following changes are included: - Added the `ReservationAffinity` struct to the builder configuration. - Updated the HCL2 spec to recognize the `reservation_affinity` block. - Added an end-to-end acceptance test to verify that instances can be successfully launched using a specific reservation. --- builder/googlecompute/builder_acc_test.go | 68 +++++++++++++++++++ builder/googlecompute/config.hcl2spec.go | 2 + .../testdata/reservation.pkr.hcl | 33 +++++++++ 3 files changed, 103 insertions(+) create mode 100644 builder/googlecompute/testdata/reservation.pkr.hcl diff --git a/builder/googlecompute/builder_acc_test.go b/builder/googlecompute/builder_acc_test.go index ff695a5a..a2d1bc77 100644 --- a/builder/googlecompute/builder_acc_test.go +++ b/builder/googlecompute/builder_acc_test.go @@ -586,3 +586,71 @@ func TestAccBuilder_CustomEndpointsAndUniverse(t *testing.T) { }) } } + +func TestAccBuilder_WithReservation(t *testing.T) { + t.Parallel() + + // Use a timestamp to generate a unique name. + uniqueID := fmt.Sprintf("packer-test-%d", time.Now().UnixNano()) + reservationName := uniqueID + imageName := uniqueID + + tmpl, err := testDataFs.ReadFile("testdata/reservation.pkr.hcl") + if err != nil { + t.Fatalf("failed to read testdata file: %s", err) + } + + testCase := &acctest.PluginTestCase{ + Name: "googlecompute-packer-with-reservation", + Template: string(tmpl), + BuildExtraArgs: []string{ + "-var", fmt.Sprintf("project_id=%s", os.Getenv("GOOGLE_PROJECT_ID")), + "-var", fmt.Sprintf("image_name=%s", imageName), + "-var", fmt.Sprintf("reservation_name=%s", reservationName), + }, + Setup: func() error { + cmd := exec.Command("gcloud", "compute", "firewall-rules", "create", "packer-test", + "--project="+os.Getenv("GOOGLE_PROJECT_ID"), + "--allow=tcp:22", + "--network=default", + "--target-tags=packer-test") + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create firewall rule: %s\nOutput: %s", err, string(output)) + } + cmd = exec.Command("gcloud", "compute", "reservations", "create", reservationName, + "--project="+os.Getenv("GOOGLE_PROJECT_ID"), + "--zone=us-central1-a", + "--machine-type=n1-standard-1", + "--vm-count=1", + "--require-specific-reservation") + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to create reservation: %s\nOutput: %s", err, string(output)) + } + return nil + }, + Teardown: func() error { + cmd := exec.Command("gcloud", "compute", "firewall-rules", "delete", "packer-test", + "--project="+os.Getenv("GOOGLE_PROJECT_ID"), "--quiet") + cmd.Run() + cmd = exec.Command("gcloud", "compute", "reservations", "delete", reservationName, + "--project="+os.Getenv("GOOGLE_PROJECT_ID"), + "--zone=us-central1-a", "--quiet") + cmd.Run() + + cmd = exec.Command("gcloud", "compute", "images", "delete", imageName, + "--project="+os.Getenv("GOOGLE_PROJECT_ID"), "--quiet") + cmd.Run() + + return nil + }, + Check: func(buildCommand *exec.Cmd, logfile string) error { + if buildCommand.ProcessState != nil { + if buildCommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + acctest.TestPlugin(t, testCase) +} diff --git a/builder/googlecompute/config.hcl2spec.go b/builder/googlecompute/config.hcl2spec.go index 0b381a21..a9a1d9a1 100644 --- a/builder/googlecompute/config.hcl2spec.go +++ b/builder/googlecompute/config.hcl2spec.go @@ -118,6 +118,7 @@ type FlatConfig struct { InstanceTerminationAction *string `mapstructure:"instance_termination_action" required:"false" cty:"instance_termination_action" hcl:"instance_termination_action"` Preemptible *bool `mapstructure:"preemptible" required:"false" cty:"preemptible" hcl:"preemptible"` NodeAffinities []common.FlatNodeAffinity `mapstructure:"node_affinity" required:"false" cty:"node_affinity" hcl:"node_affinity"` + ReservationAffinity *common.FlatReservationAffinity `mapstructure:"reservation_affinity" required:"false" cty:"reservation_affinity" hcl:"reservation_affinity"` StateTimeout *string `mapstructure:"state_timeout" required:"false" cty:"state_timeout" hcl:"state_timeout"` Region *string `mapstructure:"region" required:"false" cty:"region" hcl:"region"` Scopes []string `mapstructure:"scopes" required:"false" cty:"scopes" hcl:"scopes"` @@ -262,6 +263,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "instance_termination_action": &hcldec.AttrSpec{Name: "instance_termination_action", Type: cty.String, Required: false}, "preemptible": &hcldec.AttrSpec{Name: "preemptible", Type: cty.Bool, Required: false}, "node_affinity": &hcldec.BlockListSpec{TypeName: "node_affinity", Nested: hcldec.ObjectSpec((*common.FlatNodeAffinity)(nil).HCL2Spec())}, + "reservation_affinity": &hcldec.BlockSpec{TypeName: "reservation_affinity", Nested: hcldec.ObjectSpec((*common.FlatReservationAffinity)(nil).HCL2Spec())}, "state_timeout": &hcldec.AttrSpec{Name: "state_timeout", Type: cty.String, Required: false}, "region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false}, "scopes": &hcldec.AttrSpec{Name: "scopes", Type: cty.List(cty.String), Required: false}, diff --git a/builder/googlecompute/testdata/reservation.pkr.hcl b/builder/googlecompute/testdata/reservation.pkr.hcl new file mode 100644 index 00000000..684de4b2 --- /dev/null +++ b/builder/googlecompute/testdata/reservation.pkr.hcl @@ -0,0 +1,33 @@ +variable "image_name" { + type = string +} + +variable "reservation_name" { + type = string +} + +variable "project_id" { + type = string +} + +source "googlecompute" "reservation-test" { + project_id = var.project_id + source_image_family = "ubuntu-2204-lts" + source_image_project_id = ["ubuntu-os-cloud"] + zone = "us-central1-a" + tags = ["packer-test"] + network = "default" + ssh_username = "packer" + image_name = var.image_name + machine_type = "n1-standard-1" + + reservation_affinity { + consume_reservation_type = "SPECIFIC_RESERVATION" + key = "compute.googleapis.com/reservation-name" + values = [var.reservation_name] + } +} + +build { + sources = ["source.googlecompute.reservation-test"] +} From 2d2bb97cc7c69d3f48aa9da9ee6bfcf1d679cbe7 Mon Sep 17 00:00:00 2001 From: lornaluo Date: Thu, 30 Oct 2025 17:55:35 +0000 Subject: [PATCH 3/4] docs: Update reservation affinity documentation URL --- docs-partials/lib/common/ReservationAffinity-not-required.mdx | 2 +- lib/common/reservation_affinity.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-partials/lib/common/ReservationAffinity-not-required.mdx b/docs-partials/lib/common/ReservationAffinity-not-required.mdx index a807376c..cdba9501 100644 --- a/docs-partials/lib/common/ReservationAffinity-not-required.mdx +++ b/docs-partials/lib/common/ReservationAffinity-not-required.mdx @@ -2,7 +2,7 @@ - `consume_reservation_type` (string) - ConsumeReservationType: Specifies the type of reservation from which this instance can consume resources. - See https://cloud.google.com/compute/docs/instances/consuming-reserved-instances for examples. + See https://cloud.google.com/compute/docs/instances/reservations-overview for examples. - `key` (string) - Key: Corresponds to the label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify `compute.googleapis.com/reservation-name` diff --git a/lib/common/reservation_affinity.go b/lib/common/reservation_affinity.go index ff7539d9..de50d044 100644 --- a/lib/common/reservation_affinity.go +++ b/lib/common/reservation_affinity.go @@ -13,7 +13,7 @@ import compute "google.golang.org/api/compute/v1" type ReservationAffinity struct { // ConsumeReservationType: Specifies the type of reservation from which this // instance can consume resources. - // See https://cloud.google.com/compute/docs/instances/consuming-reserved-instances for examples. + // See https://cloud.google.com/compute/docs/instances/reservations-overview for examples. ConsumeReservationType string `mapstructure:"consume_reservation_type"` // Key: Corresponds to the label key of a reservation resource. To target a From 3f5aad314ae7122ceb9c7f5d8492656f3c497e1a Mon Sep 17 00:00:00 2001 From: lornaluo Date: Sun, 9 Nov 2025 08:09:59 +0000 Subject: [PATCH 4/4] feat: Add support for specific GCE reservations This change introduces the ability to specify a Google Compute Engine reservation for instances created by the Packer Google Compute builder. Key changes include: - Added configuration option to the builder. - Updated the acceptance test to correctly create and consume a specific reservation using the Google Cloud Go SDK, replacing CLI commands for improved reliability. - Ensured the in the test template matches the reservation. - Updated documentation to reflect the new configuration options and provide guidance on running acceptance tests. --- .../builder/googlecompute/README.md | 4 + GNUmakefile | 2 +- README.md | 20 ++++ builder/googlecompute/builder_acc_test.go | 99 +++++++++++++------ builder/googlecompute/config.go | 2 + builder/googlecompute/config.hcl2spec.go | 2 + builder/googlecompute/step_create_instance.go | 1 + .../testdata/reservation.pkr.hcl | 5 +- .../googlecompute/Config-not-required.mdx | 4 + lib/common/instance.go | 1 + lib/common/reservation_affinity.go | 1 - 11 files changed, 107 insertions(+), 34 deletions(-) diff --git a/.web-docs/components/builder/googlecompute/README.md b/.web-docs/components/builder/googlecompute/README.md index d749775e..2be233cd 100644 --- a/.web-docs/components/builder/googlecompute/README.md +++ b/.web-docs/components/builder/googlecompute/README.md @@ -253,6 +253,10 @@ builder. Refer to the [Node Affinity](#node-affinities) for more information on affinities. +- `reservation_affinity` (\*common.ReservationAffinity) - ReservationAffinity: Specifies the reservations that this instance can consume from. + +- `specific_reservation_required` (bool) - If you are using a reservation, you must set this to true. + - `state_timeout` (duration string | ex: "1h5m2s") - The time to wait for instance state changes. Defaults to "5m". - `region` (string) - The region in which to launch the instance. Defaults to the region diff --git a/GNUmakefile b/GNUmakefile index 8be99c5b..8525085f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -15,7 +15,7 @@ dev: @go build -ldflags="-X '${PLUGIN_FQN}/version.VersionPrerelease=dev'" -o '${BINARY}' packer plugins install --path ${BINARY} "$(shell echo "${PLUGIN_FQN}" | sed 's/packer-plugin-//')" -test: +test: @go test -race -count $(COUNT) $(TEST) -timeout=3m install-packer-sdc: ## Install packer sofware development command diff --git a/README.md b/README.md index b2c2d007..882978bc 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,23 @@ documentation located in the [`docs/`](docs) directory. fix a bug, please do so by opening a Pull Request in this GitHub repository. In case of feature contribution, we kindly ask you to open an issue to discuss it beforehand. + +### Running Acceptance Tests + +To run the acceptance tests locally, you need to set the `GOOGLE_PROJECT_ID` environment variable. This variable specifies the Google Cloud project where the tests will provision resources. + +You can set it directly when invoking the `make testacc` command: + +```bash +GOOGLE_PROJECT_ID="your-gcp-project-id" make testacc +``` + +Alternatively, you can export it in your shell session before running `make testacc`: + +```bash +export GOOGLE_PROJECT_ID="your-gcp-project-id" +make testacc +``` + +Note that `make` may not always inherit environment variables exported in the current shell. If you encounter issues, explicitly setting the variable in the `make` command is recommended. + diff --git a/builder/googlecompute/builder_acc_test.go b/builder/googlecompute/builder_acc_test.go index a2d1bc77..b03703ed 100644 --- a/builder/googlecompute/builder_acc_test.go +++ b/builder/googlecompute/builder_acc_test.go @@ -4,6 +4,7 @@ package googlecompute import ( + "context" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -18,6 +19,7 @@ import ( "github.com/hashicorp/packer-plugin-googlecompute/lib/common" "github.com/hashicorp/packer-plugin-sdk/acctest" + "google.golang.org/api/compute/v1" ) //go:embed testdata @@ -609,37 +611,76 @@ func TestAccBuilder_WithReservation(t *testing.T) { "-var", fmt.Sprintf("reservation_name=%s", reservationName), }, Setup: func() error { - cmd := exec.Command("gcloud", "compute", "firewall-rules", "create", "packer-test", - "--project="+os.Getenv("GOOGLE_PROJECT_ID"), - "--allow=tcp:22", - "--network=default", - "--target-tags=packer-test") - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to create firewall rule: %s\nOutput: %s", err, string(output)) - } - cmd = exec.Command("gcloud", "compute", "reservations", "create", reservationName, - "--project="+os.Getenv("GOOGLE_PROJECT_ID"), - "--zone=us-central1-a", - "--machine-type=n1-standard-1", - "--vm-count=1", - "--require-specific-reservation") - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to create reservation: %s\nOutput: %s", err, string(output)) + projectID := os.Getenv("GOOGLE_PROJECT_ID") + if projectID == "" { + return fmt.Errorf("GOOGLE_PROJECT_ID environment variable not set") + } + // The firewall rule is required for Packer to be able to SSH into the instance. + ctx := context.Background() + computeService, err := compute.NewService(ctx) + if err != nil { + return fmt.Errorf("failed to create compute service: %s", err) + } + + firewall := &compute.Firewall{ + Name: "packer-test", + Allowed: []*compute.FirewallAllowed{ + { + IPProtocol: "tcp", + Ports: []string{"22"}, + }, + }, + Network: "global/networks/default", + TargetTags: []string{"packer-test"}, + } + _, err = computeService.Firewalls.Insert(projectID, firewall).Context(ctx).Do() + if err != nil { + return fmt.Errorf("failed to create firewall rule: %s", err) + } + reservation := &compute.Reservation{ + Name: reservationName, + SpecificReservation: &compute.AllocationSpecificSKUReservation{ + Count: 1, + InstanceProperties: &compute.AllocationSpecificSKUAllocationReservedInstanceProperties{ + MachineType: "n1-standard-1", + }, + }, + SpecificReservationRequired: true, + Zone: "us-central1-a", + } + _, err = computeService.Reservations.Insert(projectID, "us-central1-a", reservation).Context(ctx).Do() + if err != nil { + return fmt.Errorf("failed to create reservation: %s", err) } return nil - }, - Teardown: func() error { - cmd := exec.Command("gcloud", "compute", "firewall-rules", "delete", "packer-test", - "--project="+os.Getenv("GOOGLE_PROJECT_ID"), "--quiet") - cmd.Run() - cmd = exec.Command("gcloud", "compute", "reservations", "delete", reservationName, - "--project="+os.Getenv("GOOGLE_PROJECT_ID"), - "--zone=us-central1-a", "--quiet") - cmd.Run() - - cmd = exec.Command("gcloud", "compute", "images", "delete", imageName, - "--project="+os.Getenv("GOOGLE_PROJECT_ID"), "--quiet") - cmd.Run() + }, Teardown: func() error { + projectID := os.Getenv("GOOGLE_PROJECT_ID") + if projectID == "" { + return fmt.Errorf("GOOGLE_PROJECT_ID environment variable not set") + } + defer os.Unsetenv("GOOGLE_PROJECT_ID") + ctx := context.Background() + computeService, err := compute.NewService(ctx) + if err != nil { + return fmt.Errorf("failed to create compute service: %s", err) + } + _, err = computeService.Firewalls.Delete(projectID, "packer-test").Context(ctx).Do() + if err != nil { + // Don't fail teardown if the firewall rule is already gone. + fmt.Printf("failed to delete firewall rule: %s", err) + } + + _, err = computeService.Reservations.Delete(projectID, "us-central1-a", reservationName).Context(ctx).Do() + if err != nil { + // Don't fail teardown if the reservation is already gone. + fmt.Printf("failed to delete reservation: %s", err) + } + + _, err = computeService.Images.Delete(projectID, imageName).Context(ctx).Do() + if err != nil { + // Don't fail teardown if the image is already gone. + fmt.Printf("failed to delete image: %s", err) + } return nil }, diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 023608f5..50eb3d16 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -243,6 +243,8 @@ type Config struct { NodeAffinities []common.NodeAffinity `mapstructure:"node_affinity" required:"false"` // ReservationAffinity: Specifies the reservations that this instance can consume from. ReservationAffinity *common.ReservationAffinity `mapstructure:"reservation_affinity" required:"false"` + // If you are using a reservation, you must set this to true. + SpecificReservationRequired bool `mapstructure:"specific_reservation_required" required:"false"` // The time to wait for instance state changes. Defaults to "5m". StateTimeout time.Duration `mapstructure:"state_timeout" required:"false"` // The region in which to launch the instance. Defaults to the region diff --git a/builder/googlecompute/config.hcl2spec.go b/builder/googlecompute/config.hcl2spec.go index a9a1d9a1..1719798d 100644 --- a/builder/googlecompute/config.hcl2spec.go +++ b/builder/googlecompute/config.hcl2spec.go @@ -119,6 +119,7 @@ type FlatConfig struct { Preemptible *bool `mapstructure:"preemptible" required:"false" cty:"preemptible" hcl:"preemptible"` NodeAffinities []common.FlatNodeAffinity `mapstructure:"node_affinity" required:"false" cty:"node_affinity" hcl:"node_affinity"` ReservationAffinity *common.FlatReservationAffinity `mapstructure:"reservation_affinity" required:"false" cty:"reservation_affinity" hcl:"reservation_affinity"` + SpecificReservationRequired *bool `mapstructure:"specific_reservation_required" required:"false" cty:"specific_reservation_required" hcl:"specific_reservation_required"` StateTimeout *string `mapstructure:"state_timeout" required:"false" cty:"state_timeout" hcl:"state_timeout"` Region *string `mapstructure:"region" required:"false" cty:"region" hcl:"region"` Scopes []string `mapstructure:"scopes" required:"false" cty:"scopes" hcl:"scopes"` @@ -264,6 +265,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "preemptible": &hcldec.AttrSpec{Name: "preemptible", Type: cty.Bool, Required: false}, "node_affinity": &hcldec.BlockListSpec{TypeName: "node_affinity", Nested: hcldec.ObjectSpec((*common.FlatNodeAffinity)(nil).HCL2Spec())}, "reservation_affinity": &hcldec.BlockSpec{TypeName: "reservation_affinity", Nested: hcldec.ObjectSpec((*common.FlatReservationAffinity)(nil).HCL2Spec())}, + "specific_reservation_required": &hcldec.AttrSpec{Name: "specific_reservation_required", Type: cty.Bool, Required: false}, "state_timeout": &hcldec.AttrSpec{Name: "state_timeout", Type: cty.String, Required: false}, "region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false}, "scopes": &hcldec.AttrSpec{Name: "scopes", Type: cty.List(cty.String), Required: false}, diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go index 8a8de190..505d173a 100644 --- a/builder/googlecompute/step_create_instance.go +++ b/builder/googlecompute/step_create_instance.go @@ -208,6 +208,7 @@ func (s *StepCreateInstance) Run(ctx context.Context, state multistep.StateBag) NodeAffinities: c.NodeAffinities, Region: c.Region, ReservationAffinity: c.ReservationAffinity, + SpecificReservationRequired: c.SpecificReservationRequired, ServiceAccountEmail: c.ServiceAccountEmail, Scopes: c.Scopes, Subnetwork: c.Subnetwork, diff --git a/builder/googlecompute/testdata/reservation.pkr.hcl b/builder/googlecompute/testdata/reservation.pkr.hcl index 684de4b2..ad5091f5 100644 --- a/builder/googlecompute/testdata/reservation.pkr.hcl +++ b/builder/googlecompute/testdata/reservation.pkr.hcl @@ -18,14 +18,13 @@ source "googlecompute" "reservation-test" { tags = ["packer-test"] network = "default" ssh_username = "packer" - image_name = var.image_name - machine_type = "n1-standard-1" - + machine_type = "n1-standard-1" reservation_affinity { consume_reservation_type = "SPECIFIC_RESERVATION" key = "compute.googleapis.com/reservation-name" values = [var.reservation_name] } + specific_reservation_required = true } build { diff --git a/docs-partials/builder/googlecompute/Config-not-required.mdx b/docs-partials/builder/googlecompute/Config-not-required.mdx index 2122076b..efdb1b99 100644 --- a/docs-partials/builder/googlecompute/Config-not-required.mdx +++ b/docs-partials/builder/googlecompute/Config-not-required.mdx @@ -199,6 +199,10 @@ Refer to the [Node Affinity](#node-affinities) for more information on affinities. +- `reservation_affinity` (\*common.ReservationAffinity) - ReservationAffinity: Specifies the reservations that this instance can consume from. + +- `specific_reservation_required` (bool) - If you are using a reservation, you must set this to true. + - `state_timeout` (duration string | ex: "1h5m2s") - The time to wait for instance state changes. Defaults to "5m". - `region` (string) - The region in which to launch the instance. Defaults to the region diff --git a/lib/common/instance.go b/lib/common/instance.go index 50699575..60b358fd 100644 --- a/lib/common/instance.go +++ b/lib/common/instance.go @@ -33,6 +33,7 @@ type InstanceConfig struct { Preemptible bool NodeAffinities []NodeAffinity ReservationAffinity *ReservationAffinity + SpecificReservationRequired bool Region string ServiceAccountEmail string Scopes []string diff --git a/lib/common/reservation_affinity.go b/lib/common/reservation_affinity.go index de50d044..72c9b494 100644 --- a/lib/common/reservation_affinity.go +++ b/lib/common/reservation_affinity.go @@ -37,4 +37,3 @@ func (r *ReservationAffinity) ComputeType() *compute.ReservationAffinity { Values: r.Values, } } -