From c92e898657be33ba7b9ad1801efd3b428bc6eace Mon Sep 17 00:00:00 2001 From: The Magician Date: Tue, 18 Oct 2022 17:05:40 -0700 Subject: [PATCH] feat: add data source google_compute_addresses (#6648) (#12829) * feat: add data_source google_compute_addresses * tests: data_source google_compute_addresses * tests: template google_compute_addresses * docs: add descriptions on some fields * fix: add missing header in tests * tests: condition labels for compute addresses * docs: add for data google_compute_addresse Signed-off-by: Modular Magician Signed-off-by: Modular Magician --- .changelog/6648.txt | 3 + .../data_source_google_compute_addresses.go | 187 ++++++++++++++++++ ...ta_source_google_compute_addresses_test.go | 167 ++++++++++++++++ google/provider.go | 1 + .../docs/d/compute_addresses.html.markdown | 86 ++++++++ 5 files changed, 444 insertions(+) create mode 100644 .changelog/6648.txt create mode 100644 google/data_source_google_compute_addresses.go create mode 100644 google/data_source_google_compute_addresses_test.go create mode 100644 website/docs/d/compute_addresses.html.markdown diff --git a/.changelog/6648.txt b/.changelog/6648.txt new file mode 100644 index 00000000000..d905a13d716 --- /dev/null +++ b/.changelog/6648.txt @@ -0,0 +1,3 @@ +```release-note:new-datasource +`google_compute_addresses` +``` diff --git a/google/data_source_google_compute_addresses.go b/google/data_source_google_compute_addresses.go new file mode 100644 index 00000000000..73891eb1ad3 --- /dev/null +++ b/google/data_source_google_compute_addresses.go @@ -0,0 +1,187 @@ +package google + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/compute/v1" +) + +func dataSourceGoogleComputeAddresses() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceGoogleComputeAddressesRead, + + Schema: map[string]*schema.Schema{ + "addresses": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The name of the IP address.`, + }, + "address": { + Type: schema.TypeString, + Computed: true, + Description: `The IP address.`, + }, + "address_type": { + Type: schema.TypeString, + Computed: true, + Description: `The IP address type.`, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "region": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "self_link": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "filter": { + Type: schema.TypeString, + Description: `Filter sets the optional parameter "filter": A filter expression that +filters resources listed in the response. The expression must specify +the field name, an operator, and the value that you want to use for +filtering. The value must be a string, a number, or a boolean. The +operator must be either "=", "!=", ">", "<", "<=", ">=" or ":". For +example, if you are filtering Compute Engine instances, you can +exclude instances named "example-instance" by specifying "name != +example-instance". The ":" operator can be used with string fields to +match substrings. For non-string fields it is equivalent to the "=" +operator. The ":*" comparison can be used to test whether a key has +been defined. For example, to find all objects with "owner" label +use: """ labels.owner:* """ You can also filter nested fields. For +example, you could specify "scheduling.automaticRestart = false" to +include instances only if they are not scheduled for automatic +restarts. You can use filtering on nested fields to filter based on +resource labels. To filter on multiple expressions, provide each +separate expression within parentheses. For example: """ +(scheduling.automaticRestart = true) (cpuPlatform = "Intel Skylake") +""" By default, each expression is an "AND" expression. However, you +can include "AND" and "OR" expressions explicitly. For example: """ +(cpuPlatform = "Intel Skylake") OR (cpuPlatform = "Intel Broadwell") +AND (scheduling.automaticRestart = true) """`, + Optional: true, + }, + + "region": { + Type: schema.TypeString, + Optional: true, + Description: `Region that should be considered to search addresses. All regions are considered if missing.`, + }, + + "project": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `The google project in which addresses are listed. Defaults to provider's configuration if missing.`, + }, + }, + } +} + +func dataSourceGoogleComputeAddressesRead(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return diag.FromErr(err) + } + + project, err := getProject(d, config) + if err != nil { + return diag.FromErr(err) + } + + allAddresses := make([]map[string]interface{}, 0) + + client := config.NewComputeClient(userAgent).Addresses + if region, has_region := d.GetOk("region"); has_region { + request := client.List(project, region.(string)) + if filter, has_filter := d.GetOk("filter"); has_filter { + request = request.Filter(filter.(string)) + } + err = request.Pages(context, func(addresses *compute.AddressList) error { + for _, address := range addresses.Items { + allAddresses = append(allAddresses, generateTfAddress(address)) + } + return nil + }) + } else { + request := client.AggregatedList(project) + if filter, has_filter := d.GetOk("filter"); has_filter { + request = request.Filter(filter.(string)) + } + err = request.Pages(context, func(addresses *compute.AddressAggregatedList) error { + for _, items := range addresses.Items { + for _, address := range items.Addresses { + allAddresses = append(allAddresses, generateTfAddress(address)) + } + } + return nil + }) + } + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("addresses", allAddresses); err != nil { + return diag.FromErr(fmt.Errorf("error setting addresses: %s", err)) + } + + if err := d.Set("project", project); err != nil { + return diag.FromErr(fmt.Errorf("error setting project: %s", err)) + } + d.SetId(computeId(project, d)) + return nil +} + +func generateTfAddress(address *compute.Address) map[string]interface{} { + return map[string]interface{}{ + "name": address.Name, + "address": address.Address, + "address_type": address.AddressType, + "description": address.Description, + "region": regionFromUrl(address.Region), + "status": address.Status, + "self_link": address.SelfLink, + } +} + +func computeId(project string, d *schema.ResourceData) string { + region := "ALL" + filter := "ALL" + if p_region, has_region := d.GetOk("region"); has_region { + region = p_region.(string) + } + if p_filter, has_filter := d.GetOk("filter"); has_filter { + filter = p_filter.(string) + } + return fmt.Sprintf("%s-%s-%s", project, region, filter) +} + +func regionFromUrl(url string) string { + parts := strings.Split(url, "/") + if count := len(parts); count > 0 { + return parts[count-1] + } + return "" +} diff --git a/google/data_source_google_compute_addresses_test.go b/google/data_source_google_compute_addresses_test.go new file mode 100644 index 00000000000..03ad2c41ae3 --- /dev/null +++ b/google/data_source_google_compute_addresses_test.go @@ -0,0 +1,167 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataSourceComputeAddresses(t *testing.T) { + t.Parallel() + + addressName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + + region := "europe-west8" + region_bis := "asia-east1" + dsName := "regional_addresses" + dsFullName := fmt.Sprintf("data.google_compute_addresses.%s", dsName) + dsAllName := "all_addresses" + dsAllFullName := fmt.Sprintf("data.google_compute_addresses.%s", dsAllName) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceComputeAddressesConfig(addressName, region, region_bis), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceComputeAddressesRegionSpecificCheck(t, addressName, dsFullName, region), + testAccDataSourceComputeAddressesAllRegionsCheck(t, addressName, dsAllFullName, region, region_bis), + ), + }, + }, + }) +} + +func testAccDataSourceComputeAddressesAllRegionsCheck(t *testing.T, address_name string, data_source_name string, expected_region string, expected_region_bis string) resource.TestCheckFunc { + return func(s *terraform.State) error { + expected_addresses := buildAddressesList(3, address_name, expected_region) + expected_addresses = append(expected_addresses, buildAddressesList(3, address_name, expected_region_bis)...) + + return testDataSourceAdressContains(s, data_source_name, expected_addresses) + } +} + +func testAccDataSourceComputeAddressesRegionSpecificCheck(t *testing.T, address_name string, data_source_name string, expected_region string) resource.TestCheckFunc { + return func(s *terraform.State) error { + expected_addresses := buildAddressesList(3, address_name, expected_region) + return testDataSourceAdressContains(s, data_source_name, expected_addresses) + } +} + +func testAccDataSourceComputeAddressesConfig(addressName, region, region_bis string) string { + return fmt.Sprintf(` +locals { + region = "%s" + region_bis = "%s" + address_name = "%s" +} + +resource "google_compute_address" "address" { + count = 3 + + region = local.region + name = "${local.address_name}-${local.region}-${count.index}" +} + +resource "google_compute_address" "address_region_bis" { + count = 3 + + region = local.region_bis + name = "${local.address_name}-${local.region_bis}-${count.index}" +} + +data "google_compute_addresses" "regional_addresses" { + filter = "name:${local.address_name}-*" + depends_on = [google_compute_address.address] + region = local.region +} + +data "google_compute_addresses" "all_addresses" { + filter = "name:${local.address_name}-*" + depends_on = [google_compute_address.address, google_compute_address.address_region_bis] +} +`, region, region_bis, addressName) +} + +type expectedAddress struct { + name string + region string +} + +func (r expectedAddress) checkAddressMatch(index int, attrs map[string]string) (bool, error) { + map_name := fmt.Sprintf("addresses.%d.name", index) + address_name := attrs[map_name] + + if address_name != r.name { + return false, nil + } + + map_region := fmt.Sprintf("addresses.%d.region", index) + region, found := attrs[map_region] + if !found { + return false, fmt.Errorf("%s doesn't exists", map_region) + } + if region != r.region { + return false, fmt.Errorf("Unexpected region: got %s expected %s", region, r.region) + } + + return true, nil +} + +func testDataSourceAdressContains(state *terraform.State, data_source_name string, addresses []expectedAddress) error { + ds, ok := state.RootModule().Resources[data_source_name] + if !ok { + return fmt.Errorf("root module has no resource called %s", data_source_name) + } + + ds_attr := ds.Primary.Attributes + + addresses_length := len(addresses) + + if ds_attr["addresses.#"] != fmt.Sprintf("%d", addresses_length) { + return fmt.Errorf("addresses.# is not equal to %d", addresses_length) + } + + for address_index := 0; address_index < addresses_length; address_index++ { + has_match := false + for j := 0; j < len(addresses); j++ { + match, err := addresses[j].checkAddressMatch(address_index, ds_attr) + if err != nil { + return err + } else { + if match { + has_match = true + addresses = removeExpectedAddress(addresses, j) + break + } + } + } + if !has_match { + return fmt.Errorf("unexpected address at index %d", address_index) // TODO improve + } + } + + if len(addresses) != 0 { + return fmt.Errorf("%+v not found in data source", addresses) + } + return nil +} + +func buildAddressesList(numberofAddresses int, addressName string, region string) []expectedAddress { + var addresses []expectedAddress + for i := 0; i < numberofAddresses; i++ { + addresses = append(addresses, expectedAddress{ + name: fmt.Sprintf("%s-%s-%d", addressName, region, i), + region: region, + }) + } + return addresses +} + +func removeExpectedAddress(s []expectedAddress, i int) []expectedAddress { + s[i] = s[len(s)-1] + return s[:len(s)-1] +} diff --git a/google/provider.go b/google/provider.go index 0d53d472489..77662663752 100644 --- a/google/provider.go +++ b/google/provider.go @@ -796,6 +796,7 @@ func Provider() *schema.Provider { "google_composer_environment": dataSourceGoogleComposerEnvironment(), "google_composer_image_versions": dataSourceGoogleComposerImageVersions(), "google_compute_address": dataSourceGoogleComputeAddress(), + "google_compute_addresses": dataSourceGoogleComputeAddresses(), "google_compute_backend_service": dataSourceGoogleComputeBackendService(), "google_compute_backend_bucket": dataSourceGoogleComputeBackendBucket(), "google_compute_default_service_account": dataSourceGoogleComputeDefaultServiceAccount(), diff --git a/website/docs/d/compute_addresses.html.markdown b/website/docs/d/compute_addresses.html.markdown new file mode 100644 index 00000000000..4e024524db5 --- /dev/null +++ b/website/docs/d/compute_addresses.html.markdown @@ -0,0 +1,86 @@ +--- +subcategory: "Compute Engine" +page_title: "Google: google_compute_addresses" +description: |- + List google compute addresses. +--- + +# google\_compute\_addresses + +List IP addresses in a project. For more information see +the official API [list](https://cloud.google.com/compute/docs/reference/latest/addresses/list) and +[aggregated lsit](https://cloud.google.com/compute/docs/reference/rest/v1/addresses/aggregatedList) documentation. + +## Example Usage + +```hcl +data "google_compute_addresses" "my_addresses" { + filter = "name:test-*" +} + +resource "google_dns_record_set" "frontend" { + name = "frontend.${google_dns_managed_zone.prod.dns_name}" + type = "A" + ttl = 300 + + managed_zone = google_dns_managed_zone.prod.name + + rrdatas = data.google_compute_addresses.my_addresses[*].address +} + +resource "google_dns_managed_zone" "prod" { + name = "prod-zone" + dns_name = "prod.mydomain.com." +} +``` + +## Argument Reference + +The following arguments are supported: + +* `project` - (Optional) The google project in which addresses are listed. + Defaults to provider's configuration if missing. + +* `region` - (Optional) Region that should be considered to search addresses. + All regions are considered if missing. + +* `filter` - (Optional) A filter expression that + filters resources listed in the response. The expression must specify + the field name, an operator, and the value that you want to use for + filtering. The value must be a string, a number, or a boolean. The + operator must be either "=", "!=", ">", "<", "<=", ">=" or ":". For + example, if you are filtering Compute Engine instances, you can + exclude instances named "example-instance" by specifying "name != + example-instance". The ":" operator can be used with string fields to + match substrings. For non-string fields it is equivalent to the "=" + operator. The ":*" comparison can be used to test whether a key has + been defined. For example, to find all objects with "owner" label + use: """ labels.owner:* """ You can also filter nested fields. For + example, you could specify "scheduling.automaticRestart = false" to + include instances only if they are not scheduled for automatic + restarts. You can use filtering on nested fields to filter based on + resource labels. To filter on multiple expressions, provide each + separate expression within parentheses. For example: """ + (scheduling.automaticRestart = true) (cpuPlatform = "Intel Skylake") + """ By default, each expression is an "AND" expression. However, you + can include "AND" and "OR" expressions explicitly. For example: """ + (cpuPlatform = "Intel Skylake") OR (cpuPlatform = "Intel Broadwell") + AND (scheduling.automaticRestart = true) + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `addresses` - A list of addresses matching the filter. Structure is [defined below](#nested_addresses). + +The `addresses` block supports: + +* `name` - The IP address name. +* `address` - The IP address (for example `1.2.3.4`). +* `address_type` - The IP address type, can be `EXTERNAL` or `INTERNAL`. +* `description` - The IP address description. +* `status` - Indicates if the address is used. Possible values are: RESERVED or IN_USE. +* `labels` - (Beta only) A map containing IP labels. +* `region` - The region in which the address resides. +* `self_link` - The URI of the created resource.