From 56f95b933db3ed22e0e8423b4db25dae0663d38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bindewald=2C=20Andr=C3=A9=20=28UIT=29?= Date: Wed, 25 Dec 2024 23:36:20 +0100 Subject: [PATCH] feat(ds/compute_skus): New data source `data.azurerm_compute_skus` --- .../compute/compute_skus_data_source.go | 183 ++++++++++++++++++ .../compute/compute_skus_data_source_test.go | 103 ++++++++++ internal/services/compute/registration.go | 1 + website/docs/d/compute_skus.html.markdown | 86 ++++++++ 4 files changed, 373 insertions(+) create mode 100644 internal/services/compute/compute_skus_data_source.go create mode 100644 internal/services/compute/compute_skus_data_source_test.go create mode 100644 website/docs/d/compute_skus.html.markdown diff --git a/internal/services/compute/compute_skus_data_source.go b/internal/services/compute/compute_skus_data_source.go new file mode 100644 index 000000000000..3f7298b0b7da --- /dev/null +++ b/internal/services/compute/compute_skus_data_source.go @@ -0,0 +1,183 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compute + +import ( + "fmt" + "slices" + "strings" + "time" + + "github.com/google/uuid" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/location" + "github.com/hashicorp/go-azure-sdk/resource-manager/compute/2021-07-01/skus" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" +) + +func dataSourceComputeSkus() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Read: dataSourceComputeSkusRead, + + Timeouts: &pluginsdk.ResourceTimeout{ + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "location": commonschema.Location(), + "include_capabilities": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "skus": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "resource_type": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "size": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "tier": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "location_restrictions": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "zone_restrictions": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "capabilities": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "zones": commonschema.ZonesMultipleComputed(), + }, + }, + }, + }, + } +} + +func dataSourceComputeSkusRead(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Compute.SkusClient + subscriptionId := meta.(*clients.Client).Account.SubscriptionId + + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + resp, err := client.ResourceSkusList(ctx, commonids.NewSubscriptionID(subscriptionId), skus.DefaultResourceSkusListOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving SKUs: %+v", err) + } + + name := d.Get("name").(string) + loc := location.Normalize(d.Get("location").(string)) + availableSkus := make([]map[string]interface{}, 0) + + if model := resp.Model; model != nil { + for _, sku := range *model { + // the API does not allow filtering by name + if name != "" { + if !strings.EqualFold(*sku.Name, name) { + continue + } + } + + // while the API accepts OData filters, the location filter is currently + // not working, thus we need to filter the results manually + locationsNormalized := make([]string, len(*sku.Locations)) + for _, v := range *sku.Locations { + locationsNormalized = append(locationsNormalized, location.Normalize(v)) + } + if !slices.Contains(locationsNormalized, loc) { + continue + } + + var zones []string + var locationRestrictions []string + var zoneRestrictions []string + capabilities := make(map[string]string) + + if sku.Restrictions != nil && len(*sku.Restrictions) > 0 { + for _, restriction := range *sku.Restrictions { + restrictionType := *restriction.Type + + switch restrictionType { + case skus.ResourceSkuRestrictionsTypeLocation: + restrictedLocationsNormalized := make([]string, 0) + for _, v := range *restriction.RestrictionInfo.Locations { + restrictedLocationsNormalized = append(restrictedLocationsNormalized, location.Normalize(v)) + } + locationRestrictions = restrictedLocationsNormalized + + case skus.ResourceSkuRestrictionsTypeZone: + zoneRestrictions = *restriction.RestrictionInfo.Zones + } + } + } + + if sku.LocationInfo != nil && len(*sku.LocationInfo) > 0 { + for _, locationInfo := range *sku.LocationInfo { + if location.Normalize(*locationInfo.Location) == loc { + zones = *locationInfo.Zones + } + } + } + + if d.Get("include_capabilities").(bool) { + if sku.Capabilities != nil && len(*sku.Capabilities) > 0 { + for _, capability := range *sku.Capabilities { + capabilities[*capability.Name] = *capability.Value + } + } + } + + availableSkus = append(availableSkus, map[string]interface{}{ + "name": sku.Name, + "resource_type": sku.ResourceType, + "size": sku.Size, + "tier": sku.Tier, + "location_restrictions": locationRestrictions, + "zone_restrictions": zoneRestrictions, + "zones": zones, + "capabilities": capabilities, + }) + } + d.SetId(uuid.New().String()) + d.Set("skus", availableSkus) + } + + return nil +} diff --git a/internal/services/compute/compute_skus_data_source_test.go b/internal/services/compute/compute_skus_data_source_test.go new file mode 100644 index 000000000000..884d511bba0e --- /dev/null +++ b/internal/services/compute/compute_skus_data_source_test.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package compute_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" +) + +type ComputeSkusDataSource struct{} + +func TestAccDataSourceComputeSkus_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_compute_skus", "test") + r := ComputeSkusDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("skus.#").HasValue("1"), + check.That(data.ResourceName).Key("skus.1").DoesNotExist(), + check.That(data.ResourceName).Key("skus.0.name").HasValue("Standard_DS2_v2"), + check.That(data.ResourceName).Key("skus.0.capabilities.#").HasValue("0"), + ), + }, + }) +} + +func TestAccDataSourceComputeSkus_withCapabilities(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_compute_skus", "test") + r := ComputeSkusDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.withCapabilities(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("skus.#").HasValue("1"), + check.That(data.ResourceName).Key("skus.1").DoesNotExist(), + check.That(data.ResourceName).Key("skus.0.name").HasValue("Standard_DS2_v2"), + check.That(data.ResourceName).Key("skus.0.capabilities.%").Exists(), + ), + }, + }) +} + +func TestAccDataSourceComputeSkus_allSkus(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_compute_skus", "test") + r := ComputeSkusDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + { + Config: r.allSkus(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("skus.0.name").Exists(), + check.That(data.ResourceName).Key("skus.1.name").Exists(), + check.That(data.ResourceName).Key("skus.2.name").Exists(), + ), + }, + }) +} + +func (ComputeSkusDataSource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_compute_skus" "test" { + name = "Standard_DS2_v2" + location = "%s" +} +`, data.Locations.Primary) +} + +func (ComputeSkusDataSource) withCapabilities(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_compute_skus" "test" { + name = "Standard_DS2_v2" + location = "%s" + include_capabilities = true +} +`, data.Locations.Primary) +} + +func (ComputeSkusDataSource) allSkus(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_compute_skus" "test" { + location = "%s" +} +`, data.Locations.Primary) +} diff --git a/internal/services/compute/registration.go b/internal/services/compute/registration.go index 638a52652d07..c8783a12ea81 100644 --- a/internal/services/compute/registration.go +++ b/internal/services/compute/registration.go @@ -26,6 +26,7 @@ func (r Registration) WebsiteCategories() []string { func (r Registration) SupportedDataSources() map[string]*pluginsdk.Resource { return map[string]*pluginsdk.Resource{ "azurerm_availability_set": dataSourceAvailabilitySet(), + "azurerm_compute_skus": dataSourceComputeSkus(), "azurerm_dedicated_host": dataSourceDedicatedHost(), "azurerm_dedicated_host_group": dataSourceDedicatedHostGroup(), "azurerm_disk_encryption_set": dataSourceDiskEncryptionSet(), diff --git a/website/docs/d/compute_skus.html.markdown b/website/docs/d/compute_skus.html.markdown new file mode 100644 index 000000000000..d79aca184762 --- /dev/null +++ b/website/docs/d/compute_skus.html.markdown @@ -0,0 +1,86 @@ +--- +subcategory: "Compute" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_compute_skus" +description: |- + Lists available Compute SKUs +--- + +# Data Source: azurerm_compute_skus + +This data source can be used to retrieve available Azure Compute SKUs. + +This can be used together with a `precondition` to check if a Virtual Machine SKU is available before the `apply` phase. + +## Example Usage + +```hcl +data "azurerm_compute_skus" "available" { + name = "Standard_D2s_v3" + location = "westus" +} + +output "available_skus" { + value = { + for sku in data.azurerm_compute_skus.available.skus : sku.name => sku + } +} + +# Changes to Outputs: +# + available_skus = { +# + Standard_D2s_v3 = { +# + capabilities = {} +# + location_restrictions = [] +# + name = "Standard_D2s_v3" +# + resource_type = "virtualMachines" +# + size = "D2s_v3" +# + tier = "Standard" +# + zone_restrictions = [] +# + zones = [ +# + "2", +# + "1", +# + "3", +# ] +# } +# } +``` + +## Argument Reference + +~> **Note:** Due to API limitations this data source will always get **ALL** available SKUs, regardless of any set filters. + +* `location` - (Required) The Azure location of the SKU. + +* `name` - (Optional) The name of the SKU, like `Standard_DS2_v2`. + +* `include_capabilities` - (Optional) Set to `true` if the SKUs capabilities should be included in the result. + +## Attributes Reference + +* `skus` - One or more `sku` blocks as defined below. + +--- + +The `sku` block exports the following: + +* `name` - The name of the SKU. + +* `resource_type` - The resource type of the SKU, like `virtualMachines` or `disks`. + +* `tier` - The tier of the SKU. + +* `size` - The size of the SKU. + +* `capabilities` - If included, this provides a map of the SKUs capabilities. + +* `zones` - If the SKU supports Availability Zones, this list contains the IDs of the zones at which the SKU is normally available. + +* `location_restrictions` - A list of locations at which the SKU is currently not available. The availability is tied to your Azure subscription ID. + +* `zone_restrictions` - A list of zones at which the SKU is currently not available. The availability is tied to your Azure subscription ID. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the SKUs.