diff --git a/docs/data-sources/availability_zones.md b/docs/data-sources/availability_zones.md new file mode 100644 index 0000000000..e1f17269ec --- /dev/null +++ b/docs/data-sources/availability_zones.md @@ -0,0 +1,31 @@ +--- +subcategory: "Account" +page_title: "Scaleway: scaleway_availability_zones" +--- + +# scaleway_availability_zones + +Use this data source to get the available zones information based on its Region. + +For technical and legal reasons, some products are split by Region or by Availability Zones. When using such product, +you can choose the location that better fits your need (country, latency, …). + +## Example Usage + +```hcl +# Get info by Region key +data scaleway_availability_zones main { + region = "nl-ams" +} +``` + +## Argument Reference + +- `region` - Region is represented as a Geographical area such as France. Defaults: `fr-par`. + +## Attributes Reference + +In addition to all above arguments, the following attributes are exported: + +- `id` - The Region ID +- `zones` - List of availability zones by regions diff --git a/scaleway/data_source_availability_zones.go b/scaleway/data_source_availability_zones.go new file mode 100644 index 0000000000..446adf15c8 --- /dev/null +++ b/scaleway/data_source_availability_zones.go @@ -0,0 +1,53 @@ +package scaleway + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/scaleway-sdk-go/validation" +) + +func DataSourceAvailabilityZones() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceAvailabilityZonesRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Description: "Region is represented as a Geographical area such as France", + Default: scw.RegionFrPar, + }, + "zones": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "Availability Zones (AZ)", + }, + }, + } +} + +func dataSourceAvailabilityZonesRead(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + regionStr := d.Get("region").(string) + + if !validation.IsRegion(regionStr) { + return diag.FromErr(SingularDataSourceFindError("Availability Zone", fmt.Errorf("not a supported region %s", regionStr))) + } + + region := scw.Region(regionStr) + d.SetId(regionStr) + _ = d.Set("zones", region.GetZones()) + + return nil +} diff --git a/scaleway/data_source_availability_zones_test.go b/scaleway/data_source_availability_zones_test.go new file mode 100644 index 0000000000..4928033975 --- /dev/null +++ b/scaleway/data_source_availability_zones_test.go @@ -0,0 +1,46 @@ +package scaleway + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccScalewayDataSourceAvailabilityZones_Basic(t *testing.T) { + tt := NewTestTools(t) + defer tt.Cleanup() + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: tt.ProviderFactories, + CheckDestroy: testAccCheckScalewayDomainRecordDestroy(tt), + Steps: []resource.TestStep{ + { + Config: ` + data scaleway_availability_zones main { + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.scaleway_availability_zones.main", "region", "fr-par"), + resource.TestCheckResourceAttr( + "data.scaleway_availability_zones.main", "zones.0", "fr-par-1"), + ), + }, + { + Config: ` + data scaleway_availability_zones main { + region = "nl-ams" + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.scaleway_availability_zones.main", "region", "nl-ams"), + resource.TestCheckResourceAttr( + "data.scaleway_availability_zones.main", "zones.0", "nl-ams-1"), + resource.TestCheckResourceAttr( + "data.scaleway_availability_zones.main", "zones.1", "nl-ams-2"), + ), + }, + }, + }) +} diff --git a/scaleway/helpers.go b/scaleway/helpers.go index e903ad3433..1452b4b3fa 100644 --- a/scaleway/helpers.go +++ b/scaleway/helpers.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/scaleway/scaleway-sdk-go/namegenerator" @@ -954,3 +955,54 @@ func customizeDiffLocalityCheck(keys ...string) schema.CustomizeDiffFunc { return nil } } + +type TooManyResultsError struct { + Count int + LastRequest interface{} +} + +func (e *TooManyResultsError) Error() string { + return fmt.Sprintf("too many results: wanted 1, got %d", e.Count) +} + +func (e *TooManyResultsError) Is(err error) bool { + _, ok := err.(*TooManyResultsError) //nolint:errorlint // Explicitly does *not* match down the error tree + return ok +} + +func (e *TooManyResultsError) As(target interface{}) bool { + t, ok := target.(**retry.NotFoundError) + if !ok { + return false + } + + *t = &retry.NotFoundError{ + Message: e.Error(), + LastRequest: e.LastRequest, + } + + return true +} + +var ErrTooManyResults = &TooManyResultsError{} + +// SingularDataSourceFindError returns a standard error message for a singular data source's non-nil resource find error. +func SingularDataSourceFindError(resourceType string, err error) error { + if NotFound(err) { + if errors.Is(err, &TooManyResultsError{}) { + return fmt.Errorf("multiple %[1]ss matched; use additional constraints to reduce matches to a single %[1]s", resourceType) + } + + return fmt.Errorf("no matching %[1]s found", resourceType) + } + + return fmt.Errorf("reading %s: %w", resourceType, err) +} + +// NotFound returns true if the error represents a "resource not found" condition. +// Specifically, NotFound returns true if the error or a wrapped error is of type +// retry.NotFoundError. +func NotFound(err error) bool { + var e *retry.NotFoundError // nosemgrep:ci.is-not-found-error + return errors.As(err, &e) +} diff --git a/scaleway/provider.go b/scaleway/provider.go index 5aa06bd11f..2dc71537a6 100644 --- a/scaleway/provider.go +++ b/scaleway/provider.go @@ -165,6 +165,7 @@ func Provider(config *ProviderConfig) plugin.ProviderFunc { DataSourcesMap: map[string]*schema.Resource{ "scaleway_account_project": dataSourceScalewayAccountProject(), "scaleway_account_ssh_key": dataSourceScalewayAccountSSHKey(), + "scaleway_availability_zones": DataSourceAvailabilityZones(), "scaleway_baremetal_offer": dataSourceScalewayBaremetalOffer(), "scaleway_baremetal_option": dataSourceScalewayBaremetalOption(), "scaleway_baremetal_os": dataSourceScalewayBaremetalOs(),