diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider_mmv1_resources.go b/mmv1/third_party/terraform/fwprovider/framework_provider_mmv1_resources.go index a24854aeeaaf..7fb2ff59fc0c 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider_mmv1_resources.go +++ b/mmv1/third_party/terraform/fwprovider/framework_provider_mmv1_resources.go @@ -3,6 +3,7 @@ package fwprovider import ( "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-provider-google/google/services/compute" "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager" ) @@ -15,6 +16,7 @@ func listResourceFunc(lr list.ListResource) func() list.ListResource { var generatedListResources = []func() list.ListResource{} var handwrittenListResources = []func() list.ListResource{ + listResourceFunc(compute.NewGoogleComputeInstanceListResource()), listResourceFunc(resourcemanager.NewGoogleServiceAccountListResource()), listResourceFunc(resourcemanager.NewGoogleProjectServiceListResource()), } diff --git a/mmv1/third_party/terraform/services/compute/list_google_compute_instance.go.tmpl b/mmv1/third_party/terraform/services/compute/list_google_compute_instance.go.tmpl new file mode 100644 index 000000000000..b52500decb00 --- /dev/null +++ b/mmv1/third_party/terraform/services/compute/list_google_compute_instance.go.tmpl @@ -0,0 +1,143 @@ +// Copyright (c) IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package compute + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +{{ if eq $.TargetVersionName `ga` }} + "google.golang.org/api/compute/v1" +{{- else }} + compute "google.golang.org/api/compute/v0.beta" +{{- end }} + + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +type GoogleComputeInstanceListResource struct { + tpgresource.ListResourceMetadata +} + +// GoogleComputeInstanceListModel matches [ListResourceMetadata.ListConfigFields] (tfsdk names and types). +type GoogleComputeInstanceListModel struct { + Project types.String `tfsdk:"project"` + Zone types.String `tfsdk:"zone"` +} + +func NewGoogleComputeInstanceListResource() list.ListResource { + listR := &GoogleComputeInstanceListResource{} + listR.TypeName = "google_compute_instance" + listR.SDKv2Resource = ResourceComputeInstance() + listR.ListConfigFields = []tpgresource.ListConfigField{ + {Name: "project", Kind: tpgresource.ListConfigKindString, Optional: true}, + {Name: "zone", Kind: tpgresource.ListConfigKindString, Optional: true}, + } + return listR +} + +func (listR *GoogleComputeInstanceListResource) List(ctx context.Context, listReq list.ListRequest, stream *list.ListResultsStream) { + var data GoogleComputeInstanceListModel + diags := listReq.Config.Get(ctx, &data) + if diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + if listR.Client == nil { + diags = append(diags, diag.NewErrorDiagnostic( + "Provider not configured", + "The Google provider client is not available; ensure the provider is configured (e.g. credentials and default project).", + )) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + project := tpgresource.GetResourceNameFromSelfLink(listR.GetProject(data.Project)) + zone := listR.GetZone(data.Zone) + + stream.Results = func(push func(list.ListResult) bool) { + err := ListComputeInstances(listR.Client, project, zone, func(rd *schema.ResourceData) error { + result := listReq.NewListResult(ctx) + + if err := listR.SetResult(ctx, listReq.IncludeResource, &result, rd, "name"); err != nil { + return err + } + + if !push(result) { + return errors.New("stream closed") + } + return nil + }) + if err != nil { + diags.AddError("API Error", err.Error()) + result := listReq.NewListResult(ctx) + result.Diagnostics = diags + push(result) + } + } +} + +func flattenComputeInstanceListItem(res map[string]interface{}, d *schema.ResourceData, config *transport_tpg.Config) error { + var instance compute.Instance + if err := tpgresource.Convert(res, &instance); err != nil { + return fmt.Errorf("error converting compute instance list response: %w", err) + } + if instance.Name == "" { + return fmt.Errorf("missing name in compute instance list response") + } + + project := d.Get("project").(string) + zone := tpgresource.GetResourceNameFromSelfLink(instance.Zone) + + d.SetId(fmt.Sprintf("projects/%s/zones/%s/instances/%s", project, zone, instance.Name)) + return populateComputeInstanceResourceData(d, &instance, project, zone, config) +} + +func ListComputeInstances(config *transport_tpg.Config, project, zone string, callback func(rd *schema.ResourceData) error) error { + if config == nil { + return fmt.Errorf("provider client is not configured") + } + d := ResourceComputeInstance().Data(&terraform.InstanceState{}) + if zone != "" { + if err := d.Set("zone", zone); err != nil { + return fmt.Errorf("error setting zone on temporary resource data: %w", err) + } + } + if project != "" { + if err := d.Set("project", project); err != nil { + return fmt.Errorf("error setting project on temporary resource data: %w", err) + } + } + url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}ComputeBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/zones/{{"{{"}}zone{{"}}"}}/instances") + if err != nil { + return err + } + + billingProject := "" + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + return transport_tpg.ListPages(transport_tpg.ListPagesOptions{ + Config: config, + TempData: d, + ListURL: url, + BillingProject: billingProject, + UserAgent: userAgent, + ItemName: "items", + Flattener: flattenComputeInstanceListItem, + Callback: callback, + }) +} diff --git a/mmv1/third_party/terraform/services/compute/list_google_compute_instance_test.go b/mmv1/third_party/terraform/services/compute/list_google_compute_instance_test.go new file mode 100644 index 000000000000..7aaed7b5ad92 --- /dev/null +++ b/mmv1/third_party/terraform/services/compute/list_google_compute_instance_test.go @@ -0,0 +1,91 @@ +// Copyright (c) IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package compute_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccComputeInstanceListResource_queryIdentity(t *testing.T) { + t.Parallel() + + project := envvar.GetTestProjectFromEnv() + zone := envvar.GetTestZoneFromEnv() + name := fmt.Sprintf("tf-test-instance-%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeInstanceListBasic(zone, name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_compute_instance.test", "zone", zone), + resource.TestCheckResourceAttr("google_compute_instance.test", "project", project), + resource.TestCheckResourceAttr("google_compute_instance.test", "name", name), + ), + }, + { + Query: true, + Config: testAccComputeInstanceListQuery(project, zone), + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("google_compute_instance.all_in_zone", map[string]knownvalue.Check{ + "zone": knownvalue.StringExact(zone), + "project": knownvalue.StringExact(project), + "name": knownvalue.StringExact(name), + }), + querycheck.ExpectLengthAtLeast("google_compute_instance.all_in_zone", 1), + }, + }, + }, + }) +} + +func testAccComputeInstanceListBasic(zone, name string) string { + return fmt.Sprintf(` +resource "google_compute_instance" "test" { + name = %q + zone = %q + machine_type = "e2-micro" + + boot_disk { + initialize_params { + image = "debian-cloud/debian-11" + } + } + + network_interface { + network = "default" + + access_config { + } + } +} +`, name, zone) +} + +func testAccComputeInstanceListQuery(project, zone string) string { + return fmt.Sprintf(` +list "google_compute_instance" "all_in_zone" { + provider = google + + config { + project = %q + zone = %q + } +} +`, project, zone) +} diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl index f27d3683a637..9b5f677e8364 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance.go.tmpl @@ -239,6 +239,26 @@ func ResourceComputeInstance() *schema.Resource { Delete: schema.DefaultTimeout(20 * time.Minute), }, + Identity: &schema.ResourceIdentity{ + Version: 1, + SchemaFunc: func() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + OptionalForImport: true, + }, + "zone": { + Type: schema.TypeString, + OptionalForImport: true, + }, + "name": { + Type: schema.TypeString, + RequiredForImport: true, + }, + } + }, + }, + // A compute instance is more or less a superset of a compute instance // template. Please attempt to maintain consistency with the // resource_compute_instance_template schema when updating this one. @@ -2020,26 +2040,35 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error return err } - md := flattenMetadataBeta(instance.Metadata) + zone := tpgresource.GetResourceNameFromSelfLink(instance.Zone) + + if err := populateComputeInstanceResourceData(d, instance, project, zone, config); err != nil { + return err + } - // If the existing state contains "metadata_startup_script" instead of "metadata.startup-script", - // we should move the remote metadata.startup-script to metadata_startup_script to avoid - // specifying it in two places. if _, ok := d.GetOk("metadata_startup_script"); ok { + md := d.Get("metadata").(map[string]interface{}) if err := d.Set("metadata_startup_script", md["startup-script"]); err != nil { return fmt.Errorf("Error setting metadata_startup_script: %s", err) } - delete(md, "startup-script") + if err := d.Set("metadata", md); err != nil { + return fmt.Errorf("Error setting metadata: %s", err) + } } - if err = d.Set("metadata", md); err != nil { - return fmt.Errorf("Error setting metadata: %s", err) + _, _, internalIP, externalIP, err := flattenNetworkInterfaces(d, config, instance.NetworkInterfaces) + if err != nil { + return err } - - if err := d.Set("metadata_fingerprint", instance.Metadata.Fingerprint); err != nil { - return fmt.Errorf("Error setting metadata_fingerprint: %s", err) + sshIP := externalIP + if sshIP == "" { + sshIP = internalIP } + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": sshIP, + }) {{ if ne $.TargetVersionName `ga` -}} if instance.PartnerMetadata != nil { @@ -2051,8 +2080,34 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error setting partner metadata: %s", err) } } + // Workaround: API doesn't update the scheduling.graceful_shutdown.max_duration.nanos field. + // To avoid diff, we need to set the value from the state not from API response. + scheduling := flattenScheduling(instance.Scheduling) + if nanos, ok := d.GetOk("scheduling.0.graceful_shutdown.0.max_duration.0.nanos"); ok { + graceful_shutdown := scheduling[0]["graceful_shutdown"].([]interface{})[0].(map[string]interface{}) + max_duration := graceful_shutdown["max_duration"].([]interface{})[0].(map[string]interface{}) + max_duration["nanos"] = int64(nanos.(int)) + + graceful_shutdown["max_duration"] = []interface{}{max_duration} + scheduling[0]["graceful_shutdown"] = []interface{}{graceful_shutdown} + } + if err := d.Set("scheduling", scheduling); err != nil { + return fmt.Errorf("Error setting scheduling: %s", err) + } {{- end }} + return nil +} + +func populateComputeInstanceResourceData(d *schema.ResourceData, instance *compute.Instance, project, zone string, config *transport_tpg.Config) error { + if err := d.Set("metadata", flattenMetadataBeta(instance.Metadata)); err != nil { + return fmt.Errorf("Error setting metadata: %s", err) + } + + if err := d.Set("metadata_fingerprint", instance.Metadata.Fingerprint); err != nil { + return fmt.Errorf("Error setting metadata_fingerprint: %s", err) + } + if err := d.Set("can_ip_forward", instance.CanIpForward); err != nil { return fmt.Errorf("Error setting can_ip_forward: %s", err) } @@ -2063,8 +2118,7 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error return err } // Set the networks - // Use the first external IP found for the default connection info. - networkInterfaces, _, internalIP, externalIP, err := flattenNetworkInterfaces(d, config, instance.NetworkInterfaces) + networkInterfaces, _, _, _, err := flattenNetworkInterfaces(d, config, instance.NetworkInterfaces) if err != nil { return err } @@ -2072,20 +2126,6 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error return err } - // Fall back on internal ip if there is no external ip. This makes sense in the situation where - // terraform is being used on a cloud instance and can therefore access the instances it creates - // via their internal ips. - sshIP := externalIP - if sshIP == "" { - sshIP = internalIP - } - - // Initialize the connection info - d.SetConnInfo(map[string]string{ - "type": "ssh", - "host": sshIP, - }) - // Set the tags fingerprint if there is one. if instance.Tags != nil { if err := d.Set("tags_fingerprint", instance.Tags.Fingerprint); err != nil { @@ -2216,14 +2256,12 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error // Remove nils from map in case there were disks in the config that were not present on read; // i.e. a disk was detached out of band ads := []map[string]interface{}{} - for _, d := range attachedDisks { - if d != nil { - ads = append(ads, d) + for _, ad := range attachedDisks { + if ad != nil { + ads = append(ads, ad) } } - zone := tpgresource.GetResourceNameFromSelfLink(instance.Zone) - if err := d.Set("service_account", flattenServiceAccounts(instance.ServiceAccounts)); err != nil { return fmt.Errorf("Error setting service_account: %s", err) } @@ -2234,27 +2272,9 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error setting scratch_disk: %s", err) } -{{ if eq $.TargetVersionName `ga` -}} if err := d.Set("scheduling", flattenScheduling(instance.Scheduling)); err != nil { return fmt.Errorf("Error setting scheduling: %s", err) } -{{ else -}} - // Workaroud: API doesn't update the scheduling.graceful_shutdown.max_duration.nanos field. - // To avoid diff, we need to set the value from the state not from API response. - scheduling := flattenScheduling(instance.Scheduling) - if nanos, ok := d.GetOk("scheduling.0.graceful_shutdown.0.max_duration.0.nanos"); ok { - graceful_shutdown := scheduling[0]["graceful_shutdown"].([]interface{})[0].(map[string]interface{}) - max_duration := graceful_shutdown["max_duration"].([]interface{})[0].(map[string]interface{}) - max_duration["nanos"] = int64(nanos.(int)) - - graceful_shutdown["max_duration"] = []interface{}{max_duration} - scheduling[0]["graceful_shutdown"] = []interface{}{graceful_shutdown} - } - if err := d.Set("scheduling", scheduling); err != nil { - return fmt.Errorf("Error setting scheduling: %s", err) - } -{{- end }} - if err := d.Set("guest_accelerator", flattenGuestAccelerators(instance.GuestAccelerators)); err != nil { return fmt.Errorf("Error setting guest_accelerator: %s", err) } @@ -2323,7 +2343,11 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error d.SetId(fmt.Sprintf("projects/%s/zones/%s/instances/%s", project, zone, instance.Name)) - return nil + return tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{ + "project": project, + "zone": zone, + "name": instance.Name, + }) } func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) error { @@ -3574,6 +3598,13 @@ func resourceComputeInstanceImportState(d *schema.ResourceData, meta interface{} return nil, fmt.Errorf("Error constructing id: %s", err) } d.SetId(id) + if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{ + "project": d.Get("project"), + "zone": d.Get("zone"), + "name": d.Get("name"), + }); err != nil { + return nil, err + } return []*schema.ResourceData{d}, nil } diff --git a/mmv1/third_party/terraform/services/compute/resource_compute_instance_test.go.tmpl b/mmv1/third_party/terraform/services/compute/resource_compute_instance_test.go.tmpl index 3a8b4c760afa..e2187d5cc326 100644 --- a/mmv1/third_party/terraform/services/compute/resource_compute_instance_test.go.tmpl +++ b/mmv1/third_party/terraform/services/compute/resource_compute_instance_test.go.tmpl @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-provider-google/google/envvar" tpgcompute "github.com/hashicorp/terraform-provider-google/google/services/compute" "github.com/hashicorp/terraform-provider-google/google/tpgresource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" {{ if eq $.TargetVersionName `ga` }} "google.golang.org/api/compute/v1" @@ -392,6 +393,39 @@ func TestAccComputeInstance_basic5(t *testing.T) { }) } +func TestAccComputeInstance_importBlockWithResourceIdentity(t *testing.T) { + t.Parallel() + + project := envvar.GetTestProjectFromEnv() + zone := "us-central1-a" + instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckComputeInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeInstance_identityImportBasic(instanceName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_compute_instance.foobar", "project", project), + resource.TestCheckResourceAttr("google_compute_instance.foobar", "zone", zone), + resource.TestCheckResourceAttr("google_compute_instance.foobar", "name", instanceName), + ), + }, + { + ResourceName: "google_compute_instance.foobar", + RefreshState: true, + ExpectNonEmptyPlan: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + }, + }, + }) +} + func TestAccComputeInstance_metadataGceContainerDeclaration(t *testing.T) { t.Parallel() @@ -6436,6 +6470,31 @@ resource "google_compute_instance" "foobar" { `, instance) } +func testAccComputeInstance_identityImportBasic(instance string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "e2-medium" + zone = "us-central1-a" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} +`, instance) +} + func testAccComputeInstance_metadataGceContainerDeclaration(instance string) string { return fmt.Sprintf(` data "google_compute_image" "my_image" { diff --git a/mmv1/third_party/terraform/website/docs/list-resources/google_compute_instance.html.markdown b/mmv1/third_party/terraform/website/docs/list-resources/google_compute_instance.html.markdown new file mode 100644 index 000000000000..3f073af2b05b --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/list-resources/google_compute_instance.html.markdown @@ -0,0 +1,63 @@ +--- +subcategory: "Compute Engine" +description: |- + List Google Compute Engine VM instances in a project and zone for use with terraform query + and .tfquery.hcl files. +--- + +# google_compute_instance (list) + +Lists **Compute Engine VM instances** in a Google Cloud project and zone for use with +[`terraform query`](https://developer.hashicorp.com/terraform/cli/commands/query) and +**`.tfquery.hcl`** files. Results correspond to existing +[`google_compute_instance`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance) +managed resources. + +For how list resources work in this provider, file layout, Terraform version requirements, and +shared `list` block arguments, refer to the guide +[Use list resources with terraform query (Google Cloud provider)](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/using_list_resources_with_terraform_query). + +## Example + +```hcl +list "google_compute_instance" "all" { + provider = google + + config { + # Optional. Defaults to the provider project when omitted. + # project = "other-project" + + # Optional. Defaults to the provider zone when omitted. + # zone = "us-central1-a" + } +} +``` + +Run `terraform query` from the directory that contains the `.tfquery.hcl` file. + +## Configuration (`config` block) + +* `project` - (Optional) Project ID to list instances from. If unset, the provider's + configured default project is used (same idea as the managed resource). + +* `zone` - (Optional) Zone to list instances in. If unset, the provider's configured + default zone is used (same idea as the managed resource). + +## Results + +By default each result includes **resource identity** for `google_compute_instance` (see +[Resource identity](https://developer.hashicorp.com/terraform/language/resources/identities)): + +* `name` - Instance name (required for identity). +* `project` - Project ID when applicable. +* `zone` - Zone the instance resides in. + +With `include_resource = true` on the `list` block, results also include resource attributes +populated from the API response. These include `machine_type`, `self_link`, `current_status`, +`description`, `tags`, `labels`, `metadata`, `network_interface`, `boot_disk`, `scratch_disk`, +`attached_disk`, `service_account`, `scheduling`, `guest_accelerator`, `shielded_instance_config`, +`confidential_instance_config`, `advanced_machine_features`, `deletion_protection`, `hostname`, +`cpu_platform`, `instance_id`, `creation_timestamp`, and `reservation_affinity`. + +Note: `metadata_startup_script` is not populated (it is state-dependent). Attached disk ordering +and raw disk encryption key fields are also omitted as they depend on prior configuration state.