From b0a9c13ad99a2e42dd24d3051b7e6e35b23405bf Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Fri, 9 Jun 2023 19:11:22 +0000 Subject: [PATCH] feat: update compute instance template resource to add field network_attachment (#8108) Signed-off-by: Modular Magician --- .changelog/8108.txt | 3 + google-beta/bootstrap_test_utils.go | 124 ++++++++++++++++++ ...resource_compute_instance_template_test.go | 85 ++++++++++++ .../compute/compute_instance_helpers.go | 27 +++- .../resource_compute_instance_template.go | 9 ++ .../d/compute_instance_template.html.markdown | 2 + .../r/compute_instance_template.html.markdown | 2 + 7 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 .changelog/8108.txt diff --git a/.changelog/8108.txt b/.changelog/8108.txt new file mode 100644 index 0000000000..56d78a8b66 --- /dev/null +++ b/.changelog/8108.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +compute: Added field `network_attachment` to `google_compute_instance_template` +``` diff --git a/google-beta/bootstrap_test_utils.go b/google-beta/bootstrap_test_utils.go index 6da5d5ea6f..c9a661c7a9 100644 --- a/google-beta/bootstrap_test_utils.go +++ b/google-beta/bootstrap_test_utils.go @@ -762,6 +762,130 @@ func BootstrapSharedCaPoolInLocation(t *testing.T, location string) string { return poolName } +func BootstrapSubnet(t *testing.T, subnetName string, networkName string) string { + projectID := acctest.GetTestProjectFromEnv() + region := acctest.GetTestRegionFromEnv() + + config := BootstrapConfig(t) + if config == nil { + t.Fatal("Could not bootstrap config.") + } + + computeService := config.NewComputeClient(config.UserAgent) + if computeService == nil { + t.Fatal("Could not create compute client.") + } + + // In order to create a networkAttachment we need to bootstrap a subnet. + _, err := computeService.Subnetworks.Get(projectID, region, subnetName).Do() + if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Subnet %q not found, bootstrapping", subnetName) + + networkUrl := fmt.Sprintf("%sprojects/%s/global/networks/%s", config.ComputeBasePath, projectID, networkName) + url := fmt.Sprintf("%sprojects/%s/regions/%s/subnetworks", config.ComputeBasePath, projectID, region) + + subnetObj := map[string]interface{}{ + "name": subnetName, + "region ": region, + "network": networkUrl, + "ipCidrRange": "10.77.1.0/28", + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: projectID, + RawURL: url, + UserAgent: config.UserAgent, + Body: subnetObj, + Timeout: 4 * time.Minute, + }) + + log.Printf("Response is, %s", res) + if err != nil { + t.Fatalf("Error bootstrapping test subnet %s: %s", subnetName, err) + } + + log.Printf("[DEBUG] Waiting for network creation to finish") + err = ComputeOperationWaitTime(config, res, projectID, "Error bootstrapping test subnet", config.UserAgent, 4*time.Minute) + if err != nil { + t.Fatalf("Error bootstrapping test subnet %s: %s", subnetName, err) + } + } + + subnet, err := computeService.Subnetworks.Get(projectID, region, subnetName).Do() + + if subnet == nil { + t.Fatalf("Error getting test subnet %s: is nil", subnetName) + } + + if err != nil { + t.Fatalf("Error getting test subnet %s: %s", subnetName, err) + } + return subnet.Name +} + +func BootstrapNetworkAttachment(t *testing.T, networkAttachmentName string, subnetName string) string { + projectID := acctest.GetTestProjectFromEnv() + region := acctest.GetTestRegionFromEnv() + + config := BootstrapConfig(t) + if config == nil { + return "" + } + + computeService := config.NewComputeClient(config.UserAgent) + if computeService == nil { + return "" + } + + networkAttachment, err := computeService.NetworkAttachments.Get(projectID, region, networkAttachmentName).Do() + if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + // Create Network Attachment Here. + log.Printf("[DEBUG] Network Attachment %s not found, bootstrapping", networkAttachmentName) + url := fmt.Sprintf("%sprojects/%s/regions/%s/networkAttachments", config.ComputeBasePath, projectID, region) + + subnetURL := fmt.Sprintf("%sprojects/%s/regions/%s/subnetworks/%s", config.ComputeBasePath, projectID, region, subnetName) + networkAttachmentObj := map[string]interface{}{ + "name": networkAttachmentName, + "region": region, + "subnetworks": []string{subnetURL}, + "connectionPreference": "ACCEPT_AUTOMATIC", + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: projectID, + RawURL: url, + UserAgent: config.UserAgent, + Body: networkAttachmentObj, + Timeout: 4 * time.Minute, + }) + if err != nil { + t.Fatalf("Error bootstrapping test Network Attachment %s: %s", networkAttachmentName, err) + } + + log.Printf("[DEBUG] Waiting for network creation to finish") + err = ComputeOperationWaitTime(config, res, projectID, "Error bootstrapping shared test subnet", config.UserAgent, 4*time.Minute) + if err != nil { + t.Fatalf("Error bootstrapping test Network Attachment %s: %s", networkAttachmentName, err) + } + } + + networkAttachment, err = computeService.NetworkAttachments.Get(projectID, region, networkAttachmentName).Do() + + if networkAttachment == nil { + t.Fatalf("Error getting test network attachment %s: is nil", networkAttachmentName) + } + + if err != nil { + t.Fatalf("Error getting test Network Attachment %s: %s", networkAttachmentName, err) + } + + return networkAttachment.Name +} + func setupProjectsAndGetAccessToken(org, billing, pid, service string, config *transport_tpg.Config) (string, error) { // Create project-1 and project-2 rmService := config.NewResourceManagerClient(config.UserAgent) diff --git a/google-beta/resource_compute_instance_template_test.go b/google-beta/resource_compute_instance_template_test.go index 199e2af865..c71e03ef22 100644 --- a/google-beta/resource_compute_instance_template_test.go +++ b/google-beta/resource_compute_instance_template_test.go @@ -1124,6 +1124,46 @@ func TestAccComputeInstanceTemplate_sourceImageEncryptionKey(t *testing.T) { }) } +func TestAccComputeInstanceTemplate_NetworkAttachment(t *testing.T) { + t.Parallel() + + var instanceTemplate compute.InstanceTemplate + + testNetworkName := BootstrapSharedTestNetwork(t, "attachment-network") + subnetName := BootstrapSubnet(t, "tf-test-subnet", testNetworkName) + networkAttachmentName := BootstrapNetworkAttachment(t, "tf-test-attachment", subnetName) + + // Need to have the full network attachment name in the format project/{project_id}/regions/{region_id}/networkAttachments/{networkAttachmentName} + fullFormNetworkAttachmentName := fmt.Sprintf("projects/%s/regions/%s/networkAttachments/%s", acctest.GetTestProjectFromEnv(), acctest.GetTestRegionFromEnv(), networkAttachmentName) + + context := map[string]interface{}{ + "subnet": subnetName, + "suffix": (RandString(t, 10)), + "network_attachment": fullFormNetworkAttachmentName, + } + + VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckComputeInstanceTemplateDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccComputeInstanceTemplate_network_attachment(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists( + t, "google_compute_instance_template.foobar", &instanceTemplate), + testAccCheckComputeInstanceTemplateHasNetworkAttachment(&instanceTemplate, fmt.Sprintf("https://www.googleapis.com/compute/beta/%s", fullFormNetworkAttachmentName)), + ), + }, + { + ResourceName: "google_compute_instance_template.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckComputeInstanceTemplateDestroyProducer(t *testing.T) func(s *terraform.State) error { return func(s *terraform.State) error { config := GoogleProviderConfig(t) @@ -1447,6 +1487,17 @@ func testAccCheckComputeInstanceTemplateHasMinCpuPlatform(instanceTemplate *comp } } +func testAccCheckComputeInstanceTemplateHasNetworkAttachment(instanceTemplate *compute.InstanceTemplate, networkAttachmentName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, networkInterface := range instanceTemplate.Properties.NetworkInterfaces { + if networkInterface.NetworkAttachment != "" && networkInterface.NetworkAttachment == networkAttachmentName { + return nil + } + } + return fmt.Errorf("Network Attachment %s, was not found in the instance template", networkAttachmentName) + } +} + func testAccCheckComputeInstanceTemplateHasInstanceResourcePolicies(instanceTemplate *compute.InstanceTemplate, resourcePolicy string) resource.TestCheckFunc { return func(s *terraform.State) error { resourcePolicyActual := instanceTemplate.Properties.ResourcePolicies[0] @@ -3238,3 +3289,37 @@ resource "google_compute_instance_template" "template" { } `, context) } + +func testAccComputeInstanceTemplate_network_attachment(context map[string]interface{}) string { + return Nprintf(` +data "google_compute_image" "my_image" { + family = "debian-11" + project = "debian-cloud" +} + + +resource "google_compute_instance_template" "foobar" { + name = "tf-test-instance-template-%{suffix}" + machine_type = "e2-medium" + + disk { + source_image = data.google_compute_image.my_image.self_link + auto_delete = true + disk_size_gb = 10 + boot = true + } + + network_interface { + network = "default" + } + + network_interface { + network_attachment = "%{network_attachment}" + } + + metadata = { + foo = "bar" + } +} +`, context) +} diff --git a/google-beta/services/compute/compute_instance_helpers.go b/google-beta/services/compute/compute_instance_helpers.go index a6af29f16b..61e226e393 100644 --- a/google-beta/services/compute/compute_instance_helpers.go +++ b/google-beta/services/compute/compute_instance_helpers.go @@ -290,6 +290,14 @@ func flattenNetworkInterfaces(d *schema.ResourceData, config *transport_tpg.Conf if internalIP == "" { internalIP = iface.NetworkIP } + + if iface.NetworkAttachment != "" { + networkAttachment, err := tpgresource.GetRelativePath(iface.NetworkAttachment) + if err != nil { + return nil, "", "", "", err + } + flattened[i]["network_attachment"] = networkAttachment + } } return flattened, region, internalIP, externalIP, nil } @@ -334,10 +342,24 @@ func expandNetworkInterfaces(d tpgresource.TerraformResourceData, config *transp for i, raw := range configs { data := raw.(map[string]interface{}) + var networkAttachment = "" network := data["network"].(string) subnetwork := data["subnetwork"].(string) - if network == "" && subnetwork == "" { - return nil, fmt.Errorf("exactly one of network or subnetwork must be provided") + if networkAttachmentObj, ok := data["network_attachment"]; ok { + networkAttachment = networkAttachmentObj.(string) + } + // Checks if networkAttachment is not specified in resource, network or subnetwork have to be specifed. + if networkAttachment == "" && network == "" && subnetwork == "" { + return nil, fmt.Errorf("exactly one of network, subnetwork, or network_attachment must be provided") + } + + if networkAttachment != "" { + if network != "" { + return nil, fmt.Errorf("Cannot have a network provided with networkAttachment given that networkAttachment is associated with a network already") + } + if subnetwork != "" { + return nil, fmt.Errorf("Cannot have a subnetwork provided with networkAttachment given that networkAttachment is associated with a subnetwork already") + } } nf, err := tpgresource.ParseNetworkFieldValue(network, d, config) @@ -354,6 +376,7 @@ func expandNetworkInterfaces(d tpgresource.TerraformResourceData, config *transp ifaces[i] = &compute.NetworkInterface{ NetworkIP: data["network_ip"].(string), Network: nf.RelativeLink(), + NetworkAttachment: networkAttachment, Subnetwork: sf.RelativeLink(), AccessConfigs: expandAccessConfigs(data["access_config"].([]interface{})), AliasIpRanges: expandAliasIpRanges(data["alias_ip_range"].([]interface{})), diff --git a/google-beta/services/compute/resource_compute_instance_template.go b/google-beta/services/compute/resource_compute_instance_template.go index 901ab2c1e3..048b7a1b17 100644 --- a/google-beta/services/compute/resource_compute_instance_template.go +++ b/google-beta/services/compute/resource_compute_instance_template.go @@ -401,6 +401,15 @@ Google Cloud KMS.`, Description: `The name of the subnetwork to attach this interface to. The subnetwork must exist in the same region this instance will be created in. Either network or subnetwork must be provided.`, }, + "network_attachment": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + DiffSuppressFunc: tpgresource.CompareSelfLinkOrResourceName, + Description: `The URL of the network attachment that this interface should connect to in the following format: projects/{projectNumber}/regions/{region_name}/networkAttachments/{network_attachment_name}.`, + }, + "subnetwork_project": { Type: schema.TypeString, Optional: true, diff --git a/website/docs/d/compute_instance_template.html.markdown b/website/docs/d/compute_instance_template.html.markdown index 1c8be88576..2af65eaae6 100644 --- a/website/docs/d/compute_instance_template.html.markdown +++ b/website/docs/d/compute_instance_template.html.markdown @@ -198,6 +198,8 @@ The `disk_encryption_key` block supports: to. The subnetwork must exist in the same `region` this instance will be created in. Either `network` or `subnetwork` must be provided. +* `network_interface` - (Optional, [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html)) The URL of the network attachment that this interface should connect to in the following format: projects/{projectNumber}/regions/{region_name}/networkAttachments/{network_attachment_name}. s + * `subnetwork_project` - The ID of the project in which the subnetwork belongs. If it is not provided, the provider project is used. diff --git a/website/docs/r/compute_instance_template.html.markdown b/website/docs/r/compute_instance_template.html.markdown index b995f223b9..8de0fec19b 100644 --- a/website/docs/r/compute_instance_template.html.markdown +++ b/website/docs/r/compute_instance_template.html.markdown @@ -470,6 +470,8 @@ The following arguments are supported: * `subnetwork` - (Optional) the name of the subnetwork to attach this interface to. The subnetwork must exist in the same `region` this instance will be created in. Either `network` or `subnetwork` must be provided. + +* `network_interface` - (Optional, [Beta](https://terraform.io/docs/providers/google/guides/provider_versions.html)) The URL of the network attachment that this interface should connect to in the following format: projects/{projectNumber}/regions/{region_name}/networkAttachments/{network_attachment_name}. * `subnetwork_project` - (Optional) The ID of the project in which the subnetwork belongs. If it is not provided, the provider project is used.