From 2c968d6baa442d64b022d6072c44d68b011375af Mon Sep 17 00:00:00 2001 From: Shaoru Hu Date: Fri, 22 Aug 2025 20:28:38 +0000 Subject: [PATCH 1/4] migrate to track2 --- README.md | 29 +-- cache.go | 24 +-- cache_test.go | 312 +++++++++++++++---------------- clients.go | 65 ++----- data_test.go | 36 +--- disk_test.go | 140 +++++++------- fakes_test.go | 172 ++++------------- go.mod | 37 ++-- go.sum | 141 ++++++-------- hack/generate_vmsize_testdata.go | 37 ++-- interface.go | 18 +- sku.go | 182 +++++++----------- sku_test.go | 250 ++++++++++++------------- vmsize_test.go | 2 +- wrap.go | 8 +- 15 files changed, 622 insertions(+), 831 deletions(-) diff --git a/README.md b/README.md index c6c98e3..a3d8690 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ import ( "fmt" "os" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck - "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/skewer" + "github.com/Azure/skewer/v2" ) const ( @@ -35,12 +35,19 @@ func main() { os.Setenv(ClientID, "AAD Client ID or AppID") os.Setenv(ClientSecret, "AADClientSecretHere") sub := os.Getenv(SubscriptionID) - authorizer, err := auth.NewAuthorizerFromEnvironment() - // Create a skus client - client := compute.NewResourceSkusClient(sub) - client.Authorizer = authorizer + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + fmt.Printf("failed to get credential: %s", err) + os.Exit(1) + } + + client, err := armcompute.NewResourceSKUsClient(sub, cred, nil) + if err != nil { + fmt.Printf("failed to get client: %s", err) + os.Exit(1) + } - cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("southcentralus"), skewer.WithResourceClient(client)) + cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceSKUsClient(client)) if err != nil { fmt.Printf("failed to instantiate sku cache: %s", err) os.Exit(1) @@ -54,9 +61,9 @@ func main() { Once we have a cache, we can query against its contents: ```go -sku, found := cache.Get(context.Background, "standard_d4s_v3", skewer.VirtualMachines, "eastus") -if !found { - return fmt.Errorf("expected to find virtual machine sku standard_d4s_v3") +sku, err := cache.Get(context.Background(), "standard_d4s_v3", skewer.VirtualMachines, "eastus") +if err != nil { + return fmt.Errorf("failed to find virtual machine sku standard_d4s_v3: %s", err) } // Check for capabilities diff --git a/cache.go b/cache.go index 90517d3..8803127 100644 --- a/cache.go +++ b/cache.go @@ -70,27 +70,14 @@ func WithClient(client client) Option { } } -// WithResourceClient is a functional option to use a cache -// backed by a ResourceClient. -func WithResourceClient(client ResourceClient) Option { +// WithResourceSKUsClient is a functional option to use a cache +// backed by a ResourceSKUsClient. +func WithResourceSKUsClient(client ResourceSKUsClient) Option { return func(c *Config) (*Config, error) { if c.client != nil { return nil, &ErrClientNotNil{} } - c.client = newWrappedResourceClient(client) - return c, nil - } -} - -// WithResourceProviderClient is a functional option to use a cache -// backed by a ResourceProviderClient. -func WithResourceProviderClient(client ResourceProviderClient) Option { - return func(c *Config) (*Config, error) { - if c.client != nil { - return nil, &ErrClientNotNil{} - } - resourceClient := newWrappedResourceProviderClient(client) - c.client = newWrappedResourceClient(resourceClient) + c.client = newWrappedResourceSKUsClient(client) return c, nil } } @@ -283,7 +270,8 @@ func (c *Cache) Equal(other *Cache) bool { return false } for i := range c.data { - if c.data[i] != other.data[i] { + // only compare location, type and name + if !c.data[i].Equal(&other.data[i]) { return false } } diff --git a/cache_test.go b/cache_test.go index 3d27d85..5465ead 100644 --- a/cache_test.go +++ b/cache_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck - "github.com/Azure/go-autorest/autorest/to" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -62,65 +62,65 @@ func Test_Cache_GetVirtualMachines(t *testing.T) { func Test_Filter(t *testing.T) { cases := map[string]struct { - unfiltered []compute.ResourceSku + unfiltered []*armcompute.ResourceSKU condition FilterFn - expected []compute.ResourceSku + expected []*armcompute.ResourceSKU }{ "nil slice filters to nil slice": { condition: func(*SKU) bool { return true }, }, "empty slice filters to empty slice": { - unfiltered: []compute.ResourceSku{}, + unfiltered: []*armcompute.ResourceSKU{}, condition: func(*SKU) bool { return true }, - expected: []compute.ResourceSku{}, + expected: []*armcompute.ResourceSKU{}, }, "slice with non-matching element filters to empty slice": { - unfiltered: []compute.ResourceSku{ + unfiltered: []*armcompute.ResourceSKU{ { - ResourceType: to.StringPtr("nomatch"), + ResourceType: to.Ptr("nomatch"), }, }, condition: func(s *SKU) bool { return s.GetName() == "match" }, - expected: []compute.ResourceSku{}, + expected: []*armcompute.ResourceSKU{}, }, "slice with one matching element doesn't change": { - unfiltered: []compute.ResourceSku{ + unfiltered: []*armcompute.ResourceSKU{ { - ResourceType: to.StringPtr("match"), + ResourceType: to.Ptr("match"), }, }, condition: func(s *SKU) bool { return true }, - expected: []compute.ResourceSku{ + expected: []*armcompute.ResourceSKU{ { - ResourceType: to.StringPtr("match"), + ResourceType: to.Ptr("match"), }, }, }, "all matching elements removed": { - unfiltered: []compute.ResourceSku{ + unfiltered: []*armcompute.ResourceSKU{ { - ResourceType: to.StringPtr("match"), + ResourceType: to.Ptr("match"), }, { - ResourceType: to.StringPtr("nomatch"), + ResourceType: to.Ptr("nomatch"), }, { - ResourceType: to.StringPtr("match"), + ResourceType: to.Ptr("match"), }, { - ResourceType: to.StringPtr("unmatch"), + ResourceType: to.Ptr("unmatch"), }, { - ResourceType: to.StringPtr("match"), + ResourceType: to.Ptr("match"), }, }, condition: func(s *SKU) bool { return !s.IsResourceType("match") }, - expected: []compute.ResourceSku{ + expected: []*armcompute.ResourceSKU{ { - ResourceType: to.StringPtr("nomatch"), + ResourceType: to.Ptr("nomatch"), }, { - ResourceType: to.StringPtr("unmatch"), + ResourceType: to.Ptr("unmatch"), }, }, }, @@ -180,7 +180,7 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen cases := map[string]struct { sku string resourceType string - have []compute.ResourceSku + have []*armcompute.ResourceSKU found bool }{ "should return false with no data": { @@ -190,11 +190,11 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should match when found at index=0": { sku: "foo", resourceType: "bar", - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr("bar"), - Locations: &[]string{""}, + Name: to.Ptr("foo"), + ResourceType: to.Ptr("bar"), + Locations: []*string{to.Ptr("")}, }, }, found: true, @@ -202,15 +202,15 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should match when found at index=1": { sku: "foo", resourceType: "bar", - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("other"), - ResourceType: to.StringPtr("baz"), + Name: to.Ptr("other"), + ResourceType: to.Ptr("baz"), }, { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr("bar"), - Locations: &[]string{""}, + Name: to.Ptr("foo"), + ResourceType: to.Ptr("bar"), + Locations: []*string{to.Ptr("")}, }, }, found: true, @@ -218,15 +218,15 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should match regardless of sku capitalization": { sku: "foo", resourceType: "bar", - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("other"), - ResourceType: to.StringPtr("baz"), + Name: to.Ptr("other"), + ResourceType: to.Ptr("baz"), }, { - Name: to.StringPtr("FoO"), - ResourceType: to.StringPtr("bar"), - Locations: &[]string{""}, + Name: to.Ptr("FoO"), + ResourceType: to.Ptr("bar"), + Locations: []*string{to.Ptr("")}, }, }, found: true, @@ -234,9 +234,9 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should return false when no match exists": { sku: "foo", resourceType: "bar", - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("other"), + Name: to.Ptr("other"), }, }, }, @@ -281,21 +281,21 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen cases := map[string]struct { - have []compute.ResourceSku + have []*armcompute.ResourceSKU want []string }{ "should find 1 result": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -303,30 +303,30 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: []string{"1"}, }, "should find 2 results": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, }, { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"2"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("2")}, }, }, }, @@ -334,17 +334,17 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: []string{"1", "2"}, }, "should not find due to location mismatch": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "foobar", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("foobar"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("foobar"), - Zones: &[]string{"1"}, + Location: to.Ptr("foobar"), + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -352,23 +352,23 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: nil, }, "should not find due to location restriction": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, - Restrictions: &[]compute.ResourceSkuRestrictions{ + Restrictions: []*armcompute.ResourceSKURestrictions{ { - Type: compute.Location, - Values: &[]string{"baz"}, + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeLocation), + Values: []*string{to.Ptr("baz")}, }, }, }, @@ -376,25 +376,25 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: nil, }, "should not find due to zone restriction": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, - Restrictions: &[]compute.ResourceSkuRestrictions{ + Restrictions: []*armcompute.ResourceSKURestrictions{ { - Type: compute.Zone, - Values: &[]string{"baz"}, - RestrictionInfo: &compute.ResourceSkuRestrictionInfo{ - Zones: &[]string{"1"}, + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeZone), + Values: []*string{to.Ptr("baz")}, + RestrictionInfo: &armcompute.ResourceSKURestrictionInfo{ + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -418,7 +418,7 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen return a < b }), }...); diff != "" { - t.Errorf(diff) + t.Error(diff) } }) } @@ -426,21 +426,21 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //nolint:funlen cases := map[string]struct { - have []compute.ResourceSku + have []*armcompute.ResourceSKU want []string }{ "should find 1 result": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -448,17 +448,17 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: []string{"1"}, }, "should find 2 results": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1", "2"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1"), to.Ptr("2")}, }, }, }, @@ -466,17 +466,17 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: []string{"1", "2"}, }, "should not find due to size mismatch": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foobar"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foobar"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -484,17 +484,17 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: nil, }, "should not find due to location mismatch": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "foobar", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("foobar"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("foobar"), - Zones: &[]string{"1"}, + Location: to.Ptr("foobar"), + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -502,23 +502,23 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: nil, }, "should not find due to location restriction": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, - Restrictions: &[]compute.ResourceSkuRestrictions{ + Restrictions: []*armcompute.ResourceSKURestrictions{ { - Type: compute.Location, - Values: &[]string{"baz"}, + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeLocation), + Values: []*string{to.Ptr("baz")}, }, }, }, @@ -526,25 +526,25 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: nil, }, "should not find due to zone restriction": { - have: []compute.ResourceSku{ + have: []*armcompute.ResourceSKU{ { - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr(string(VirtualMachines)), - Locations: &[]string{ - "baz", + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), }, - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - Location: to.StringPtr("baz"), - Zones: &[]string{"1"}, + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, }, }, - Restrictions: &[]compute.ResourceSkuRestrictions{ + Restrictions: []*armcompute.ResourceSKURestrictions{ { - Type: compute.Zone, - Values: &[]string{"baz"}, - RestrictionInfo: &compute.ResourceSkuRestrictionInfo{ - Zones: &[]string{"1"}, + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeZone), + Values: []*string{to.Ptr("baz")}, + RestrictionInfo: &armcompute.ResourceSKURestrictionInfo{ + Zones: []*string{to.Ptr("1")}, }, }, }, @@ -568,7 +568,7 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli return a < b }), }...); diff != "" { - t.Fatalf(diff) + t.Fatal(diff) } }) } diff --git a/clients.go b/clients.go index ab90c85..620a24f 100644 --- a/clients.go +++ b/clients.go @@ -3,61 +3,36 @@ package skewer import ( "context" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/pkg/errors" ) -// wrappedResourceClient defines a wrapper for the typical Azure client -// signature to collect all resource skus from the iterator returned by ListComplete(). -type wrappedResourceClient struct { - client ResourceClient +// wrappedResourceSKUsClient defines a wrapper for the typical Azure track2 client +// signature to collect all resource skus from the pager returned by NewListPager(). +type wrappedResourceSKUsClient struct { + client ResourceSKUsClient } -func newWrappedResourceClient(client ResourceClient) *wrappedResourceClient { - return &wrappedResourceClient{client} +func newWrappedResourceSKUsClient(client ResourceSKUsClient) *wrappedResourceSKUsClient { + return &wrappedResourceSKUsClient{client} } -// List greedily traverses all returned sku pages -func (w *wrappedResourceClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]compute.ResourceSku, error) { - return iterate(ctx, filter, includeExtendedLocations, w.client.ListComplete) -} - -// wrappedResourceProviderClient defines a wrapper for the typical Azure client -// signature to collect all resource skus from the iterator returned by -// List(). It only differs from wrappedResourceClient in signature. -type wrappedResourceProviderClient struct { - client ResourceProviderClient -} - -func newWrappedResourceProviderClient(client ResourceProviderClient) *wrappedResourceProviderClient { - return &wrappedResourceProviderClient{client} -} - -//nolint:lll -func (w *wrappedResourceProviderClient) ListComplete(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultIterator, error) { - page, err := w.client.List(ctx, filter, includeExtendedLocations) - if err != nil { - return compute.ResourceSkusResultIterator{}, nil +func (w *wrappedResourceSKUsClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { + options := &armcompute.ResourceSKUsClientListOptions{} + if filter != "" { + options.Filter = &filter } - return compute.NewResourceSkusResultIterator(page), nil -} - -type iterFunc func(context.Context, string, string) (compute.ResourceSkusResultIterator, error) - -// iterate invokes fn to get an iterator, then drains it into an array. -func iterate(ctx context.Context, filter, includeExtendedLocations string, fn iterFunc) ([]compute.ResourceSku, error) { - iter, err := fn(ctx, filter, includeExtendedLocations) - if err != nil { - return nil, errors.Wrap(err, "could not list resource skus") + if includeExtendedLocations != "" { + options.IncludeExtendedLocations = &includeExtendedLocations } - - var skus []compute.ResourceSku - for iter.NotDone() { - skus = append(skus, iter.Value()) - if err := iter.NextWithContext(ctx); err != nil { - return nil, errors.Wrap(err, "could not iterate resource skus") + pager := w.client.NewListPager(options) + var skus []*armcompute.ResourceSKU + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not list resource skus") } + skus = append(skus, page.Value...) } - return skus, nil } diff --git a/data_test.go b/data_test.go index 329029c..db4378c 100644 --- a/data_test.go +++ b/data_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -30,26 +30,14 @@ func Test_Data(t *testing.T) { skus: dataWrapper.Value, } - resourceClient, err := newSuccessfulFakeResourceClient([][]compute.ResourceSku{ + resourceSKUsClient, err := newSuccessfulFakeResourceSKUsClient([][]*armcompute.ResourceSKU{ dataWrapper.Value, }) if err != nil { t.Error(err) } - chunkedResourceClient, err := newSuccessfulFakeResourceClient(chunk(dataWrapper.Value, 10)) - if err != nil { - t.Error(err) - } - - resourceProviderClient, err := newSuccessfulFakeResourceProviderClient([][]compute.ResourceSku{ - dataWrapper.Value, - }) - if err != nil { - t.Error(err) - } - - chunkedResourceProviderClient, err := newSuccessfulFakeResourceProviderClient(chunk(dataWrapper.Value, 10)) + chunkedResourceSKUsClient, err := newSuccessfulFakeResourceSKUsClient(chunk(dataWrapper.Value, 10)) if err != nil { t.Error(err) } @@ -59,24 +47,14 @@ func Test_Data(t *testing.T) { cases := map[string]struct { newCacheFunc NewCacheFunc }{ - "resourceClient": { - newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { - return NewCache(ctx, WithResourceClient(resourceClient), WithLocation("eastus")) - }, - }, - "chunkedResourceClient": { - newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { - return NewCache(ctx, WithResourceClient(chunkedResourceClient), WithLocation("eastus")) - }, - }, - "resourceProviderClient": { + "resourceSKUsClient": { newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { - return NewCache(ctx, WithResourceProviderClient(resourceProviderClient), WithLocation("eastus")) + return NewCache(ctx, WithResourceSKUsClient(resourceSKUsClient), WithLocation("eastus")) }, }, - "chunkedResourceProviderClient": { + "chunkedResourceSKUsClient": { newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { - return NewCache(ctx, WithResourceProviderClient(chunkedResourceProviderClient), WithLocation("eastus")) + return NewCache(ctx, WithResourceSKUsClient(chunkedResourceSKUsClient), WithLocation("eastus")) }, }, "wrappedClient": { diff --git a/disk_test.go b/disk_test.go index ef45aa9..d353c8f 100644 --- a/disk_test.go +++ b/disk_test.go @@ -3,53 +3,53 @@ package skewer import ( "testing" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck - "github.com/Azure/go-autorest/autorest/to" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" ) func Test_SKU_HasSCSISupport(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU expect bool }{ "empty capability list should return true (backward compatibility)": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, expect: true, }, "no disk controller capability should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{}, + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, }, expect: true, }, "SCSI only should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("SCSI"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI"), }, }, }, expect: true, }, "SCSI and NVMe should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("SCSI,NVMe"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI,NVMe"), }, }, }, expect: true, }, "NVMe only should return false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("NVMe"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("NVMe"), }, }, }, @@ -70,58 +70,58 @@ func Test_SKU_HasSCSISupport(t *testing.T) { func Test_SKU_HasNVMeSupport(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU expect bool }{ "empty capability list should return false": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, expect: false, }, "no disk controller capability should return false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{}, + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, }, expect: false, }, "SCSI only should return false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("SCSI"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI"), }, }, }, expect: false, }, "SCSI and NVMe should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("SCSI,NVMe"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI,NVMe"), }, }, }, expect: true, }, "NVMe only should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("NVMe"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("NVMe"), }, }, }, expect: true, }, "NVMe in mixed case should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(DiskControllerTypes), - Value: to.StringPtr("SCSI,NVMe,Other"), + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI,NVMe,Other"), }, }, }, @@ -142,52 +142,52 @@ func Test_SKU_HasNVMeSupport(t *testing.T) { func Test_SKU_SupportsNVMeEphemeralOSDisk(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU expect bool }{ "empty capability list should return false": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, expect: false, }, "no ephemeral placement capability should return false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("vCPUs"), - Value: to.StringPtr("8"), + Name: to.Ptr("vCPUs"), + Value: to.Ptr("8"), }, }, }, expect: false, }, "ResourceDisk only should return false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(SupportedEphemeralOSDiskPlacements), - Value: to.StringPtr("ResourceDisk"), + Name: to.Ptr(SupportedEphemeralOSDiskPlacements), + Value: to.Ptr("ResourceDisk"), }, }, }, expect: false, }, "NvmeDisk should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(SupportedEphemeralOSDiskPlacements), - Value: to.StringPtr("NvmeDisk"), + Name: to.Ptr(SupportedEphemeralOSDiskPlacements), + Value: to.Ptr("NvmeDisk"), }, }, }, expect: true, }, "ResourceDisk and NvmeDisk should return true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(SupportedEphemeralOSDiskPlacements), - Value: to.StringPtr("ResourceDisk,NvmeDisk"), + Name: to.Ptr(SupportedEphemeralOSDiskPlacements), + Value: to.Ptr("ResourceDisk,NvmeDisk"), }, }, }, @@ -208,42 +208,42 @@ func Test_SKU_SupportsNVMeEphemeralOSDisk(t *testing.T) { func Test_SKU_NVMeDiskSizeInMiB(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU expect int64 err string }{ "empty capability list should return error": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, err: (&ErrCapabilityNotFound{NvmeDiskSizeInMiB}).Error(), }, "no NVMe disk size capability should return error": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("vCPUs"), - Value: to.StringPtr("8"), + Name: to.Ptr("vCPUs"), + Value: to.Ptr("8"), }, }, }, err: (&ErrCapabilityNotFound{NvmeDiskSizeInMiB}).Error(), }, "valid NVMe disk size should return value": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(NvmeDiskSizeInMiB), - Value: to.StringPtr("1024000"), + Name: to.Ptr(NvmeDiskSizeInMiB), + Value: to.Ptr("1024000"), }, }, }, expect: 1024000, }, "invalid NVMe disk size should return parse error": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(NvmeDiskSizeInMiB), - Value: to.StringPtr("not-a-number"), + Name: to.Ptr(NvmeDiskSizeInMiB), + Value: to.Ptr("not-a-number"), }, }, }, diff --git a/fakes_test.go b/fakes_test.go index df14007..c617a5d 100644 --- a/fakes_test.go +++ b/fakes_test.go @@ -5,12 +5,13 @@ import ( "encoding/json" "os" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" ) // dataWrapper is a convenience wrapper for deserializing json testdata type dataWrapper struct { - Value []compute.ResourceSku `json:"value,omitempty"` + Value []*armcompute.ResourceSKU `json:"value,omitempty"` } // newDataWrapper takes a path to a list of compute skus and parses them @@ -32,124 +33,59 @@ func newDataWrapper(path string) (*dataWrapper, error) { // fakeClient is close to the simplest fake client implementation usable // by the cache. It does not use pagination like Azure clients. type fakeClient struct { - skus []compute.ResourceSku + skus []*armcompute.ResourceSKU err error } -func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]compute.ResourceSku, error) { +var _ client = &fakeClient{} + +func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { if f.err != nil { return nil, f.err } return f.skus, nil } -// fakeResourceClient is a fake client for the real Azure types. It -// returns a result iterator and can test against arbitrary sequences of -// return pages, injecting failure. -type fakeResourceClient struct { - res compute.ResourceSkusResultIterator - err error -} - -//nolint:lll -func (f *fakeResourceClient) ListComplete(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultIterator, error) { - if f.err != nil { - return compute.ResourceSkusResultIterator{}, f.err - } - return f.res, nil -} - -//nolint:deadcode,unused -func newFailingFakeResourceClient(reterr error) *fakeResourceClient { - return &fakeResourceClient{ - res: compute.ResourceSkusResultIterator{}, - err: reterr, - } -} - -// newSuccessfulFakeResourceClient takes a list of sku lists and returns -// a ResourceClient which iterates over all of them, mapping each sku -// list to a page of values. -func newSuccessfulFakeResourceClient(skuLists [][]compute.ResourceSku) (*fakeResourceClient, error) { - iterator, err := newFakeResourceSkusResultIterator(skuLists) - if err != nil { - return nil, err - } - - return &fakeResourceClient{ - res: iterator, - err: nil, - }, nil -} - -// fakeResourceProviderClient is a fake client for the real Azure types. It -// returns a result iterator and can test against arbitrary sequences of -// return pages, injecting failure. This uses the resource provider -// signature for testing purposes. -type fakeResourceProviderClient struct { - res compute.ResourceSkusResultPage - err error -} - -//nolint:lll -func (f *fakeResourceProviderClient) List(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultPage, error) { - if f.err != nil { - return compute.ResourceSkusResultPage{}, f.err - } - return f.res, nil -} - -//nolint:deadcode,unused -func newFailingFakeResourceProviderClient(reterr error) *fakeResourceProviderClient { - return &fakeResourceProviderClient{ - res: compute.ResourceSkusResultPage{}, - err: reterr, - } +// fakeResourceSKUsClient is a fake client for the real Azure types. +type fakeResourceSKUsClient struct { + skus [][]*armcompute.ResourceSKU + err error } -// newSuccessfulFakeResourceProviderClient takes a list of sku lists and returns -// a ResourceProviderClient which iterates over all of them, mapping each sku -// list to a page of values. -func newSuccessfulFakeResourceProviderClient(skuLists [][]compute.ResourceSku) (*fakeResourceProviderClient, error) { - page, err := newFakeResourceSkusResultPage(skuLists) - if err != nil { - return nil, err - } - - return &fakeResourceProviderClient{ - res: page, - err: nil, +var _ ResourceSKUsClient = &fakeResourceSKUsClient{} + +func (f *fakeResourceSKUsClient) NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] { + pageCount := 0 + pager := runtime.NewPager(runtime.PagingHandler[armcompute.ResourceSKUsClientListResponse]{ + More: func(current armcompute.ResourceSKUsClientListResponse) bool { + return pageCount < len(f.skus) + }, + Fetcher: func(ctx context.Context, current *armcompute.ResourceSKUsClientListResponse) (armcompute.ResourceSKUsClientListResponse, error) { + if pageCount >= len(f.skus) { + return armcompute.ResourceSKUsClientListResponse{}, f.err + } + pageCount += 1 + return armcompute.ResourceSKUsClientListResponse{ + ResourceSKUsResult: armcompute.ResourceSKUsResult{ + Value: f.skus[pageCount-1], + }, + }, f.err + }, + }) + return pager +} + +// newSuccessfulFakeResourceSKUsClient takes a list of sku lists and returns a ResourceSKUsClient. +func newSuccessfulFakeResourceSKUsClient(skuLists [][]*armcompute.ResourceSKU) (*fakeResourceSKUsClient, error) { + return &fakeResourceSKUsClient{ + skus: skuLists, + err: nil, }, nil } -// newFakeResourceSkusResultPage takes a list of sku lists and -// returns an iterator over all items, mapping each sku -// list to a page of values. -func newFakeResourceSkusResultPage(skuLists [][]compute.ResourceSku) (compute.ResourceSkusResultPage, error) { - pages := newPageList(skuLists) - newPage := compute.NewResourceSkusResultPage(compute.ResourceSkusResult{}, pages.next) - - if err := newPage.NextWithContext(context.Background()); err != nil { - return compute.ResourceSkusResultPage{}, err - } - return newPage, nil -} - -// newFakeResourceSkusResultIterator takes a list of sku lists and -// returns an iterator over all items, mapping each sku -// list to a page of values. -func newFakeResourceSkusResultIterator(skuLists [][]compute.ResourceSku) (compute.ResourceSkusResultIterator, error) { - pages := newPageList(skuLists) - newPage := compute.NewResourceSkusResultPage(compute.ResourceSkusResult{}, pages.next) - if err := newPage.NextWithContext(context.Background()); err != nil { - return compute.ResourceSkusResultIterator{}, err - } - return compute.NewResourceSkusResultIterator(newPage), nil -} - // chunk divides a list into count pieces. -func chunk(skus []compute.ResourceSku, count int) [][]compute.ResourceSku { - divided := [][]compute.ResourceSku{} +func chunk(skus []*armcompute.ResourceSKU, count int) [][]*armcompute.ResourceSKU { + divided := [][]*armcompute.ResourceSKU{} size := (len(skus) + count - 1) / count for i := 0; i < len(skus); i += size { end := i + size @@ -162,29 +98,3 @@ func chunk(skus []compute.ResourceSku, count int) [][]compute.ResourceSku { } return divided } - -// pageList is a utility type to help construct ResourceSkusResultIterators. -type pageList struct { - cursor int - pages []compute.ResourceSkusResult -} - -func newPageList(skuLists [][]compute.ResourceSku) *pageList { - list := &pageList{} - for i := 0; i < len(skuLists); i++ { - list.pages = append(list.pages, compute.ResourceSkusResult{ - Value: &skuLists[i], - }) - } - return list -} - -// next underpins ResourceSkusResultIterator's NextWithDone() method. -func (p *pageList) next(context.Context, compute.ResourceSkusResult) (compute.ResourceSkusResult, error) { - if p.cursor >= len(p.pages) { - return compute.ResourceSkusResult{}, nil - } - old := p.cursor - p.cursor++ - return p.pages[old], nil -} diff --git a/go.mod b/go.mod index 4af3a20..e0430ff 100644 --- a/go.mod +++ b/go.mod @@ -1,34 +1,33 @@ -module github.com/Azure/skewer +module github.com/Azure/skewer/v2 -go 1.18 +go 1.23.0 + +toolchain go1.24.4 require ( - github.com/Azure/azure-sdk-for-go v68.0.0+incompatible - github.com/Azure/go-autorest/autorest v0.11.29 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/google/go-cmp v0.5.9 github.com/pkg/errors v0.9.1 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 - github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 - github.com/stretchr/testify v1.8.4 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.0.0 + github.com/stretchr/testify v1.10.0 ) require ( - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.11.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 236c9a5..9f62c81 100644 --- a/go.sum +++ b/go.sum @@ -1,97 +1,66 @@ -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= -github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= -github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0 h1:z7Mqz6l0EFH549GvHEqfjKvi+cRScxLWbaoeLm9wxVQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0/go.mod h1:v6gbfH+7DG7xH2kUNs+ZJ9tF6O3iNnR85wMtmr+F54o= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.0.0 h1:ConMW11qUpZOqv3OXCPJhl1icxETBs+Ey93Nw8lK3fM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.0.0/go.mod h1:B/orXAsKLlSau3LDJu3SoVqVuifYJf4jdlXstaefJZA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/generate_vmsize_testdata.go b/hack/generate_vmsize_testdata.go index ba76b34..a4eecd0 100644 --- a/hack/generate_vmsize_testdata.go +++ b/hack/generate_vmsize_testdata.go @@ -6,35 +6,40 @@ import ( "os" "text/template" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck - "github.com/Azure/go-autorest/autorest/azure/auth" - "github.com/Azure/skewer/testdata" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/skewer/v2/testdata" ) func getSKUs(subscriptionID, region string) (map[string]testdata.SKUInfo, error) { - authorizer, err := auth.NewAuthorizerFromCLI() + cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { return nil, err } - // Create a new compute client - client := compute.NewResourceSkusClient(subscriptionID) - client.Authorizer = authorizer - - // List SKUs for the specified region - skuList, err := client.List(context.Background(), region, "") + client, err := armcompute.NewResourceSKUsClient(subscriptionID, cred, nil) if err != nil { return nil, err } + ctx := context.Background() + filter := fmt.Sprintf("location eq '%s'", region) + pager := client.NewListPager(&armcompute.ResourceSKUsClientListOptions{Filter: &filter}) + skus := map[string]testdata.SKUInfo{} - for _, sku := range skuList.Values() { - if *sku.ResourceType == "virtualMachines" { - if _, ok := skus[*sku.Name]; !ok { - skuInfo := testdata.SKUInfo{ - Size: *sku.Size, + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + for _, v := range page.Value { + if v.ResourceType != nil && *v.ResourceType == "virtualMachines" { + if _, ok := skus[*v.Name]; !ok { + skuInfo := testdata.SKUInfo{ + Size: *v.Size, + } + skus[*v.Name] = skuInfo } - skus[*sku.Name] = skuInfo } } } diff --git a/interface.go b/interface.go index 9cb506b..fd37584 100644 --- a/interface.go +++ b/interface.go @@ -3,22 +3,18 @@ package skewer import ( "context" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" ) -// ResourceClient is the required Azure client interface used to populate skewer's data. -type ResourceClient interface { - ListComplete(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultIterator, error) +// ResourceSKUsClient is the required Azure track2 client interface used to populate skewer's data. +type ResourceSKUsClient interface { + NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] } -// ResourceProviderClient is a convenience interface for uses cases -// specific to Azure resource providers. -type ResourceProviderClient interface { - List(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultPage, error) -} +var _ ResourceSKUsClient = &armcompute.ResourceSKUsClient{} // client defines the internal interface required by the skewer Cache. -// TODO(ace): implement a lazy iterator with caching (and a cursor?) type client interface { - List(ctx context.Context, filter, includeExtendedLocations string) ([]compute.ResourceSku, error) + List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) } diff --git a/sku.go b/sku.go index 7a6dbad..816450e 100644 --- a/sku.go +++ b/sku.go @@ -5,12 +5,12 @@ import ( "strconv" "strings" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/pkg/errors" ) // SKU wraps an Azure compute SKU with richer functionality -type SKU compute.ResourceSku +type SKU armcompute.ResourceSKU // ErrCapabilityNotFound will be returned when a capability could not be // found, even without a value. @@ -144,11 +144,8 @@ func (s *SKU) GetCPUArchitectureType() (string, error) { // capability is not found, the value was nil, or the value could not be // parsed as an integer. func (s *SKU) GetCapabilityIntegerQuantity(name string) (int64, error) { - if s.Capabilities == nil { - return -1, &ErrCapabilityNotFound{name} - } - for _, capability := range *s.Capabilities { - if capability.Name != nil && *capability.Name == name { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && *capability.Name == name { if capability.Value != nil { intVal, err := strconv.ParseInt(*capability.Value, ten, sixtyFour) if err != nil { @@ -167,11 +164,8 @@ func (s *SKU) GetCapabilityIntegerQuantity(name string) (int64, error) { // if the capability is not found, the value was nil, or the value could // not be parsed as an integer. func (s *SKU) GetCapabilityFloatQuantity(name string) (float64, error) { - if s.Capabilities == nil { - return -1, &ErrCapabilityNotFound{name} - } - for _, capability := range *s.Capabilities { - if capability.Name != nil && *capability.Name == name { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && *capability.Name == name { if capability.Value != nil { intVal, err := strconv.ParseFloat(*capability.Value, sixtyFour) if err != nil { @@ -188,11 +182,8 @@ func (s *SKU) GetCapabilityFloatQuantity(name string) (float64, error) { // GetCapabilityString retrieves string capability with the provided name. // It errors if the capability is not found or the value was nil func (s *SKU) GetCapabilityString(name string) (string, error) { - if s.Capabilities == nil { - return "", &ErrCapabilityNotFound{name} - } - for _, capability := range *s.Capabilities { - if capability.Name != nil && *capability.Name == name { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && *capability.Name == name { if capability.Value != nil { return *capability.Value, nil } @@ -207,11 +198,8 @@ func (s *SKU) GetCapabilityString(name string) (string, error) { // "EncryptionAtHostSupported", "AcceleratedNetworkingEnabled", and // "RdmaEnabled" func (s *SKU) HasCapability(name string) bool { - if s.Capabilities == nil { - return false - } - for _, capability := range *s.Capabilities { - if capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { return capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) } } @@ -227,19 +215,16 @@ func (s *SKU) HasCapability(name string) bool { // available. // For per zone capability check, use "HasCapabilityInZone" func (s *SKU) HasZonalCapability(name string) bool { - if s.LocationInfo == nil { - return false - } - for _, locationInfo := range *s.LocationInfo { - if locationInfo.ZoneDetails == nil { + for _, locationInfo := range s.LocationInfo { + if locationInfo == nil { continue } - for _, zoneDetails := range *locationInfo.ZoneDetails { - if zoneDetails.Capabilities == nil { + for _, zoneDetails := range locationInfo.ZoneDetails { + if zoneDetails == nil { continue } - for _, capability := range *zoneDetails.Capabilities { - if capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range zoneDetails.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) { return true } @@ -253,32 +238,27 @@ func (s *SKU) HasZonalCapability(name string) bool { // HasCapabilityInZone return true if the specified capability name is supported in the // specified zone. func (s *SKU) HasCapabilityInZone(name, zone string) bool { - if s.LocationInfo == nil { - return false - } - for _, locationInfo := range *s.LocationInfo { - if locationInfo.ZoneDetails == nil { + for _, locationInfo := range s.LocationInfo { + if locationInfo == nil { continue } - for _, zoneDetails := range *locationInfo.ZoneDetails { - if zoneDetails.Capabilities == nil { + for _, zoneDetails := range locationInfo.ZoneDetails { + if zoneDetails == nil { continue } foundZone := false - if zoneDetails.Name != nil { - for _, zoneName := range *zoneDetails.Name { - if strings.EqualFold(zone, zoneName) { - foundZone = true - break - } + for _, zoneName := range zoneDetails.Name { + if zoneName != nil && strings.EqualFold(zone, *zoneName) { + foundZone = true + break } } if !foundZone { continue } - for _, capability := range *zoneDetails.Capabilities { - if capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range zoneDetails.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) { return true } @@ -294,11 +274,8 @@ func (s *SKU) HasCapabilityInZone(name, zone string) bool { // the desired substring. An example is "HyperVGenerations" which may be // "V1,V2" func (s *SKU) HasCapabilityWithSeparator(name, value string) bool { - if s.Capabilities == nil { - return false - } - for _, capability := range *s.Capabilities { - if capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { return capability.Value != nil && strings.Contains(normalizeLocation(*capability.Value), normalizeLocation(value)) } } @@ -314,11 +291,8 @@ func (s *SKU) HasCapabilityWithSeparator(name, value string) bool { // "CombinedTempDiskAndCachedWriteBytesPerSecond", "UncachedDiskIOPS", // and "UncachedDiskBytesPerSecond" func (s *SKU) HasCapabilityWithMinCapacity(name string, value int64) (bool, error) { - if s.Capabilities == nil { - return false, nil - } - for _, capability := range *s.Capabilities { - if capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil { intVal, err := strconv.ParseInt(*capability.Value, ten, sixtyFour) if err != nil { @@ -337,18 +311,13 @@ func (s *SKU) HasCapabilityWithMinCapacity(name string, value int64) (bool, erro // IsAvailable returns true when the requested location matches one on // the sku, and there are no total restrictions on the location. func (s *SKU) IsAvailable(location string) bool { - if s.LocationInfo == nil { - return false - } - for _, locationInfo := range *s.LocationInfo { - if locationInfo.Location != nil { + for _, locationInfo := range s.LocationInfo { + if locationInfo != nil && locationInfo.Location != nil { if locationEquals(*locationInfo.Location, location) { - if s.Restrictions != nil { - for _, restriction := range *s.Restrictions { - // Can't deploy to any zones in this location. We're done. - if restriction.Type == compute.Location { - return false - } + for _, restriction := range s.Restrictions { + // Can't deploy to any zones in this location. We're done. + if restriction != nil && restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { + return false } } return true @@ -361,16 +330,13 @@ func (s *SKU) IsAvailable(location string) bool { // IsRestricted returns true when a location restriction exists for // this SKU. func (s *SKU) IsRestricted(location string) bool { - if s.Restrictions == nil { - return false - } - for _, restriction := range *s.Restrictions { - if restriction.Values == nil { + for _, restriction := range s.Restrictions { + if restriction == nil || restriction.Values == nil { continue } - for _, candidate := range *restriction.Values { + for _, candidate := range restriction.Values { // Can't deploy in this location. We're done. - if locationEquals(candidate, location) && restriction.Type == compute.Location { + if candidate != nil && locationEquals(*candidate, location) && restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { return true } } @@ -439,25 +405,25 @@ func (s *SKU) GetLocation() (string, error) { return "", fmt.Errorf("sku had nil location array") } - if len(*s.Locations) < 1 { + if len(s.Locations) < 1 { return "", fmt.Errorf("sku had no locations") } - if len(*s.Locations) > 1 { + if len(s.Locations) > 1 { return "", fmt.Errorf("sku had multiple locations, refusing to disambiguate") } - return (*s.Locations)[0], nil + if s.Locations[0] == nil { + return "", fmt.Errorf("sku had nil location") + } + + return *s.Locations[0], nil } // HasLocation returns true if the given sku exposes this region for deployment. func (s *SKU) HasLocation(location string) bool { - if s.Locations == nil { - return false - } - - for _, candidate := range *s.Locations { - if locationEquals(candidate, location) { + for _, candidate := range s.Locations { + if candidate != nil && locationEquals(*candidate, location) { return true } } @@ -468,19 +434,15 @@ func (s *SKU) HasLocation(location string) bool { // HasLocationRestriction returns true if the location is restricted for // this sku. func (s *SKU) HasLocationRestriction(location string) bool { - if s.Restrictions == nil { - return false - } - - for _, restriction := range *s.Restrictions { - if restriction.Type != compute.Location { + for _, restriction := range s.Restrictions { + if restriction.Type != nil && *restriction.Type != armcompute.ResourceSKURestrictionsTypeLocation { continue } if restriction.Values == nil { continue } - for _, candidate := range *restriction.Values { - if locationEquals(candidate, location) { + for _, candidate := range restriction.Values { + if candidate != nil && locationEquals(*candidate, location) { return true } } @@ -518,33 +480,33 @@ func (s *SKU) AvailabilityZones(location string) map[string]bool { //nolint:gocy availableZones := make(map[string]bool) restrictedZones := make(map[string]bool) - for _, locationInfo := range *s.LocationInfo { - if locationInfo.Location == nil { + for _, locationInfo := range s.LocationInfo { + if locationInfo == nil || locationInfo.Location == nil { continue } if locationEquals(*locationInfo.Location, location) { // add all zones - if locationInfo.Zones != nil { - for _, zone := range *locationInfo.Zones { - availableZones[zone] = true + for _, zone := range locationInfo.Zones { + if zone != nil { + availableZones[*zone] = true } } // iterate restrictions, remove any restricted zones for this location - if s.Restrictions != nil { - for _, restriction := range *s.Restrictions { - if restriction.Values != nil { - for _, candidate := range *restriction.Values { - if locationEquals(candidate, location) { - if restriction.Type == compute.Location { - // Can't deploy in this location. We're done. - return nil - } + for _, restriction := range s.Restrictions { + if restriction != nil { + for _, candidate := range restriction.Values { + if candidate != nil && locationEquals(*candidate, location) { + if restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { + // Can't deploy in this location. We're done. + return nil + } - if restriction.RestrictionInfo != nil && restriction.RestrictionInfo.Zones != nil { - // remove restricted zones - for _, zone := range *restriction.RestrictionInfo.Zones { - restrictedZones[zone] = true + if restriction.RestrictionInfo != nil { + // remove restricted zones + for _, zone := range restriction.RestrictionInfo.Zones { + if zone != nil { + restrictedZones[*zone] = true } } } @@ -565,7 +527,7 @@ func (s *SKU) AvailabilityZones(location string) map[string]bool { //nolint:gocy // Equal returns true when two skus have the same location, type, and name. func (s *SKU) Equal(other *SKU) bool { location, localErr := s.GetLocation() - otherLocation, otherErr := s.GetLocation() + otherLocation, otherErr := other.GetLocation() return strings.EqualFold(s.GetResourceType(), other.GetResourceType()) && strings.EqualFold(s.GetName(), other.GetName()) && locationEquals(location, otherLocation) && diff --git a/sku_test.go b/sku_test.go index ec79d0a..07c146b 100644 --- a/sku_test.go +++ b/sku_test.go @@ -4,36 +4,36 @@ import ( "fmt" "testing" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck - "github.com/Azure/go-autorest/autorest/to" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/google/go-cmp/cmp" ) func Test_SKU_GetCapabilityQuantity(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU capability string expect int64 err string }{ "empty capability list should return capability not found": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, capability: "", err: (&ErrCapabilityNotFound{""}).Error(), }, "empty capability should not match sku with empty list of capabilities": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{}, + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, }, capability: "", err: (&ErrCapabilityNotFound{""}).Error(), }, "empty capability should fail to parse when not integer": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(""), - Value: to.StringPtr("False"), + Name: to.Ptr(""), + Value: to.Ptr("False"), }, }, }, @@ -41,11 +41,11 @@ func Test_SKU_GetCapabilityQuantity(t *testing.T) { err: "CapabilityValueParse: failed to parse string 'False' as int64, error: 'strconv.ParseInt: parsing \"False\": invalid syntax'", //nolint:lll }, "foo capability should return successfully with integer": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("100"), + Name: to.Ptr("foo"), + Value: to.Ptr("100"), }, }, }, @@ -80,59 +80,59 @@ func Test_SKU_GetCapabilityQuantity(t *testing.T) { func Test_SKU_HasCapability(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU capability string expect bool }{ "empty capability should not match empty sku": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, capability: "", }, "empty capability should not match sku with empty list of capabilities": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{}, + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, }, capability: "", }, "empty capability should not match when present and false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(""), - Value: to.StringPtr("False"), + Name: to.Ptr(""), + Value: to.Ptr("False"), }, }, }, capability: "", }, "empty capability should not match when present and weird value": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(""), - Value: to.StringPtr("foobar"), + Name: to.Ptr(""), + Value: to.Ptr("foobar"), }, }, }, capability: "", }, "foo capability should not match when false": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("False"), + Name: to.Ptr("foo"), + Value: to.Ptr("False"), }, }, }, capability: "foo", }, "foo capability should match when true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("True"), + Name: to.Ptr("foo"), + Value: to.Ptr("True"), }, }, }, @@ -154,28 +154,28 @@ func Test_SKU_HasCapability(t *testing.T) { func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU capability string capacity int64 expect bool err error }{ "empty capability should not match empty sku": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, capability: "", }, "empty capability should not match sku with empty list of capabilities": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{}, + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, }, capability: "", }, "empty capability should error when present and weird value": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(""), - Value: to.StringPtr("foobar"), + Name: to.Ptr(""), + Value: to.Ptr("foobar"), }, }, }, @@ -183,11 +183,11 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { err: fmt.Errorf("failed to parse string 'foobar' as int64: strconv.ParseInt: parsing \"foobar\": invalid syntax"), }, "empty capability should match when present with zero capacity and requesting zero": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr(""), - Value: to.StringPtr("0"), + Name: to.Ptr(""), + Value: to.Ptr("0"), }, }, }, @@ -195,11 +195,11 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { expect: true, }, "foo capability should not match when present and less than capacity": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("100"), + Name: to.Ptr("foo"), + Value: to.Ptr("100"), }, }, }, @@ -207,11 +207,11 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { capacity: 200, }, "foo capability should match when true": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("10"), + Name: to.Ptr("foo"), + Value: to.Ptr("10"), }, }, }, @@ -241,27 +241,27 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { func Test_SKU_GetResourceTypeAndName(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU expectName string expectResourceType string }{ "nil resourceType should return empty string": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, expectResourceType: "", expectName: "", }, "empty resourceType should return empty string": { - sku: compute.ResourceSku{ - Name: to.StringPtr(""), - ResourceType: to.StringPtr(""), + sku: armcompute.ResourceSKU{ + Name: to.Ptr(""), + ResourceType: to.Ptr(""), }, expectResourceType: "", expectName: "", }, "populated resourceType should return correctly": { - sku: compute.ResourceSku{ - Name: to.StringPtr("foo"), - ResourceType: to.StringPtr("foo"), + sku: armcompute.ResourceSKU{ + Name: to.Ptr("foo"), + ResourceType: to.Ptr("foo"), }, expectResourceType: "foo", expectName: "foo", @@ -284,30 +284,30 @@ func Test_SKU_GetResourceTypeAndName(t *testing.T) { func Test_SKU_IsResourceType(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU resourceType string expect bool }{ "nil resourceType should not match anything": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, resourceType: "", }, "empty resourceType should match empty string": { - sku: compute.ResourceSku{ - ResourceType: to.StringPtr(""), + sku: armcompute.ResourceSKU{ + ResourceType: to.Ptr(""), }, resourceType: "", expect: true, }, "empty resourceType should not match non-empty string": { - sku: compute.ResourceSku{ - ResourceType: to.StringPtr(""), + sku: armcompute.ResourceSKU{ + ResourceType: to.Ptr(""), }, resourceType: "foo", }, "populated resourceType should match itself": { - sku: compute.ResourceSku{ - ResourceType: to.StringPtr("foo"), + sku: armcompute.ResourceSKU{ + ResourceType: to.Ptr("foo"), }, resourceType: "foo", expect: true, @@ -327,48 +327,48 @@ func Test_SKU_IsResourceType(t *testing.T) { func Test_SKU_GetLocation(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU expect string expectErr string }{ "nil locations should return empty string": { - sku: compute.ResourceSku{}, + sku: armcompute.ResourceSKU{}, expect: "", }, "empty array of locations return empty string": { - sku: compute.ResourceSku{ - Locations: &[]string{}, + sku: armcompute.ResourceSKU{ + Locations: []*string{}, }, expect: "", }, "single empty value should return empty string": { - sku: compute.ResourceSku{ - Locations: &[]string{ - "", + sku: armcompute.ResourceSKU{ + Locations: []*string{ + to.Ptr(""), }, }, expect: "", }, "populated location should return correctly": { - sku: compute.ResourceSku{ - Locations: &[]string{ - "foo", + sku: armcompute.ResourceSKU{ + Locations: []*string{ + to.Ptr("foo"), }, }, expect: "foo", }, "should return error with multiple choices": { - sku: compute.ResourceSku{ - Locations: &[]string{ - "bar", - "foo", + sku: armcompute.ResourceSKU{ + Locations: []*string{ + to.Ptr("bar"), + to.Ptr("foo"), }, }, expectErr: "sku had multiple locations, refusing to disambiguate", }, "should return error with no choices": { - sku: compute.ResourceSku{ - Locations: &[]string{}, + sku: armcompute.ResourceSKU{ + Locations: []*string{}, }, expectErr: "sku had no locations", }, @@ -399,22 +399,22 @@ func Test_SKU_AvailabilityZones(t *testing.T) {} //nolint:funlen func Test_SKU_HasCapabilityInZone(t *testing.T) { cases := map[string]struct { - sku compute.ResourceSku + sku armcompute.ResourceSKU capability string zone string expect bool }{ "should return false when capability is false": { - sku: compute.ResourceSku{ - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - ZoneDetails: &[]compute.ResourceSkuZoneDetails{ + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ { - Name: &[]string{"1", "3"}, - Capabilities: &[]compute.ResourceSkuCapabilities{ + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("False"), + Name: to.Ptr("foo"), + Value: to.Ptr("False"), }, }, }, @@ -427,16 +427,16 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: false, }, "should return false when zone doesn't match": { - sku: compute.ResourceSku{ - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - ZoneDetails: &[]compute.ResourceSkuZoneDetails{ + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ { - Name: &[]string{"1", "3"}, - Capabilities: &[]compute.ResourceSkuCapabilities{ + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("True"), + Name: to.Ptr("foo"), + Value: to.Ptr("True"), }, }, }, @@ -449,11 +449,11 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: false, }, "should not return true when the capability is not set in availability zone but set on sku capability": { - sku: compute.ResourceSku{ - Capabilities: &[]compute.ResourceSkuCapabilities{ + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("True"), + Name: to.Ptr("foo"), + Value: to.Ptr("True"), }, }, }, @@ -462,16 +462,16 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: false, }, "should return true when capability and zone match": { - sku: compute.ResourceSku{ - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - ZoneDetails: &[]compute.ResourceSkuZoneDetails{ + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ { - Name: &[]string{"1", "3"}, - Capabilities: &[]compute.ResourceSkuCapabilities{ + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("True"), + Name: to.Ptr("foo"), + Value: to.Ptr("True"), }, }, }, @@ -484,16 +484,16 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: true, }, "should return true when capability and zone match for zone 3": { - sku: compute.ResourceSku{ - LocationInfo: &[]compute.ResourceSkuLocationInfo{ + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ { - ZoneDetails: &[]compute.ResourceSkuZoneDetails{ + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ { - Name: &[]string{"1", "3"}, - Capabilities: &[]compute.ResourceSkuCapabilities{ + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ { - Name: to.StringPtr("foo"), - Value: to.StringPtr("True"), + Name: to.Ptr("foo"), + Value: to.Ptr("True"), }, }, }, @@ -528,32 +528,32 @@ func Test_SKU_Includes(t *testing.T) { "empty list should not include": { skuList: []SKU{}, sku: SKU{ - Name: to.StringPtr("foo"), + Name: to.Ptr("foo"), }, expect: false, }, "missing name should not include": { skuList: []SKU{ { - Name: to.StringPtr("foo"), + Name: to.Ptr("foo"), }, }, sku: SKU{ - Name: to.StringPtr("bar"), + Name: to.Ptr("bar"), }, expect: false, }, "name is included": { skuList: []SKU{ { - Name: to.StringPtr("foo"), + Name: to.Ptr("foo"), }, { - Name: to.StringPtr("bar"), + Name: to.Ptr("bar"), }, }, sku: SKU{ - Name: to.StringPtr("bar"), + Name: to.Ptr("bar"), }, expect: true, }, diff --git a/vmsize_test.go b/vmsize_test.go index 5d711b5..03e6bfd 100644 --- a/vmsize_test.go +++ b/vmsize_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/Azure/skewer/testdata" + "github.com/Azure/skewer/v2/testdata" "github.com/stretchr/testify/assert" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" diff --git a/wrap.go b/wrap.go index 711555d..98f4edf 100644 --- a/wrap.go +++ b/wrap.go @@ -1,13 +1,15 @@ package skewer -import "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck +import "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" // Wrap takes an array of compute resource skus and wraps them into an // array of our richer type. -func Wrap(in []compute.ResourceSku) []SKU { +func Wrap(in []*armcompute.ResourceSKU) []SKU { out := make([]SKU, len(in)) for index, value := range in { - out[index] = SKU(value) + if value != nil { + out[index] = (SKU)(*value) + } } return out } From 0fbafccb79cc2b947c987ad22b6dffae0601e339 Mon Sep 17 00:00:00 2001 From: Shaoru Hu Date: Fri, 19 Sep 2025 20:07:02 +0000 Subject: [PATCH 2/4] fix --- README.md | 23 +++++---------- cache.go | 3 +- cache_test.go | 5 ---- data_test.go | 12 ++------ example/example.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++ fakes_test.go | 4 +-- sku_test.go | 4 +-- vmsize.go | 2 +- wrap.go | 2 +- 9 files changed, 87 insertions(+), 39 deletions(-) create mode 100644 example/example.go diff --git a/README.md b/README.md index a3d8690..b8db8ea 100644 --- a/README.md +++ b/README.md @@ -22,25 +22,16 @@ import ( "github.com/Azure/skewer/v2" ) -const ( - SubscriptionID = "AZURE_SUBSCRIPTION_ID" - TenantID = "AZURE_TENANT_ID" - ClientID = "AZURE_CLIENT_ID" - ClientSecret = "AZURE_CLIENT_SECRET" -) - func main() { - os.Setenv(SubscriptionID, "subscriptionID") - os.Setenv(TenantID, "TenantID") - os.Setenv(ClientID, "AAD Client ID or AppID") - os.Setenv(ClientSecret, "AADClientSecretHere") - sub := os.Getenv(SubscriptionID) + // az login + // export AZURE_SUBSCRIPTION_ID="subscription-id" + sub := os.Getenv("AZURE_SUBSCRIPTION_ID") cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { fmt.Printf("failed to get credential: %s", err) os.Exit(1) } - + client, err := armcompute.NewResourceSKUsClient(sub, cred, nil) if err != nil { fmt.Printf("failed to get client: %s", err) @@ -63,12 +54,12 @@ Once we have a cache, we can query against its contents: ```go sku, err := cache.Get(context.Background(), "standard_d4s_v3", skewer.VirtualMachines, "eastus") if err != nil { - return fmt.Errorf("failed to find virtual machine sku standard_d4s_v3: %s", err) + return fmt.Errorf("failed to find virtual machine sku standard_d4s_v3: %s", err) } // Check for capabilities if sku.IsEphemeralOSDiskSupported() { - fmt.Println("SKU %s supports ephemeral OS disk!", sku.GetName()) + fmt.Printf("SKU %s supports ephemeral OS disk!\n", sku.GetName()) } cpu, err := sku.VCPU() @@ -81,7 +72,7 @@ if err != nil { return fmt.Errorf("failed to parse memory from sku: %s", err) } -fmt.Printf("vm sku %s has %d vCPU cores and %.2fGi of memory", sku.GetName(), cpu, memory) +fmt.Printf("vm sku %s has %d vCPU cores and %.2fGi of memory\n", sku.GetName(), cpu, memory) ``` # Development diff --git a/cache.go b/cache.go index 8803127..c056efe 100644 --- a/cache.go +++ b/cache.go @@ -270,7 +270,8 @@ func (c *Cache) Equal(other *Cache) bool { return false } for i := range c.data { - // only compare location, type and name + // we can't use c.data[i] != other.data[i] since there are many pointers + // use Equal to compare location, type and name if !c.data[i].Equal(&other.data[i]) { return false } diff --git a/cache_test.go b/cache_test.go index 5465ead..05cebda 100644 --- a/cache_test.go +++ b/cache_test.go @@ -38,7 +38,6 @@ func Test_WithLocation(t *testing.T) { } for name, tc := range cases { - tc := tc t.Run(name, func(t *testing.T) { cache, err := NewStaticCache(nil, tc.options...) if err != nil { @@ -126,7 +125,6 @@ func Test_Filter(t *testing.T) { }, } for name, tc := range cases { - tc := tc t.Run(name, func(t *testing.T) { result := Filter(Wrap(tc.unfiltered), tc.condition) if diff := cmp.Diff(result, Wrap(tc.expected), []cmp.Option{ @@ -243,7 +241,6 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen } for name, tc := range cases { - tc := tc t.Run(name, func(t *testing.T) { cache := &Cache{ data: Wrap(tc.have), @@ -405,7 +402,6 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen } for name, tc := range cases { - tc := tc t.Run(name, func(t *testing.T) { cache, err := NewStaticCache(Wrap(tc.have), WithLocation("baz")) if err != nil { @@ -555,7 +551,6 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli } for name, tc := range cases { - tc := tc t.Run(name, func(t *testing.T) { cache, err := NewStaticCache(Wrap(tc.have), WithLocation("baz")) if err != nil { diff --git a/data_test.go b/data_test.go index db4378c..070b8c7 100644 --- a/data_test.go +++ b/data_test.go @@ -30,17 +30,9 @@ func Test_Data(t *testing.T) { skus: dataWrapper.Value, } - resourceSKUsClient, err := newSuccessfulFakeResourceSKUsClient([][]*armcompute.ResourceSKU{ - dataWrapper.Value, - }) - if err != nil { - t.Error(err) - } + resourceSKUsClient := newSuccessfulFakeResourceSKUsClient([][]*armcompute.ResourceSKU{dataWrapper.Value}) - chunkedResourceSKUsClient, err := newSuccessfulFakeResourceSKUsClient(chunk(dataWrapper.Value, 10)) - if err != nil { - t.Error(err) - } + chunkedResourceSKUsClient := newSuccessfulFakeResourceSKUsClient(chunk(dataWrapper.Value, 10)) ctx := context.Background() diff --git a/example/example.go b/example/example.go new file mode 100644 index 0000000..8f50d31 --- /dev/null +++ b/example/example.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + + "github.com/Azure/skewer/v2" +) + +func main() { + // az login + // export AZURE_SUBSCRIPTION_ID="subscription-id" + sub := os.Getenv("AZURE_SUBSCRIPTION_ID") + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + fmt.Printf("failed to get credential: %s", err) + os.Exit(1) + } + + client, err := armcompute.NewResourceSKUsClient(sub, cred, nil) + if err != nil { + fmt.Printf("failed to get client: %s", err) + os.Exit(1) + } + + cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceSKUsClient(client)) + if err != nil { + fmt.Printf("failed to instantiate sku cache: %s", err) + os.Exit(1) + } + + for _, sku := range cache.List(context.Background()) { + fmt.Printf("sku: %s\n", sku.GetName()) + } + + err = checkSKU(cache) + if err != nil { + fmt.Printf("failed to check sku: %s", err) + os.Exit(1) + } +} + +func checkSKU(cache *skewer.Cache) error { + sku, err := cache.Get(context.Background(), "standard_d4s_v3", skewer.VirtualMachines, "eastus") + if err != nil { + return fmt.Errorf("failed to find virtual machine sku standard_d4s_v3: %s", err) + } + + // Check for capabilities + if sku.IsEphemeralOSDiskSupported() { + fmt.Printf("SKU %s supports ephemeral OS disk!\n", sku.GetName()) + } + + cpu, err := sku.VCPU() + if err != nil { + return fmt.Errorf("failed to parse cpu from sku: %s", err) + } + + memory, err := sku.Memory() + if err != nil { + return fmt.Errorf("failed to parse memory from sku: %s", err) + } + + fmt.Printf("vm sku %s has %d vCPU cores and %.2fGi of memory\n", sku.GetName(), cpu, memory) + + return nil +} diff --git a/fakes_test.go b/fakes_test.go index c617a5d..80a83e2 100644 --- a/fakes_test.go +++ b/fakes_test.go @@ -76,11 +76,11 @@ func (f *fakeResourceSKUsClient) NewListPager(options *armcompute.ResourceSKUsCl } // newSuccessfulFakeResourceSKUsClient takes a list of sku lists and returns a ResourceSKUsClient. -func newSuccessfulFakeResourceSKUsClient(skuLists [][]*armcompute.ResourceSKU) (*fakeResourceSKUsClient, error) { +func newSuccessfulFakeResourceSKUsClient(skuLists [][]*armcompute.ResourceSKU) *fakeResourceSKUsClient { return &fakeResourceSKUsClient{ skus: skuLists, err: nil, - }, nil + } } // chunk divides a list into count pieces. diff --git a/sku_test.go b/sku_test.go index 07c146b..d28a00f 100644 --- a/sku_test.go +++ b/sku_test.go @@ -559,10 +559,8 @@ func Test_SKU_Includes(t *testing.T) { }, } for name, tc := range cases { - tc := tc t.Run(name, func(t *testing.T) { - sku := SKU(tc.sku) - if diff := cmp.Diff(tc.expect, sku.MemberOf(tc.skuList)); diff != "" { + if diff := cmp.Diff(tc.expect, tc.sku.MemberOf(tc.skuList)); diff != "" { t.Error(diff) } }) diff --git a/vmsize.go b/vmsize.go index bcd1bec..3d2d028 100644 --- a/vmsize.go +++ b/vmsize.go @@ -49,7 +49,7 @@ type VMSizeType struct { // parseVMSize parses the VM size and returns the parts as a map. func parseVMSize(vmSizeName string) ([]string, error) { parts := skuSizeScheme.FindStringSubmatch(vmSizeName) - if parts == nil || len(parts) < 10 { + if len(parts) < 10 { return nil, fmt.Errorf("could not parse VM size %s", vmSizeName) } return parts, nil diff --git a/wrap.go b/wrap.go index 98f4edf..c899580 100644 --- a/wrap.go +++ b/wrap.go @@ -8,7 +8,7 @@ func Wrap(in []*armcompute.ResourceSKU) []SKU { out := make([]SKU, len(in)) for index, value := range in { if value != nil { - out[index] = (SKU)(*value) + out[index] = SKU(*value) } } return out From 114d543d01091454ab29b13eede6cdbc4cbd0868 Mon Sep 17 00:00:00 2001 From: Shaoru Hu Date: Thu, 2 Oct 2025 18:03:16 +0000 Subject: [PATCH 3/4] rename ResourceSKUsClient to ResourceClient --- README.md | 4 ++-- cache.go | 8 ++++---- clients.go | 14 +++++++------- data_test.go | 12 ++++++------ example/example.go | 4 ++-- fakes_test.go | 45 +++++++++++++++++++++++++++++---------------- interface.go | 6 +++--- 7 files changed, 53 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index b8db8ea..ac908ba 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ import ( func main() { // az login - // export AZURE_SUBSCRIPTION_ID="subscription-id" + // export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv) sub := os.Getenv("AZURE_SUBSCRIPTION_ID") cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -38,7 +38,7 @@ func main() { os.Exit(1) } - cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceSKUsClient(client)) + cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceClient(client)) if err != nil { fmt.Printf("failed to instantiate sku cache: %s", err) os.Exit(1) diff --git a/cache.go b/cache.go index c056efe..d63092b 100644 --- a/cache.go +++ b/cache.go @@ -70,14 +70,14 @@ func WithClient(client client) Option { } } -// WithResourceSKUsClient is a functional option to use a cache -// backed by a ResourceSKUsClient. -func WithResourceSKUsClient(client ResourceSKUsClient) Option { +// WithResourceClient is a functional option to use a cache +// backed by a ResourceClient. +func WithResourceClient(client ResourceClient) Option { return func(c *Config) (*Config, error) { if c.client != nil { return nil, &ErrClientNotNil{} } - c.client = newWrappedResourceSKUsClient(client) + c.client = newWrappedResourceClient(client) return c, nil } } diff --git a/clients.go b/clients.go index 620a24f..35da160 100644 --- a/clients.go +++ b/clients.go @@ -7,17 +7,17 @@ import ( "github.com/pkg/errors" ) -// wrappedResourceSKUsClient defines a wrapper for the typical Azure track2 client -// signature to collect all resource skus from the pager returned by NewListPager(). -type wrappedResourceSKUsClient struct { - client ResourceSKUsClient +// wrappedResourceClient defines a wrapper for the typical Azure client +// signature to collect all resource skus from the iterator returned by NewListPager(). +type wrappedResourceClient struct { + client ResourceClient } -func newWrappedResourceSKUsClient(client ResourceSKUsClient) *wrappedResourceSKUsClient { - return &wrappedResourceSKUsClient{client} +func newWrappedResourceClient(client ResourceClient) *wrappedResourceClient { + return &wrappedResourceClient{client} } -func (w *wrappedResourceSKUsClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { +func (w *wrappedResourceClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { options := &armcompute.ResourceSKUsClientListOptions{} if filter != "" { options.Filter = &filter diff --git a/data_test.go b/data_test.go index 070b8c7..d53f749 100644 --- a/data_test.go +++ b/data_test.go @@ -30,23 +30,23 @@ func Test_Data(t *testing.T) { skus: dataWrapper.Value, } - resourceSKUsClient := newSuccessfulFakeResourceSKUsClient([][]*armcompute.ResourceSKU{dataWrapper.Value}) + resourceClient := newSuccessfulFakeResourceClient([][]*armcompute.ResourceSKU{dataWrapper.Value}) - chunkedResourceSKUsClient := newSuccessfulFakeResourceSKUsClient(chunk(dataWrapper.Value, 10)) + chunkedResourceClient := newSuccessfulFakeResourceClient(chunk(dataWrapper.Value, 10)) ctx := context.Background() cases := map[string]struct { newCacheFunc NewCacheFunc }{ - "resourceSKUsClient": { + "resourceClient": { newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { - return NewCache(ctx, WithResourceSKUsClient(resourceSKUsClient), WithLocation("eastus")) + return NewCache(ctx, WithResourceClient(resourceClient), WithLocation("eastus")) }, }, - "chunkedResourceSKUsClient": { + "chunkedResourceClient": { newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { - return NewCache(ctx, WithResourceSKUsClient(chunkedResourceSKUsClient), WithLocation("eastus")) + return NewCache(ctx, WithResourceClient(chunkedResourceClient), WithLocation("eastus")) }, }, "wrappedClient": { diff --git a/example/example.go b/example/example.go index 8f50d31..19a3f7a 100644 --- a/example/example.go +++ b/example/example.go @@ -13,7 +13,7 @@ import ( func main() { // az login - // export AZURE_SUBSCRIPTION_ID="subscription-id" + // export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv) sub := os.Getenv("AZURE_SUBSCRIPTION_ID") cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -27,7 +27,7 @@ func main() { os.Exit(1) } - cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceSKUsClient(client)) + cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceClient(client)) if err != nil { fmt.Printf("failed to instantiate sku cache: %s", err) os.Exit(1) diff --git a/fakes_test.go b/fakes_test.go index 80a83e2..f3eab3c 100644 --- a/fakes_test.go +++ b/fakes_test.go @@ -46,40 +46,53 @@ func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations return f.skus, nil } -// fakeResourceSKUsClient is a fake client for the real Azure types. -type fakeResourceSKUsClient struct { - skus [][]*armcompute.ResourceSKU - err error +// fakeResourceClient is a fake client for the real Azure types. It +// returns a result iterator and can test against arbitrary sequences of +// return pages, injecting failure. +type fakeResourceClient struct { + skuLists [][]*armcompute.ResourceSKU + err error } -var _ ResourceSKUsClient = &fakeResourceSKUsClient{} - -func (f *fakeResourceSKUsClient) NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] { +func (f *fakeResourceClient) NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] { pageCount := 0 pager := runtime.NewPager(runtime.PagingHandler[armcompute.ResourceSKUsClientListResponse]{ More: func(current armcompute.ResourceSKUsClientListResponse) bool { - return pageCount < len(f.skus) + return pageCount < len(f.skuLists) }, Fetcher: func(ctx context.Context, current *armcompute.ResourceSKUsClientListResponse) (armcompute.ResourceSKUsClientListResponse, error) { - if pageCount >= len(f.skus) { + if f.err != nil { return armcompute.ResourceSKUsClientListResponse{}, f.err } + if pageCount >= len(f.skuLists) { + return armcompute.ResourceSKUsClientListResponse{}, nil + } pageCount += 1 return armcompute.ResourceSKUsClientListResponse{ ResourceSKUsResult: armcompute.ResourceSKUsResult{ - Value: f.skus[pageCount-1], + Value: f.skuLists[pageCount-1], }, - }, f.err + }, nil }, }) return pager } -// newSuccessfulFakeResourceSKUsClient takes a list of sku lists and returns a ResourceSKUsClient. -func newSuccessfulFakeResourceSKUsClient(skuLists [][]*armcompute.ResourceSKU) *fakeResourceSKUsClient { - return &fakeResourceSKUsClient{ - skus: skuLists, - err: nil, +//nolint:deadcode,unused +func newFailingFakeResourceClient(reterr error) *fakeResourceClient { + return &fakeResourceClient{ + skuLists: [][]*armcompute.ResourceSKU{{}}, + err: reterr, + } +} + +// newSuccessfulFakeResourceClient takes a list of sku lists and returns +// a ResourceClient which iterates over all of them, mapping each sku +// list to a page of values. +func newSuccessfulFakeResourceClient(skuLists [][]*armcompute.ResourceSKU) *fakeResourceClient { + return &fakeResourceClient{ + skuLists: skuLists, + err: nil, } } diff --git a/interface.go b/interface.go index fd37584..e57aaa9 100644 --- a/interface.go +++ b/interface.go @@ -7,12 +7,12 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" ) -// ResourceSKUsClient is the required Azure track2 client interface used to populate skewer's data. -type ResourceSKUsClient interface { +// ResourceClient is the required Azure client interface used to populate skewer's data. +type ResourceClient interface { NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] } -var _ ResourceSKUsClient = &armcompute.ResourceSKUsClient{} +var _ ResourceClient = &armcompute.ResourceSKUsClient{} // client defines the internal interface required by the skewer Cache. type client interface { From f854972d941116221e999a9860533569fbfdca2f Mon Sep 17 00:00:00 2001 From: Shaoru Hu Date: Tue, 7 Oct 2025 21:47:27 +0000 Subject: [PATCH 4/4] move to folder v2/ --- README.md | 50 +- cache.go | 17 +- cache_test.go | 317 +-- clients.go | 55 +- data_test.go | 36 +- disk_test.go | 140 +- fakes_test.go | 157 +- go.mod | 38 +- go.sum | 125 +- hack/generate_vmsize_testdata.go | 37 +- interface.go | 14 +- sku.go | 182 +- sku_test.go | 254 +-- v2/README.md | 139 ++ v2/cache.go | 371 ++++ v2/cache_test.go | 570 +++++ v2/clients.go | 38 + v2/const.go | 83 + v2/data_test.go | 375 ++++ v2/disk.go | 25 + v2/disk_test.go | 272 +++ {example => v2/example}/example.go | 0 v2/fakes_test.go | 113 + v2/hack/generate_vmsize_testdata.go | 108 + v2/interface.go | 20 + v2/sku.go | 546 +++++ v2/sku_test.go | 568 +++++ v2/strings.go | 20 + v2/testdata/eastus.json | 534 +++++ v2/testdata/generated_vmsize_testdata.go | 2426 ++++++++++++++++++++++ v2/vmsize.go | 160 ++ v2/vmsize_test.go | 199 ++ v2/wrap.go | 15 + vmsize.go | 2 +- vmsize_test.go | 2 +- wrap.go | 8 +- 36 files changed, 7435 insertions(+), 581 deletions(-) create mode 100644 v2/README.md create mode 100644 v2/cache.go create mode 100644 v2/cache_test.go create mode 100644 v2/clients.go create mode 100644 v2/const.go create mode 100644 v2/data_test.go create mode 100644 v2/disk.go create mode 100644 v2/disk_test.go rename {example => v2/example}/example.go (100%) create mode 100644 v2/fakes_test.go create mode 100644 v2/hack/generate_vmsize_testdata.go create mode 100644 v2/interface.go create mode 100644 v2/sku.go create mode 100644 v2/sku_test.go create mode 100644 v2/strings.go create mode 100644 v2/testdata/eastus.json create mode 100644 v2/testdata/generated_vmsize_testdata.go create mode 100644 v2/vmsize.go create mode 100644 v2/vmsize_test.go create mode 100644 v2/wrap.go diff --git a/README.md b/README.md index ac908ba..c6c98e3 100644 --- a/README.md +++ b/README.md @@ -16,29 +16,31 @@ import ( "fmt" "os" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/go-autorest/autorest/azure/auth" - "github.com/Azure/skewer/v2" + "github.com/Azure/skewer" ) -func main() { - // az login - // export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv) - sub := os.Getenv("AZURE_SUBSCRIPTION_ID") - cred, err := azidentity.NewDefaultAzureCredential(nil) - if err != nil { - fmt.Printf("failed to get credential: %s", err) - os.Exit(1) - } - - client, err := armcompute.NewResourceSKUsClient(sub, cred, nil) - if err != nil { - fmt.Printf("failed to get client: %s", err) - os.Exit(1) - } +const ( + SubscriptionID = "AZURE_SUBSCRIPTION_ID" + TenantID = "AZURE_TENANT_ID" + ClientID = "AZURE_CLIENT_ID" + ClientSecret = "AZURE_CLIENT_SECRET" +) - cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceClient(client)) +func main() { + os.Setenv(SubscriptionID, "subscriptionID") + os.Setenv(TenantID, "TenantID") + os.Setenv(ClientID, "AAD Client ID or AppID") + os.Setenv(ClientSecret, "AADClientSecretHere") + sub := os.Getenv(SubscriptionID) + authorizer, err := auth.NewAuthorizerFromEnvironment() + // Create a skus client + client := compute.NewResourceSkusClient(sub) + client.Authorizer = authorizer + + cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("southcentralus"), skewer.WithResourceClient(client)) if err != nil { fmt.Printf("failed to instantiate sku cache: %s", err) os.Exit(1) @@ -52,14 +54,14 @@ func main() { Once we have a cache, we can query against its contents: ```go -sku, err := cache.Get(context.Background(), "standard_d4s_v3", skewer.VirtualMachines, "eastus") -if err != nil { - return fmt.Errorf("failed to find virtual machine sku standard_d4s_v3: %s", err) +sku, found := cache.Get(context.Background, "standard_d4s_v3", skewer.VirtualMachines, "eastus") +if !found { + return fmt.Errorf("expected to find virtual machine sku standard_d4s_v3") } // Check for capabilities if sku.IsEphemeralOSDiskSupported() { - fmt.Printf("SKU %s supports ephemeral OS disk!\n", sku.GetName()) + fmt.Println("SKU %s supports ephemeral OS disk!", sku.GetName()) } cpu, err := sku.VCPU() @@ -72,7 +74,7 @@ if err != nil { return fmt.Errorf("failed to parse memory from sku: %s", err) } -fmt.Printf("vm sku %s has %d vCPU cores and %.2fGi of memory\n", sku.GetName(), cpu, memory) +fmt.Printf("vm sku %s has %d vCPU cores and %.2fGi of memory", sku.GetName(), cpu, memory) ``` # Development diff --git a/cache.go b/cache.go index d63092b..90517d3 100644 --- a/cache.go +++ b/cache.go @@ -82,6 +82,19 @@ func WithResourceClient(client ResourceClient) Option { } } +// WithResourceProviderClient is a functional option to use a cache +// backed by a ResourceProviderClient. +func WithResourceProviderClient(client ResourceProviderClient) Option { + return func(c *Config) (*Config, error) { + if c.client != nil { + return nil, &ErrClientNotNil{} + } + resourceClient := newWrappedResourceProviderClient(client) + c.client = newWrappedResourceClient(resourceClient) + return c, nil + } +} + // NewCacheFunc describes the live cache instantiation signature. Used // for testing. type NewCacheFunc func(ctx context.Context, opts ...Option) (*Cache, error) @@ -270,9 +283,7 @@ func (c *Cache) Equal(other *Cache) bool { return false } for i := range c.data { - // we can't use c.data[i] != other.data[i] since there are many pointers - // use Equal to compare location, type and name - if !c.data[i].Equal(&other.data[i]) { + if c.data[i] != other.data[i] { return false } } diff --git a/cache_test.go b/cache_test.go index 05cebda..3d27d85 100644 --- a/cache_test.go +++ b/cache_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/go-autorest/autorest/to" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -38,6 +38,7 @@ func Test_WithLocation(t *testing.T) { } for name, tc := range cases { + tc := tc t.Run(name, func(t *testing.T) { cache, err := NewStaticCache(nil, tc.options...) if err != nil { @@ -61,70 +62,71 @@ func Test_Cache_GetVirtualMachines(t *testing.T) { func Test_Filter(t *testing.T) { cases := map[string]struct { - unfiltered []*armcompute.ResourceSKU + unfiltered []compute.ResourceSku condition FilterFn - expected []*armcompute.ResourceSKU + expected []compute.ResourceSku }{ "nil slice filters to nil slice": { condition: func(*SKU) bool { return true }, }, "empty slice filters to empty slice": { - unfiltered: []*armcompute.ResourceSKU{}, + unfiltered: []compute.ResourceSku{}, condition: func(*SKU) bool { return true }, - expected: []*armcompute.ResourceSKU{}, + expected: []compute.ResourceSku{}, }, "slice with non-matching element filters to empty slice": { - unfiltered: []*armcompute.ResourceSKU{ + unfiltered: []compute.ResourceSku{ { - ResourceType: to.Ptr("nomatch"), + ResourceType: to.StringPtr("nomatch"), }, }, condition: func(s *SKU) bool { return s.GetName() == "match" }, - expected: []*armcompute.ResourceSKU{}, + expected: []compute.ResourceSku{}, }, "slice with one matching element doesn't change": { - unfiltered: []*armcompute.ResourceSKU{ + unfiltered: []compute.ResourceSku{ { - ResourceType: to.Ptr("match"), + ResourceType: to.StringPtr("match"), }, }, condition: func(s *SKU) bool { return true }, - expected: []*armcompute.ResourceSKU{ + expected: []compute.ResourceSku{ { - ResourceType: to.Ptr("match"), + ResourceType: to.StringPtr("match"), }, }, }, "all matching elements removed": { - unfiltered: []*armcompute.ResourceSKU{ + unfiltered: []compute.ResourceSku{ { - ResourceType: to.Ptr("match"), + ResourceType: to.StringPtr("match"), }, { - ResourceType: to.Ptr("nomatch"), + ResourceType: to.StringPtr("nomatch"), }, { - ResourceType: to.Ptr("match"), + ResourceType: to.StringPtr("match"), }, { - ResourceType: to.Ptr("unmatch"), + ResourceType: to.StringPtr("unmatch"), }, { - ResourceType: to.Ptr("match"), + ResourceType: to.StringPtr("match"), }, }, condition: func(s *SKU) bool { return !s.IsResourceType("match") }, - expected: []*armcompute.ResourceSKU{ + expected: []compute.ResourceSku{ { - ResourceType: to.Ptr("nomatch"), + ResourceType: to.StringPtr("nomatch"), }, { - ResourceType: to.Ptr("unmatch"), + ResourceType: to.StringPtr("unmatch"), }, }, }, } for name, tc := range cases { + tc := tc t.Run(name, func(t *testing.T) { result := Filter(Wrap(tc.unfiltered), tc.condition) if diff := cmp.Diff(result, Wrap(tc.expected), []cmp.Option{ @@ -178,7 +180,7 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen cases := map[string]struct { sku string resourceType string - have []*armcompute.ResourceSKU + have []compute.ResourceSku found bool }{ "should return false with no data": { @@ -188,11 +190,11 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should match when found at index=0": { sku: "foo", resourceType: "bar", - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr("bar"), - Locations: []*string{to.Ptr("")}, + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr("bar"), + Locations: &[]string{""}, }, }, found: true, @@ -200,15 +202,15 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should match when found at index=1": { sku: "foo", resourceType: "bar", - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("other"), - ResourceType: to.Ptr("baz"), + Name: to.StringPtr("other"), + ResourceType: to.StringPtr("baz"), }, { - Name: to.Ptr("foo"), - ResourceType: to.Ptr("bar"), - Locations: []*string{to.Ptr("")}, + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr("bar"), + Locations: &[]string{""}, }, }, found: true, @@ -216,15 +218,15 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should match regardless of sku capitalization": { sku: "foo", resourceType: "bar", - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("other"), - ResourceType: to.Ptr("baz"), + Name: to.StringPtr("other"), + ResourceType: to.StringPtr("baz"), }, { - Name: to.Ptr("FoO"), - ResourceType: to.Ptr("bar"), - Locations: []*string{to.Ptr("")}, + Name: to.StringPtr("FoO"), + ResourceType: to.StringPtr("bar"), + Locations: &[]string{""}, }, }, found: true, @@ -232,15 +234,16 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen "should return false when no match exists": { sku: "foo", resourceType: "bar", - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("other"), + Name: to.StringPtr("other"), }, }, }, } for name, tc := range cases { + tc := tc t.Run(name, func(t *testing.T) { cache := &Cache{ data: Wrap(tc.have), @@ -278,21 +281,21 @@ func Test_Cache_Get(t *testing.T) { //nolint:funlen func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen cases := map[string]struct { - have []*armcompute.ResourceSKU + have []compute.ResourceSku want []string }{ "should find 1 result": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, }, @@ -300,30 +303,30 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: []string{"1"}, }, "should find 2 results": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, }, { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("2")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"2"}, }, }, }, @@ -331,17 +334,17 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: []string{"1", "2"}, }, "should not find due to location mismatch": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("foobar"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "foobar", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("foobar"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("foobar"), + Zones: &[]string{"1"}, }, }, }, @@ -349,23 +352,23 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: nil, }, "should not find due to location restriction": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, - Restrictions: []*armcompute.ResourceSKURestrictions{ + Restrictions: &[]compute.ResourceSkuRestrictions{ { - Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeLocation), - Values: []*string{to.Ptr("baz")}, + Type: compute.Location, + Values: &[]string{"baz"}, }, }, }, @@ -373,25 +376,25 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen want: nil, }, "should not find due to zone restriction": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, - Restrictions: []*armcompute.ResourceSKURestrictions{ + Restrictions: &[]compute.ResourceSkuRestrictions{ { - Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeZone), - Values: []*string{to.Ptr("baz")}, - RestrictionInfo: &armcompute.ResourceSKURestrictionInfo{ - Zones: []*string{to.Ptr("1")}, + Type: compute.Zone, + Values: &[]string{"baz"}, + RestrictionInfo: &compute.ResourceSkuRestrictionInfo{ + Zones: &[]string{"1"}, }, }, }, @@ -402,6 +405,7 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen } for name, tc := range cases { + tc := tc t.Run(name, func(t *testing.T) { cache, err := NewStaticCache(Wrap(tc.have), WithLocation("baz")) if err != nil { @@ -414,7 +418,7 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen return a < b }), }...); diff != "" { - t.Error(diff) + t.Errorf(diff) } }) } @@ -422,21 +426,21 @@ func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //nolint:funlen cases := map[string]struct { - have []*armcompute.ResourceSKU + have []compute.ResourceSku want []string }{ "should find 1 result": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, }, @@ -444,17 +448,17 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: []string{"1"}, }, "should find 2 results": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1"), to.Ptr("2")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1", "2"}, }, }, }, @@ -462,17 +466,17 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: []string{"1", "2"}, }, "should not find due to size mismatch": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foobar"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foobar"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, }, @@ -480,17 +484,17 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: nil, }, "should not find due to location mismatch": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("foobar"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "foobar", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("foobar"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("foobar"), + Zones: &[]string{"1"}, }, }, }, @@ -498,23 +502,23 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: nil, }, "should not find due to location restriction": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, - Restrictions: []*armcompute.ResourceSKURestrictions{ + Restrictions: &[]compute.ResourceSkuRestrictions{ { - Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeLocation), - Values: []*string{to.Ptr("baz")}, + Type: compute.Location, + Values: &[]string{"baz"}, }, }, }, @@ -522,25 +526,25 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli want: nil, }, "should not find due to zone restriction": { - have: []*armcompute.ResourceSKU{ + have: []compute.ResourceSku{ { - Name: to.Ptr("foo"), - ResourceType: to.Ptr(string(VirtualMachines)), - Locations: []*string{ - to.Ptr("baz"), + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr(string(VirtualMachines)), + Locations: &[]string{ + "baz", }, - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - Location: to.Ptr("baz"), - Zones: []*string{to.Ptr("1")}, + Location: to.StringPtr("baz"), + Zones: &[]string{"1"}, }, }, - Restrictions: []*armcompute.ResourceSKURestrictions{ + Restrictions: &[]compute.ResourceSkuRestrictions{ { - Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeZone), - Values: []*string{to.Ptr("baz")}, - RestrictionInfo: &armcompute.ResourceSKURestrictionInfo{ - Zones: []*string{to.Ptr("1")}, + Type: compute.Zone, + Values: &[]string{"baz"}, + RestrictionInfo: &compute.ResourceSkuRestrictionInfo{ + Zones: &[]string{"1"}, }, }, }, @@ -551,6 +555,7 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli } for name, tc := range cases { + tc := tc t.Run(name, func(t *testing.T) { cache, err := NewStaticCache(Wrap(tc.have), WithLocation("baz")) if err != nil { @@ -563,7 +568,7 @@ func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //noli return a < b }), }...); diff != "" { - t.Fatal(diff) + t.Fatalf(diff) } }) } diff --git a/clients.go b/clients.go index 35da160..ab90c85 100644 --- a/clients.go +++ b/clients.go @@ -3,12 +3,12 @@ package skewer import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck "github.com/pkg/errors" ) // wrappedResourceClient defines a wrapper for the typical Azure client -// signature to collect all resource skus from the iterator returned by NewListPager(). +// signature to collect all resource skus from the iterator returned by ListComplete(). type wrappedResourceClient struct { client ResourceClient } @@ -17,22 +17,47 @@ func newWrappedResourceClient(client ResourceClient) *wrappedResourceClient { return &wrappedResourceClient{client} } -func (w *wrappedResourceClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { - options := &armcompute.ResourceSKUsClientListOptions{} - if filter != "" { - options.Filter = &filter +// List greedily traverses all returned sku pages +func (w *wrappedResourceClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]compute.ResourceSku, error) { + return iterate(ctx, filter, includeExtendedLocations, w.client.ListComplete) +} + +// wrappedResourceProviderClient defines a wrapper for the typical Azure client +// signature to collect all resource skus from the iterator returned by +// List(). It only differs from wrappedResourceClient in signature. +type wrappedResourceProviderClient struct { + client ResourceProviderClient +} + +func newWrappedResourceProviderClient(client ResourceProviderClient) *wrappedResourceProviderClient { + return &wrappedResourceProviderClient{client} +} + +//nolint:lll +func (w *wrappedResourceProviderClient) ListComplete(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultIterator, error) { + page, err := w.client.List(ctx, filter, includeExtendedLocations) + if err != nil { + return compute.ResourceSkusResultIterator{}, nil } - if includeExtendedLocations != "" { - options.IncludeExtendedLocations = &includeExtendedLocations + return compute.NewResourceSkusResultIterator(page), nil +} + +type iterFunc func(context.Context, string, string) (compute.ResourceSkusResultIterator, error) + +// iterate invokes fn to get an iterator, then drains it into an array. +func iterate(ctx context.Context, filter, includeExtendedLocations string, fn iterFunc) ([]compute.ResourceSku, error) { + iter, err := fn(ctx, filter, includeExtendedLocations) + if err != nil { + return nil, errors.Wrap(err, "could not list resource skus") } - pager := w.client.NewListPager(options) - var skus []*armcompute.ResourceSKU - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, errors.Wrap(err, "could not list resource skus") + + var skus []compute.ResourceSku + for iter.NotDone() { + skus = append(skus, iter.Value()) + if err := iter.NextWithContext(ctx); err != nil { + return nil, errors.Wrap(err, "could not iterate resource skus") } - skus = append(skus, page.Value...) } + return skus, nil } diff --git a/data_test.go b/data_test.go index d53f749..329029c 100644 --- a/data_test.go +++ b/data_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -30,9 +30,29 @@ func Test_Data(t *testing.T) { skus: dataWrapper.Value, } - resourceClient := newSuccessfulFakeResourceClient([][]*armcompute.ResourceSKU{dataWrapper.Value}) + resourceClient, err := newSuccessfulFakeResourceClient([][]compute.ResourceSku{ + dataWrapper.Value, + }) + if err != nil { + t.Error(err) + } + + chunkedResourceClient, err := newSuccessfulFakeResourceClient(chunk(dataWrapper.Value, 10)) + if err != nil { + t.Error(err) + } + + resourceProviderClient, err := newSuccessfulFakeResourceProviderClient([][]compute.ResourceSku{ + dataWrapper.Value, + }) + if err != nil { + t.Error(err) + } - chunkedResourceClient := newSuccessfulFakeResourceClient(chunk(dataWrapper.Value, 10)) + chunkedResourceProviderClient, err := newSuccessfulFakeResourceProviderClient(chunk(dataWrapper.Value, 10)) + if err != nil { + t.Error(err) + } ctx := context.Background() @@ -49,6 +69,16 @@ func Test_Data(t *testing.T) { return NewCache(ctx, WithResourceClient(chunkedResourceClient), WithLocation("eastus")) }, }, + "resourceProviderClient": { + newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { + return NewCache(ctx, WithResourceProviderClient(resourceProviderClient), WithLocation("eastus")) + }, + }, + "chunkedResourceProviderClient": { + newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { + return NewCache(ctx, WithResourceProviderClient(chunkedResourceProviderClient), WithLocation("eastus")) + }, + }, "wrappedClient": { newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { return NewCache(ctx, WithClient(fakeClient), WithLocation("eastus")) diff --git a/disk_test.go b/disk_test.go index d353c8f..ef45aa9 100644 --- a/disk_test.go +++ b/disk_test.go @@ -3,53 +3,53 @@ package skewer import ( "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/go-autorest/autorest/to" ) func Test_SKU_HasSCSISupport(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku expect bool }{ "empty capability list should return true (backward compatibility)": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, expect: true, }, "no disk controller capability should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{}, + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{}, }, expect: true, }, "SCSI only should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("SCSI"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("SCSI"), }, }, }, expect: true, }, "SCSI and NVMe should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("SCSI,NVMe"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("SCSI,NVMe"), }, }, }, expect: true, }, "NVMe only should return false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("NVMe"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("NVMe"), }, }, }, @@ -70,58 +70,58 @@ func Test_SKU_HasSCSISupport(t *testing.T) { func Test_SKU_HasNVMeSupport(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku expect bool }{ "empty capability list should return false": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, expect: false, }, "no disk controller capability should return false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{}, + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{}, }, expect: false, }, "SCSI only should return false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("SCSI"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("SCSI"), }, }, }, expect: false, }, "SCSI and NVMe should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("SCSI,NVMe"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("SCSI,NVMe"), }, }, }, expect: true, }, "NVMe only should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("NVMe"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("NVMe"), }, }, }, expect: true, }, "NVMe in mixed case should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(DiskControllerTypes), - Value: to.Ptr("SCSI,NVMe,Other"), + Name: to.StringPtr(DiskControllerTypes), + Value: to.StringPtr("SCSI,NVMe,Other"), }, }, }, @@ -142,52 +142,52 @@ func Test_SKU_HasNVMeSupport(t *testing.T) { func Test_SKU_SupportsNVMeEphemeralOSDisk(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku expect bool }{ "empty capability list should return false": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, expect: false, }, "no ephemeral placement capability should return false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("vCPUs"), - Value: to.Ptr("8"), + Name: to.StringPtr("vCPUs"), + Value: to.StringPtr("8"), }, }, }, expect: false, }, "ResourceDisk only should return false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(SupportedEphemeralOSDiskPlacements), - Value: to.Ptr("ResourceDisk"), + Name: to.StringPtr(SupportedEphemeralOSDiskPlacements), + Value: to.StringPtr("ResourceDisk"), }, }, }, expect: false, }, "NvmeDisk should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(SupportedEphemeralOSDiskPlacements), - Value: to.Ptr("NvmeDisk"), + Name: to.StringPtr(SupportedEphemeralOSDiskPlacements), + Value: to.StringPtr("NvmeDisk"), }, }, }, expect: true, }, "ResourceDisk and NvmeDisk should return true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(SupportedEphemeralOSDiskPlacements), - Value: to.Ptr("ResourceDisk,NvmeDisk"), + Name: to.StringPtr(SupportedEphemeralOSDiskPlacements), + Value: to.StringPtr("ResourceDisk,NvmeDisk"), }, }, }, @@ -208,42 +208,42 @@ func Test_SKU_SupportsNVMeEphemeralOSDisk(t *testing.T) { func Test_SKU_NVMeDiskSizeInMiB(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku expect int64 err string }{ "empty capability list should return error": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, err: (&ErrCapabilityNotFound{NvmeDiskSizeInMiB}).Error(), }, "no NVMe disk size capability should return error": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("vCPUs"), - Value: to.Ptr("8"), + Name: to.StringPtr("vCPUs"), + Value: to.StringPtr("8"), }, }, }, err: (&ErrCapabilityNotFound{NvmeDiskSizeInMiB}).Error(), }, "valid NVMe disk size should return value": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(NvmeDiskSizeInMiB), - Value: to.Ptr("1024000"), + Name: to.StringPtr(NvmeDiskSizeInMiB), + Value: to.StringPtr("1024000"), }, }, }, expect: 1024000, }, "invalid NVMe disk size should return parse error": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(NvmeDiskSizeInMiB), - Value: to.Ptr("not-a-number"), + Name: to.StringPtr(NvmeDiskSizeInMiB), + Value: to.StringPtr("not-a-number"), }, }, }, diff --git a/fakes_test.go b/fakes_test.go index f3eab3c..df14007 100644 --- a/fakes_test.go +++ b/fakes_test.go @@ -5,13 +5,12 @@ import ( "encoding/json" "os" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck ) // dataWrapper is a convenience wrapper for deserializing json testdata type dataWrapper struct { - Value []*armcompute.ResourceSKU `json:"value,omitempty"` + Value []compute.ResourceSku `json:"value,omitempty"` } // newDataWrapper takes a path to a list of compute skus and parses them @@ -33,13 +32,11 @@ func newDataWrapper(path string) (*dataWrapper, error) { // fakeClient is close to the simplest fake client implementation usable // by the cache. It does not use pagination like Azure clients. type fakeClient struct { - skus []*armcompute.ResourceSKU + skus []compute.ResourceSku err error } -var _ client = &fakeClient{} - -func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { +func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]compute.ResourceSku, error) { if f.err != nil { return nil, f.err } @@ -50,55 +47,109 @@ func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations // returns a result iterator and can test against arbitrary sequences of // return pages, injecting failure. type fakeResourceClient struct { - skuLists [][]*armcompute.ResourceSKU - err error -} - -func (f *fakeResourceClient) NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] { - pageCount := 0 - pager := runtime.NewPager(runtime.PagingHandler[armcompute.ResourceSKUsClientListResponse]{ - More: func(current armcompute.ResourceSKUsClientListResponse) bool { - return pageCount < len(f.skuLists) - }, - Fetcher: func(ctx context.Context, current *armcompute.ResourceSKUsClientListResponse) (armcompute.ResourceSKUsClientListResponse, error) { - if f.err != nil { - return armcompute.ResourceSKUsClientListResponse{}, f.err - } - if pageCount >= len(f.skuLists) { - return armcompute.ResourceSKUsClientListResponse{}, nil - } - pageCount += 1 - return armcompute.ResourceSKUsClientListResponse{ - ResourceSKUsResult: armcompute.ResourceSKUsResult{ - Value: f.skuLists[pageCount-1], - }, - }, nil - }, - }) - return pager + res compute.ResourceSkusResultIterator + err error +} + +//nolint:lll +func (f *fakeResourceClient) ListComplete(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultIterator, error) { + if f.err != nil { + return compute.ResourceSkusResultIterator{}, f.err + } + return f.res, nil } //nolint:deadcode,unused func newFailingFakeResourceClient(reterr error) *fakeResourceClient { return &fakeResourceClient{ - skuLists: [][]*armcompute.ResourceSKU{{}}, - err: reterr, + res: compute.ResourceSkusResultIterator{}, + err: reterr, } } // newSuccessfulFakeResourceClient takes a list of sku lists and returns // a ResourceClient which iterates over all of them, mapping each sku // list to a page of values. -func newSuccessfulFakeResourceClient(skuLists [][]*armcompute.ResourceSKU) *fakeResourceClient { +func newSuccessfulFakeResourceClient(skuLists [][]compute.ResourceSku) (*fakeResourceClient, error) { + iterator, err := newFakeResourceSkusResultIterator(skuLists) + if err != nil { + return nil, err + } + return &fakeResourceClient{ - skuLists: skuLists, - err: nil, + res: iterator, + err: nil, + }, nil +} + +// fakeResourceProviderClient is a fake client for the real Azure types. It +// returns a result iterator and can test against arbitrary sequences of +// return pages, injecting failure. This uses the resource provider +// signature for testing purposes. +type fakeResourceProviderClient struct { + res compute.ResourceSkusResultPage + err error +} + +//nolint:lll +func (f *fakeResourceProviderClient) List(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultPage, error) { + if f.err != nil { + return compute.ResourceSkusResultPage{}, f.err } + return f.res, nil +} + +//nolint:deadcode,unused +func newFailingFakeResourceProviderClient(reterr error) *fakeResourceProviderClient { + return &fakeResourceProviderClient{ + res: compute.ResourceSkusResultPage{}, + err: reterr, + } +} + +// newSuccessfulFakeResourceProviderClient takes a list of sku lists and returns +// a ResourceProviderClient which iterates over all of them, mapping each sku +// list to a page of values. +func newSuccessfulFakeResourceProviderClient(skuLists [][]compute.ResourceSku) (*fakeResourceProviderClient, error) { + page, err := newFakeResourceSkusResultPage(skuLists) + if err != nil { + return nil, err + } + + return &fakeResourceProviderClient{ + res: page, + err: nil, + }, nil +} + +// newFakeResourceSkusResultPage takes a list of sku lists and +// returns an iterator over all items, mapping each sku +// list to a page of values. +func newFakeResourceSkusResultPage(skuLists [][]compute.ResourceSku) (compute.ResourceSkusResultPage, error) { + pages := newPageList(skuLists) + newPage := compute.NewResourceSkusResultPage(compute.ResourceSkusResult{}, pages.next) + + if err := newPage.NextWithContext(context.Background()); err != nil { + return compute.ResourceSkusResultPage{}, err + } + return newPage, nil +} + +// newFakeResourceSkusResultIterator takes a list of sku lists and +// returns an iterator over all items, mapping each sku +// list to a page of values. +func newFakeResourceSkusResultIterator(skuLists [][]compute.ResourceSku) (compute.ResourceSkusResultIterator, error) { + pages := newPageList(skuLists) + newPage := compute.NewResourceSkusResultPage(compute.ResourceSkusResult{}, pages.next) + if err := newPage.NextWithContext(context.Background()); err != nil { + return compute.ResourceSkusResultIterator{}, err + } + return compute.NewResourceSkusResultIterator(newPage), nil } // chunk divides a list into count pieces. -func chunk(skus []*armcompute.ResourceSKU, count int) [][]*armcompute.ResourceSKU { - divided := [][]*armcompute.ResourceSKU{} +func chunk(skus []compute.ResourceSku, count int) [][]compute.ResourceSku { + divided := [][]compute.ResourceSku{} size := (len(skus) + count - 1) / count for i := 0; i < len(skus); i += size { end := i + size @@ -111,3 +162,29 @@ func chunk(skus []*armcompute.ResourceSKU, count int) [][]*armcompute.ResourceSK } return divided } + +// pageList is a utility type to help construct ResourceSkusResultIterators. +type pageList struct { + cursor int + pages []compute.ResourceSkusResult +} + +func newPageList(skuLists [][]compute.ResourceSku) *pageList { + list := &pageList{} + for i := 0; i < len(skuLists); i++ { + list.pages = append(list.pages, compute.ResourceSkusResult{ + Value: &skuLists[i], + }) + } + return list +} + +// next underpins ResourceSkusResultIterator's NextWithDone() method. +func (p *pageList) next(context.Context, compute.ResourceSkusResult) (compute.ResourceSkusResult, error) { + if p.cursor >= len(p.pages) { + return compute.ResourceSkusResult{}, nil + } + old := p.cursor + p.cursor++ + return p.pages[old], nil +} diff --git a/go.mod b/go.mod index e0430ff..1e8e927 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,45 @@ -module github.com/Azure/skewer/v2 +module github.com/Azure/skewer go 1.23.0 -toolchain go1.24.4 - require ( + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible + github.com/Azure/go-autorest/autorest v0.11.29 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/google/go-cmp v0.5.9 github.com/pkg/errors v0.9.1 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.0.0 - github.com/stretchr/testify v1.10.0 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 + github.com/stretchr/testify v1.11.1 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9f62c81..a4fe8b7 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0 h1:z7Mqz6l0EFH549GvHEqfjKvi+cRScxLWbaoeLm9wxVQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.4.0/go.mod h1:v6gbfH+7DG7xH2kUNs+ZJ9tF6O3iNnR85wMtmr+F54o= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.0.0 h1:ConMW11qUpZOqv3OXCPJhl1icxETBs+Ey93Nw8lK3fM= @@ -14,18 +16,48 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsI github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= +github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -38,29 +70,70 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= -github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/generate_vmsize_testdata.go b/hack/generate_vmsize_testdata.go index a4eecd0..ba76b34 100644 --- a/hack/generate_vmsize_testdata.go +++ b/hack/generate_vmsize_testdata.go @@ -6,40 +6,35 @@ import ( "os" "text/template" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/skewer/v2/testdata" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/Azure/skewer/testdata" ) func getSKUs(subscriptionID, region string) (map[string]testdata.SKUInfo, error) { - cred, err := azidentity.NewDefaultAzureCredential(nil) + authorizer, err := auth.NewAuthorizerFromCLI() if err != nil { return nil, err } - client, err := armcompute.NewResourceSKUsClient(subscriptionID, cred, nil) + // Create a new compute client + client := compute.NewResourceSkusClient(subscriptionID) + client.Authorizer = authorizer + + // List SKUs for the specified region + skuList, err := client.List(context.Background(), region, "") if err != nil { return nil, err } - ctx := context.Background() - filter := fmt.Sprintf("location eq '%s'", region) - pager := client.NewListPager(&armcompute.ResourceSKUsClientListOptions{Filter: &filter}) - skus := map[string]testdata.SKUInfo{} - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, err - } - for _, v := range page.Value { - if v.ResourceType != nil && *v.ResourceType == "virtualMachines" { - if _, ok := skus[*v.Name]; !ok { - skuInfo := testdata.SKUInfo{ - Size: *v.Size, - } - skus[*v.Name] = skuInfo + for _, sku := range skuList.Values() { + if *sku.ResourceType == "virtualMachines" { + if _, ok := skus[*sku.Name]; !ok { + skuInfo := testdata.SKUInfo{ + Size: *sku.Size, } + skus[*sku.Name] = skuInfo } } } diff --git a/interface.go b/interface.go index e57aaa9..9cb506b 100644 --- a/interface.go +++ b/interface.go @@ -3,18 +3,22 @@ package skewer import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck ) // ResourceClient is the required Azure client interface used to populate skewer's data. type ResourceClient interface { - NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] + ListComplete(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultIterator, error) } -var _ ResourceClient = &armcompute.ResourceSKUsClient{} +// ResourceProviderClient is a convenience interface for uses cases +// specific to Azure resource providers. +type ResourceProviderClient interface { + List(ctx context.Context, filter, includeExtendedLocations string) (compute.ResourceSkusResultPage, error) +} // client defines the internal interface required by the skewer Cache. +// TODO(ace): implement a lazy iterator with caching (and a cursor?) type client interface { - List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) + List(ctx context.Context, filter, includeExtendedLocations string) ([]compute.ResourceSku, error) } diff --git a/sku.go b/sku.go index 816450e..7a6dbad 100644 --- a/sku.go +++ b/sku.go @@ -5,12 +5,12 @@ import ( "strconv" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck "github.com/pkg/errors" ) // SKU wraps an Azure compute SKU with richer functionality -type SKU armcompute.ResourceSKU +type SKU compute.ResourceSku // ErrCapabilityNotFound will be returned when a capability could not be // found, even without a value. @@ -144,8 +144,11 @@ func (s *SKU) GetCPUArchitectureType() (string, error) { // capability is not found, the value was nil, or the value could not be // parsed as an integer. func (s *SKU) GetCapabilityIntegerQuantity(name string) (int64, error) { - for _, capability := range s.Capabilities { - if capability != nil && capability.Name != nil && *capability.Name == name { + if s.Capabilities == nil { + return -1, &ErrCapabilityNotFound{name} + } + for _, capability := range *s.Capabilities { + if capability.Name != nil && *capability.Name == name { if capability.Value != nil { intVal, err := strconv.ParseInt(*capability.Value, ten, sixtyFour) if err != nil { @@ -164,8 +167,11 @@ func (s *SKU) GetCapabilityIntegerQuantity(name string) (int64, error) { // if the capability is not found, the value was nil, or the value could // not be parsed as an integer. func (s *SKU) GetCapabilityFloatQuantity(name string) (float64, error) { - for _, capability := range s.Capabilities { - if capability != nil && capability.Name != nil && *capability.Name == name { + if s.Capabilities == nil { + return -1, &ErrCapabilityNotFound{name} + } + for _, capability := range *s.Capabilities { + if capability.Name != nil && *capability.Name == name { if capability.Value != nil { intVal, err := strconv.ParseFloat(*capability.Value, sixtyFour) if err != nil { @@ -182,8 +188,11 @@ func (s *SKU) GetCapabilityFloatQuantity(name string) (float64, error) { // GetCapabilityString retrieves string capability with the provided name. // It errors if the capability is not found or the value was nil func (s *SKU) GetCapabilityString(name string) (string, error) { - for _, capability := range s.Capabilities { - if capability != nil && capability.Name != nil && *capability.Name == name { + if s.Capabilities == nil { + return "", &ErrCapabilityNotFound{name} + } + for _, capability := range *s.Capabilities { + if capability.Name != nil && *capability.Name == name { if capability.Value != nil { return *capability.Value, nil } @@ -198,8 +207,11 @@ func (s *SKU) GetCapabilityString(name string) (string, error) { // "EncryptionAtHostSupported", "AcceleratedNetworkingEnabled", and // "RdmaEnabled" func (s *SKU) HasCapability(name string) bool { - for _, capability := range s.Capabilities { - if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + if s.Capabilities == nil { + return false + } + for _, capability := range *s.Capabilities { + if capability.Name != nil && strings.EqualFold(*capability.Name, name) { return capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) } } @@ -215,16 +227,19 @@ func (s *SKU) HasCapability(name string) bool { // available. // For per zone capability check, use "HasCapabilityInZone" func (s *SKU) HasZonalCapability(name string) bool { - for _, locationInfo := range s.LocationInfo { - if locationInfo == nil { + if s.LocationInfo == nil { + return false + } + for _, locationInfo := range *s.LocationInfo { + if locationInfo.ZoneDetails == nil { continue } - for _, zoneDetails := range locationInfo.ZoneDetails { - if zoneDetails == nil { + for _, zoneDetails := range *locationInfo.ZoneDetails { + if zoneDetails.Capabilities == nil { continue } - for _, capability := range zoneDetails.Capabilities { - if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range *zoneDetails.Capabilities { + if capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) { return true } @@ -238,27 +253,32 @@ func (s *SKU) HasZonalCapability(name string) bool { // HasCapabilityInZone return true if the specified capability name is supported in the // specified zone. func (s *SKU) HasCapabilityInZone(name, zone string) bool { - for _, locationInfo := range s.LocationInfo { - if locationInfo == nil { + if s.LocationInfo == nil { + return false + } + for _, locationInfo := range *s.LocationInfo { + if locationInfo.ZoneDetails == nil { continue } - for _, zoneDetails := range locationInfo.ZoneDetails { - if zoneDetails == nil { + for _, zoneDetails := range *locationInfo.ZoneDetails { + if zoneDetails.Capabilities == nil { continue } foundZone := false - for _, zoneName := range zoneDetails.Name { - if zoneName != nil && strings.EqualFold(zone, *zoneName) { - foundZone = true - break + if zoneDetails.Name != nil { + for _, zoneName := range *zoneDetails.Name { + if strings.EqualFold(zone, zoneName) { + foundZone = true + break + } } } if !foundZone { continue } - for _, capability := range zoneDetails.Capabilities { - if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + for _, capability := range *zoneDetails.Capabilities { + if capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) { return true } @@ -274,8 +294,11 @@ func (s *SKU) HasCapabilityInZone(name, zone string) bool { // the desired substring. An example is "HyperVGenerations" which may be // "V1,V2" func (s *SKU) HasCapabilityWithSeparator(name, value string) bool { - for _, capability := range s.Capabilities { - if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + if s.Capabilities == nil { + return false + } + for _, capability := range *s.Capabilities { + if capability.Name != nil && strings.EqualFold(*capability.Name, name) { return capability.Value != nil && strings.Contains(normalizeLocation(*capability.Value), normalizeLocation(value)) } } @@ -291,8 +314,11 @@ func (s *SKU) HasCapabilityWithSeparator(name, value string) bool { // "CombinedTempDiskAndCachedWriteBytesPerSecond", "UncachedDiskIOPS", // and "UncachedDiskBytesPerSecond" func (s *SKU) HasCapabilityWithMinCapacity(name string, value int64) (bool, error) { - for _, capability := range s.Capabilities { - if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + if s.Capabilities == nil { + return false, nil + } + for _, capability := range *s.Capabilities { + if capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil { intVal, err := strconv.ParseInt(*capability.Value, ten, sixtyFour) if err != nil { @@ -311,13 +337,18 @@ func (s *SKU) HasCapabilityWithMinCapacity(name string, value int64) (bool, erro // IsAvailable returns true when the requested location matches one on // the sku, and there are no total restrictions on the location. func (s *SKU) IsAvailable(location string) bool { - for _, locationInfo := range s.LocationInfo { - if locationInfo != nil && locationInfo.Location != nil { + if s.LocationInfo == nil { + return false + } + for _, locationInfo := range *s.LocationInfo { + if locationInfo.Location != nil { if locationEquals(*locationInfo.Location, location) { - for _, restriction := range s.Restrictions { - // Can't deploy to any zones in this location. We're done. - if restriction != nil && restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { - return false + if s.Restrictions != nil { + for _, restriction := range *s.Restrictions { + // Can't deploy to any zones in this location. We're done. + if restriction.Type == compute.Location { + return false + } } } return true @@ -330,13 +361,16 @@ func (s *SKU) IsAvailable(location string) bool { // IsRestricted returns true when a location restriction exists for // this SKU. func (s *SKU) IsRestricted(location string) bool { - for _, restriction := range s.Restrictions { - if restriction == nil || restriction.Values == nil { + if s.Restrictions == nil { + return false + } + for _, restriction := range *s.Restrictions { + if restriction.Values == nil { continue } - for _, candidate := range restriction.Values { + for _, candidate := range *restriction.Values { // Can't deploy in this location. We're done. - if candidate != nil && locationEquals(*candidate, location) && restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { + if locationEquals(candidate, location) && restriction.Type == compute.Location { return true } } @@ -405,25 +439,25 @@ func (s *SKU) GetLocation() (string, error) { return "", fmt.Errorf("sku had nil location array") } - if len(s.Locations) < 1 { + if len(*s.Locations) < 1 { return "", fmt.Errorf("sku had no locations") } - if len(s.Locations) > 1 { + if len(*s.Locations) > 1 { return "", fmt.Errorf("sku had multiple locations, refusing to disambiguate") } - if s.Locations[0] == nil { - return "", fmt.Errorf("sku had nil location") - } - - return *s.Locations[0], nil + return (*s.Locations)[0], nil } // HasLocation returns true if the given sku exposes this region for deployment. func (s *SKU) HasLocation(location string) bool { - for _, candidate := range s.Locations { - if candidate != nil && locationEquals(*candidate, location) { + if s.Locations == nil { + return false + } + + for _, candidate := range *s.Locations { + if locationEquals(candidate, location) { return true } } @@ -434,15 +468,19 @@ func (s *SKU) HasLocation(location string) bool { // HasLocationRestriction returns true if the location is restricted for // this sku. func (s *SKU) HasLocationRestriction(location string) bool { - for _, restriction := range s.Restrictions { - if restriction.Type != nil && *restriction.Type != armcompute.ResourceSKURestrictionsTypeLocation { + if s.Restrictions == nil { + return false + } + + for _, restriction := range *s.Restrictions { + if restriction.Type != compute.Location { continue } if restriction.Values == nil { continue } - for _, candidate := range restriction.Values { - if candidate != nil && locationEquals(*candidate, location) { + for _, candidate := range *restriction.Values { + if locationEquals(candidate, location) { return true } } @@ -480,33 +518,33 @@ func (s *SKU) AvailabilityZones(location string) map[string]bool { //nolint:gocy availableZones := make(map[string]bool) restrictedZones := make(map[string]bool) - for _, locationInfo := range s.LocationInfo { - if locationInfo == nil || locationInfo.Location == nil { + for _, locationInfo := range *s.LocationInfo { + if locationInfo.Location == nil { continue } if locationEquals(*locationInfo.Location, location) { // add all zones - for _, zone := range locationInfo.Zones { - if zone != nil { - availableZones[*zone] = true + if locationInfo.Zones != nil { + for _, zone := range *locationInfo.Zones { + availableZones[zone] = true } } // iterate restrictions, remove any restricted zones for this location - for _, restriction := range s.Restrictions { - if restriction != nil { - for _, candidate := range restriction.Values { - if candidate != nil && locationEquals(*candidate, location) { - if restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { - // Can't deploy in this location. We're done. - return nil - } + if s.Restrictions != nil { + for _, restriction := range *s.Restrictions { + if restriction.Values != nil { + for _, candidate := range *restriction.Values { + if locationEquals(candidate, location) { + if restriction.Type == compute.Location { + // Can't deploy in this location. We're done. + return nil + } - if restriction.RestrictionInfo != nil { - // remove restricted zones - for _, zone := range restriction.RestrictionInfo.Zones { - if zone != nil { - restrictedZones[*zone] = true + if restriction.RestrictionInfo != nil && restriction.RestrictionInfo.Zones != nil { + // remove restricted zones + for _, zone := range *restriction.RestrictionInfo.Zones { + restrictedZones[zone] = true } } } @@ -527,7 +565,7 @@ func (s *SKU) AvailabilityZones(location string) map[string]bool { //nolint:gocy // Equal returns true when two skus have the same location, type, and name. func (s *SKU) Equal(other *SKU) bool { location, localErr := s.GetLocation() - otherLocation, otherErr := other.GetLocation() + otherLocation, otherErr := s.GetLocation() return strings.EqualFold(s.GetResourceType(), other.GetResourceType()) && strings.EqualFold(s.GetName(), other.GetName()) && locationEquals(location, otherLocation) && diff --git a/sku_test.go b/sku_test.go index d28a00f..ec79d0a 100644 --- a/sku_test.go +++ b/sku_test.go @@ -4,36 +4,36 @@ import ( "fmt" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck + "github.com/Azure/go-autorest/autorest/to" "github.com/google/go-cmp/cmp" ) func Test_SKU_GetCapabilityQuantity(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku capability string expect int64 err string }{ "empty capability list should return capability not found": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, capability: "", err: (&ErrCapabilityNotFound{""}).Error(), }, "empty capability should not match sku with empty list of capabilities": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{}, + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{}, }, capability: "", err: (&ErrCapabilityNotFound{""}).Error(), }, "empty capability should fail to parse when not integer": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(""), - Value: to.Ptr("False"), + Name: to.StringPtr(""), + Value: to.StringPtr("False"), }, }, }, @@ -41,11 +41,11 @@ func Test_SKU_GetCapabilityQuantity(t *testing.T) { err: "CapabilityValueParse: failed to parse string 'False' as int64, error: 'strconv.ParseInt: parsing \"False\": invalid syntax'", //nolint:lll }, "foo capability should return successfully with integer": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("100"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("100"), }, }, }, @@ -80,59 +80,59 @@ func Test_SKU_GetCapabilityQuantity(t *testing.T) { func Test_SKU_HasCapability(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku capability string expect bool }{ "empty capability should not match empty sku": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, capability: "", }, "empty capability should not match sku with empty list of capabilities": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{}, + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{}, }, capability: "", }, "empty capability should not match when present and false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(""), - Value: to.Ptr("False"), + Name: to.StringPtr(""), + Value: to.StringPtr("False"), }, }, }, capability: "", }, "empty capability should not match when present and weird value": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(""), - Value: to.Ptr("foobar"), + Name: to.StringPtr(""), + Value: to.StringPtr("foobar"), }, }, }, capability: "", }, "foo capability should not match when false": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("False"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("False"), }, }, }, capability: "foo", }, "foo capability should match when true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("True"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("True"), }, }, }, @@ -154,28 +154,28 @@ func Test_SKU_HasCapability(t *testing.T) { func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku capability string capacity int64 expect bool err error }{ "empty capability should not match empty sku": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, capability: "", }, "empty capability should not match sku with empty list of capabilities": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{}, + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{}, }, capability: "", }, "empty capability should error when present and weird value": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(""), - Value: to.Ptr("foobar"), + Name: to.StringPtr(""), + Value: to.StringPtr("foobar"), }, }, }, @@ -183,11 +183,11 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { err: fmt.Errorf("failed to parse string 'foobar' as int64: strconv.ParseInt: parsing \"foobar\": invalid syntax"), }, "empty capability should match when present with zero capacity and requesting zero": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr(""), - Value: to.Ptr("0"), + Name: to.StringPtr(""), + Value: to.StringPtr("0"), }, }, }, @@ -195,11 +195,11 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { expect: true, }, "foo capability should not match when present and less than capacity": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("100"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("100"), }, }, }, @@ -207,11 +207,11 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { capacity: 200, }, "foo capability should match when true": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("10"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("10"), }, }, }, @@ -241,27 +241,27 @@ func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { func Test_SKU_GetResourceTypeAndName(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku expectName string expectResourceType string }{ "nil resourceType should return empty string": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, expectResourceType: "", expectName: "", }, "empty resourceType should return empty string": { - sku: armcompute.ResourceSKU{ - Name: to.Ptr(""), - ResourceType: to.Ptr(""), + sku: compute.ResourceSku{ + Name: to.StringPtr(""), + ResourceType: to.StringPtr(""), }, expectResourceType: "", expectName: "", }, "populated resourceType should return correctly": { - sku: armcompute.ResourceSKU{ - Name: to.Ptr("foo"), - ResourceType: to.Ptr("foo"), + sku: compute.ResourceSku{ + Name: to.StringPtr("foo"), + ResourceType: to.StringPtr("foo"), }, expectResourceType: "foo", expectName: "foo", @@ -284,30 +284,30 @@ func Test_SKU_GetResourceTypeAndName(t *testing.T) { func Test_SKU_IsResourceType(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku resourceType string expect bool }{ "nil resourceType should not match anything": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, resourceType: "", }, "empty resourceType should match empty string": { - sku: armcompute.ResourceSKU{ - ResourceType: to.Ptr(""), + sku: compute.ResourceSku{ + ResourceType: to.StringPtr(""), }, resourceType: "", expect: true, }, "empty resourceType should not match non-empty string": { - sku: armcompute.ResourceSKU{ - ResourceType: to.Ptr(""), + sku: compute.ResourceSku{ + ResourceType: to.StringPtr(""), }, resourceType: "foo", }, "populated resourceType should match itself": { - sku: armcompute.ResourceSKU{ - ResourceType: to.Ptr("foo"), + sku: compute.ResourceSku{ + ResourceType: to.StringPtr("foo"), }, resourceType: "foo", expect: true, @@ -327,48 +327,48 @@ func Test_SKU_IsResourceType(t *testing.T) { func Test_SKU_GetLocation(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku expect string expectErr string }{ "nil locations should return empty string": { - sku: armcompute.ResourceSKU{}, + sku: compute.ResourceSku{}, expect: "", }, "empty array of locations return empty string": { - sku: armcompute.ResourceSKU{ - Locations: []*string{}, + sku: compute.ResourceSku{ + Locations: &[]string{}, }, expect: "", }, "single empty value should return empty string": { - sku: armcompute.ResourceSKU{ - Locations: []*string{ - to.Ptr(""), + sku: compute.ResourceSku{ + Locations: &[]string{ + "", }, }, expect: "", }, "populated location should return correctly": { - sku: armcompute.ResourceSKU{ - Locations: []*string{ - to.Ptr("foo"), + sku: compute.ResourceSku{ + Locations: &[]string{ + "foo", }, }, expect: "foo", }, "should return error with multiple choices": { - sku: armcompute.ResourceSKU{ - Locations: []*string{ - to.Ptr("bar"), - to.Ptr("foo"), + sku: compute.ResourceSku{ + Locations: &[]string{ + "bar", + "foo", }, }, expectErr: "sku had multiple locations, refusing to disambiguate", }, "should return error with no choices": { - sku: armcompute.ResourceSKU{ - Locations: []*string{}, + sku: compute.ResourceSku{ + Locations: &[]string{}, }, expectErr: "sku had no locations", }, @@ -399,22 +399,22 @@ func Test_SKU_AvailabilityZones(t *testing.T) {} //nolint:funlen func Test_SKU_HasCapabilityInZone(t *testing.T) { cases := map[string]struct { - sku armcompute.ResourceSKU + sku compute.ResourceSku capability string zone string expect bool }{ "should return false when capability is false": { - sku: armcompute.ResourceSKU{ - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + sku: compute.ResourceSku{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + ZoneDetails: &[]compute.ResourceSkuZoneDetails{ { - Name: []*string{to.Ptr("1"), to.Ptr("3")}, - Capabilities: []*armcompute.ResourceSKUCapabilities{ + Name: &[]string{"1", "3"}, + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("False"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("False"), }, }, }, @@ -427,16 +427,16 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: false, }, "should return false when zone doesn't match": { - sku: armcompute.ResourceSKU{ - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + sku: compute.ResourceSku{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + ZoneDetails: &[]compute.ResourceSkuZoneDetails{ { - Name: []*string{to.Ptr("1"), to.Ptr("3")}, - Capabilities: []*armcompute.ResourceSKUCapabilities{ + Name: &[]string{"1", "3"}, + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("True"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("True"), }, }, }, @@ -449,11 +449,11 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: false, }, "should not return true when the capability is not set in availability zone but set on sku capability": { - sku: armcompute.ResourceSKU{ - Capabilities: []*armcompute.ResourceSKUCapabilities{ + sku: compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("True"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("True"), }, }, }, @@ -462,16 +462,16 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: false, }, "should return true when capability and zone match": { - sku: armcompute.ResourceSKU{ - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + sku: compute.ResourceSku{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + ZoneDetails: &[]compute.ResourceSkuZoneDetails{ { - Name: []*string{to.Ptr("1"), to.Ptr("3")}, - Capabilities: []*armcompute.ResourceSKUCapabilities{ + Name: &[]string{"1", "3"}, + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("True"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("True"), }, }, }, @@ -484,16 +484,16 @@ func Test_SKU_HasCapabilityInZone(t *testing.T) { expect: true, }, "should return true when capability and zone match for zone 3": { - sku: armcompute.ResourceSKU{ - LocationInfo: []*armcompute.ResourceSKULocationInfo{ + sku: compute.ResourceSku{ + LocationInfo: &[]compute.ResourceSkuLocationInfo{ { - ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + ZoneDetails: &[]compute.ResourceSkuZoneDetails{ { - Name: []*string{to.Ptr("1"), to.Ptr("3")}, - Capabilities: []*armcompute.ResourceSKUCapabilities{ + Name: &[]string{"1", "3"}, + Capabilities: &[]compute.ResourceSkuCapabilities{ { - Name: to.Ptr("foo"), - Value: to.Ptr("True"), + Name: to.StringPtr("foo"), + Value: to.StringPtr("True"), }, }, }, @@ -528,39 +528,41 @@ func Test_SKU_Includes(t *testing.T) { "empty list should not include": { skuList: []SKU{}, sku: SKU{ - Name: to.Ptr("foo"), + Name: to.StringPtr("foo"), }, expect: false, }, "missing name should not include": { skuList: []SKU{ { - Name: to.Ptr("foo"), + Name: to.StringPtr("foo"), }, }, sku: SKU{ - Name: to.Ptr("bar"), + Name: to.StringPtr("bar"), }, expect: false, }, "name is included": { skuList: []SKU{ { - Name: to.Ptr("foo"), + Name: to.StringPtr("foo"), }, { - Name: to.Ptr("bar"), + Name: to.StringPtr("bar"), }, }, sku: SKU{ - Name: to.Ptr("bar"), + Name: to.StringPtr("bar"), }, expect: true, }, } for name, tc := range cases { + tc := tc t.Run(name, func(t *testing.T) { - if diff := cmp.Diff(tc.expect, tc.sku.MemberOf(tc.skuList)); diff != "" { + sku := SKU(tc.sku) + if diff := cmp.Diff(tc.expect, sku.MemberOf(tc.skuList)); diff != "" { t.Error(diff) } }) diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..ac908ba --- /dev/null +++ b/v2/README.md @@ -0,0 +1,139 @@ +# skewer [![GoDoc](https://godoc.org/github.com/Azure/skewer?status.svg)](https://godoc.org/github.com/Azure/skewer) [![codecov](https://codecov.io/gh/azure/skewer/branch/main/graph/badge.svg)](https://codecov.io/gh/azure/skewer) + +A package to simplify working with Azure's Resource SKU APIs by wrapping +the existing Azure SDK for Go. + +## Usage + +This package requires an existing, authorized Azure client. Here is a +complete example using the simplest methods. + +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + + "github.com/Azure/skewer/v2" +) + +func main() { + // az login + // export AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv) + sub := os.Getenv("AZURE_SUBSCRIPTION_ID") + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + fmt.Printf("failed to get credential: %s", err) + os.Exit(1) + } + + client, err := armcompute.NewResourceSKUsClient(sub, cred, nil) + if err != nil { + fmt.Printf("failed to get client: %s", err) + os.Exit(1) + } + + cache, err := skewer.NewCache(context.Background(), skewer.WithLocation("eastus"), skewer.WithResourceClient(client)) + if err != nil { + fmt.Printf("failed to instantiate sku cache: %s", err) + os.Exit(1) + } + + for _, sku := range cache.List(context.Background()) { + fmt.Printf("sku: %s\n", sku.GetName()) + } +} +``` + +Once we have a cache, we can query against its contents: +```go +sku, err := cache.Get(context.Background(), "standard_d4s_v3", skewer.VirtualMachines, "eastus") +if err != nil { + return fmt.Errorf("failed to find virtual machine sku standard_d4s_v3: %s", err) +} + +// Check for capabilities +if sku.IsEphemeralOSDiskSupported() { + fmt.Printf("SKU %s supports ephemeral OS disk!\n", sku.GetName()) +} + +cpu, err := sku.VCPU() +if err != nil { + return fmt.Errorf("failed to parse cpu from sku: %s", err) +} + +memory, err := sku.Memory() +if err != nil { + return fmt.Errorf("failed to parse memory from sku: %s", err) +} + +fmt.Printf("vm sku %s has %d vCPU cores and %.2fGi of memory\n", sku.GetName(), cpu, memory) +``` + +# Development + +This project uses a simple [justfile](https://github.com/casey/just) for +make-like functionality. The commands are simple enough to run on their +own if you do not want to install just. + +For each command below like `just $CMD`, the full manual commands are +below separated by one line. + +Default: tidy, fmt, lint, test and calculate coverage. +``` +$ just +$ +$ go mod tidy +$ go fmt +$ golangci-lint run --fix +$ go test -v -race -coverprofile=coverage.out -covermode=atomic ./... +$ go tool cover -html=coverage.out -o coverage.html +``` + +Clean up dependencies: +``` +$ just tidy +$ +$ go mod tidy +``` + +Format: +``` +$ just fmt +$ +$ go fmt +``` + +Lint: +``` +$ just lint +$ +$ golangci-lint run --fix +``` + +Test and calculate coverage: +``` +$ just cover +$ +$ go test -v -race -coverprofile=coverage.out -covermode=atomic ./... +$ go tool cover -html=coverage.out -o coverage.html +``` + +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/v2/cache.go b/v2/cache.go new file mode 100644 index 0000000..d63092b --- /dev/null +++ b/v2/cache.go @@ -0,0 +1,371 @@ +package skewer + +import ( + "context" + "fmt" + "strings" +) + +// Config contains configuration options for a cache. +type Config struct { + location string + includeExtendedLocations string + filter string + client client +} + +// Cache stores a list of known skus, possibly fetched with a provided client +type Cache struct { + config *Config + data []SKU +} + +// Option describes functional options to customize the listing behavior of the cache. +type Option func(c *Config) (*Config, error) + +// WithLocation is a functional option to filter skus by location +func WithLocation(location string) Option { + return func(c *Config) (*Config, error) { + c.location = location + c.filter = fmt.Sprintf("location eq '%s'", location) + return c, nil + } +} + +// WithExtendedLocations is a functional option to include extended locations +func WithExtendedLocations() Option { + return func(c *Config) (*Config, error) { + c.includeExtendedLocations = "true" + return c, nil + } +} + +// ErrClientNil will be returned when a user attempts to create a cache +// without a client and use it. +type ErrClientNil struct { +} + +func (e *ErrClientNil) Error() string { + return "cache requires a client provided by functional options to refresh" +} + +// ErrClientNotNil will be returned when a user attempts to set two +// clients on the same cache. +type ErrClientNotNil struct { +} + +func (e *ErrClientNotNil) Error() string { + return "only provide one client option when instantiating a cache" +} + +// WithClient is a functional option to use a cache +// backed by a client meeting the skewer signature. +func WithClient(client client) Option { + return func(c *Config) (*Config, error) { + if c.client != nil { + return nil, &ErrClientNotNil{} + } + c.client = client + return c, nil + } +} + +// WithResourceClient is a functional option to use a cache +// backed by a ResourceClient. +func WithResourceClient(client ResourceClient) Option { + return func(c *Config) (*Config, error) { + if c.client != nil { + return nil, &ErrClientNotNil{} + } + c.client = newWrappedResourceClient(client) + return c, nil + } +} + +// NewCacheFunc describes the live cache instantiation signature. Used +// for testing. +type NewCacheFunc func(ctx context.Context, opts ...Option) (*Cache, error) + +// NewCache instantiates a cache of resource sku data with a ResourceClient +// client, optionally with additional filtering by location. The +// accepted client interface matches the real Azure clients (it returns +// a paginated iterator). +func NewCache(ctx context.Context, opts ...Option) (*Cache, error) { + config := &Config{} + + for _, optionFn := range opts { + var err error + if config, err = optionFn(config); err != nil { + return nil, err + } + } + + if config.client == nil { + return nil, &ErrClientNil{} + } + + c := &Cache{ + config: config, + } + + if err := c.refresh(ctx); err != nil { + return nil, err + } + + return c, nil +} + +// NewStaticCache initializes a cache with data and no ability to refresh. Used for testing. +func NewStaticCache(data []SKU, opts ...Option) (*Cache, error) { + config := &Config{} + + for _, optionFn := range opts { + var err error + if config, err = optionFn(config); err != nil { + return nil, err + } + } + + c := &Cache{ + data: data, + config: config, + } + + return c, nil +} + +func (c *Cache) refresh(ctx context.Context) error { + data, err := c.config.client.List(ctx, c.config.filter, c.config.includeExtendedLocations) + if err != nil { + return err + } + + c.data = Wrap(data) + + return nil +} + +// ErrMultipleSKUsMatch will be returned when multiple skus match a +// fully qualified triple of resource type, location and name. This should usually not happen. +type ErrMultipleSKUsMatch struct { + Name string + Location string + Type string +} + +func (e *ErrMultipleSKUsMatch) Error() string { + return fmt.Sprintf("found multiple skus matching type: %s, name %s, and location %s", e.Type, e.Name, e.Location) +} + +// ErrSKUNotFound will be returned when no skus match a fully qualified +// triple of resource type, location and name. The SKU may not exist. +type ErrSKUNotFound struct { + Name string + Location string + Type string +} + +func (e *ErrSKUNotFound) Error() string { + return fmt.Sprintf("failed to find any skus matching type: %s, name %s, and location %s", e.Type, e.Name, e.Location) +} + +// Get returns the first matching resource of a given name and type in a location. +func (c *Cache) Get(ctx context.Context, name, resourceType, location string) (SKU, error) { + filtered := Filter(c.data, []FilterFn{ + ResourceTypeFilter(resourceType), + NameFilter(name), + LocationFilter(location), + }...) + + if len(filtered) > 1 { + return SKU{}, &ErrMultipleSKUsMatch{ + Name: name, + Location: location, + Type: resourceType, + } + } + + if len(filtered) < 1 { + return SKU{}, &ErrSKUNotFound{ + Name: name, + Location: location, + Type: resourceType, + } + } + + return filtered[0], nil +} + +// List returns all resource types for this location. +func (c *Cache) List(ctx context.Context, filters ...FilterFn) []SKU { + return Filter(c.data, filters...) +} + +// GetVirtualMachines returns the list of all virtual machines *SKUs in a given azure location. +func (c *Cache) GetVirtualMachines(ctx context.Context) []SKU { + return Filter(c.data, ResourceTypeFilter(VirtualMachines)) +} + +// GetVirtualMachineAvailabilityZones returns all virtual machine zones available in a given location. +func (c *Cache) GetVirtualMachineAvailabilityZones(ctx context.Context) []string { + return c.GetAvailabilityZones(ctx, ResourceTypeFilter(VirtualMachines)) +} + +// GetVirtualMachineAvailabilityZonesForSize returns all virtual machine zones available in a given location. +func (c *Cache) GetVirtualMachineAvailabilityZonesForSize(ctx context.Context, size string) []string { + return c.GetAvailabilityZones(ctx, ResourceTypeFilter(VirtualMachines), NameFilter(size)) +} + +// GetAvailabilityZones returns the list of all availability zones in a given azure location. +func (c *Cache) GetAvailabilityZones(ctx context.Context, filters ...FilterFn) []string { + allZones := make(map[string]bool) + + Map(c.data, func(s *SKU) SKU { + if All(s, filters) { + for zone := range s.AvailabilityZones(c.config.location) { + allZones[zone] = true + } + } + return SKU{} + }) + + result := make([]string, 0, len(allZones)) + for zone := range allZones { + result = append(result, zone) + } + + return result +} + +// Equal compares two configs. +func (c *Config) Equal(other *Config) bool { + if c == nil && other == nil { + return true + } + if c == nil && other != nil { + return false + } + if c != nil && other == nil { + return false + } + return c.location == other.location && + c.filter == other.filter +} + +// Equal compares two caches. +func (c *Cache) Equal(other *Cache) bool { + if c == nil && other == nil { + return true + } + if c == nil && other != nil { + return false + } + if c != nil && other == nil { + return false + } + if c != nil && other != nil { + return c.config.Equal(other.config) + } + if len(c.data) != len(other.data) { + return false + } + for i := range c.data { + // we can't use c.data[i] != other.data[i] since there are many pointers + // use Equal to compare location, type and name + if !c.data[i].Equal(&other.data[i]) { + return false + } + } + return true +} + +// All returns true if the provided sku meets all provided conditions. +func All(sku *SKU, conditions []FilterFn) bool { + for _, condition := range conditions { + if !condition(sku) { + return false + } + } + return true +} + +// Filter returns a new slice containing all values in the slice that +// satisfy all filterFn predicates. +func Filter(skus []SKU, filterFn ...FilterFn) []SKU { + if skus == nil { + return nil + } + + filtered := make([]SKU, 0) + for i := range skus { + if All(&skus[i], filterFn) { + filtered = append(filtered, skus[i]) + } + } + + return filtered +} + +// Map returns a new slice containing the results of applying the +// mapFn to each value in the original slice. +func Map(skus []SKU, fn MapFn) []SKU { + if skus == nil { + return nil + } + + mapped := make([]SKU, 0, len(skus)) + for i := range skus { + mapped = append(mapped, fn(&skus[i])) + } + + return mapped +} + +// FilterFn is a convenience type for filtering. +type FilterFn func(*SKU) bool + +// ResourceTypeFilter produces a filter function for any resource type. +func ResourceTypeFilter(resourceType string) func(*SKU) bool { + return func(s *SKU) bool { + return s.IsResourceType(resourceType) + } +} + +// NameFilter produces a filter function for the name of a resource sku. +func NameFilter(name string) func(*SKU) bool { + return func(s *SKU) bool { + return strings.EqualFold(s.GetName(), name) + } +} + +// LocationFilter matches against a SKU listing the given location +func LocationFilter(location string) func(*SKU) bool { + return func(s *SKU) bool { + return s.HasLocation(normalizeLocation(location)) + } +} + +// UnsafeLocationFilter produces a filter function for the location of a +// resource sku. +// This function dangerously ignores all SKUS without a properly +// specified location. Use this only if you know what you are doing. +func UnsafeLocationFilter(location string) func(*SKU) bool { + return func(s *SKU) bool { + // TODO(ace): how to handle better? + want, err := s.GetLocation() + if err != nil { + return false + } + return locationEquals(want, location) + } +} + +// IncludesFilter returns a FilterFn that checks if the SKU is included in the provided list of SKUs. +func IncludesFilter(skuList []SKU) func(*SKU) bool { + return func(s *SKU) bool { + return s.MemberOf(skuList) + } +} + +// MapFn is a convenience type for mapping. +type MapFn func(*SKU) SKU diff --git a/v2/cache_test.go b/v2/cache_test.go new file mode 100644 index 0000000..05cebda --- /dev/null +++ b/v2/cache_test.go @@ -0,0 +1,570 @@ +package skewer + +import ( + "context" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_NewCache(t *testing.T) { + cases := map[string]struct{}{} + _ = cases +} + +func Test_WithLocation(t *testing.T) { + cases := map[string]struct { + options []Option + expect *Cache + }{ + "should be empty with no options": { + expect: &Cache{ + config: &Config{}, + }, + }, + "should have location and filter": { + options: []Option{WithLocation("foo")}, + expect: &Cache{ + config: &Config{ + filter: "location eq 'foo'", + location: "foo", + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + cache, err := NewStaticCache(nil, tc.options...) + if err != nil { + t.Error(err) + } + if diff := cmp.Diff(tc.expect.config, cache.config); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_Cache_List(t *testing.T) { + cases := map[string]struct{}{} + _ = cases +} +func Test_Cache_GetVirtualMachines(t *testing.T) { + cases := map[string]struct{}{} + _ = cases +} + +func Test_Filter(t *testing.T) { + cases := map[string]struct { + unfiltered []*armcompute.ResourceSKU + condition FilterFn + expected []*armcompute.ResourceSKU + }{ + "nil slice filters to nil slice": { + condition: func(*SKU) bool { return true }, + }, + "empty slice filters to empty slice": { + unfiltered: []*armcompute.ResourceSKU{}, + condition: func(*SKU) bool { return true }, + expected: []*armcompute.ResourceSKU{}, + }, + "slice with non-matching element filters to empty slice": { + unfiltered: []*armcompute.ResourceSKU{ + { + ResourceType: to.Ptr("nomatch"), + }, + }, + condition: func(s *SKU) bool { return s.GetName() == "match" }, + expected: []*armcompute.ResourceSKU{}, + }, + "slice with one matching element doesn't change": { + unfiltered: []*armcompute.ResourceSKU{ + { + ResourceType: to.Ptr("match"), + }, + }, + condition: func(s *SKU) bool { return true }, + expected: []*armcompute.ResourceSKU{ + { + ResourceType: to.Ptr("match"), + }, + }, + }, + "all matching elements removed": { + unfiltered: []*armcompute.ResourceSKU{ + { + ResourceType: to.Ptr("match"), + }, + { + ResourceType: to.Ptr("nomatch"), + }, + { + ResourceType: to.Ptr("match"), + }, + { + ResourceType: to.Ptr("unmatch"), + }, + { + ResourceType: to.Ptr("match"), + }, + }, + condition: func(s *SKU) bool { return !s.IsResourceType("match") }, + expected: []*armcompute.ResourceSKU{ + { + ResourceType: to.Ptr("nomatch"), + }, + { + ResourceType: to.Ptr("unmatch"), + }, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + result := Filter(Wrap(tc.unfiltered), tc.condition) + if diff := cmp.Diff(result, Wrap(tc.expected), []cmp.Option{ + cmpopts.EquateEmpty(), + }...); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_Map(t *testing.T) { + t.Run("nil slice maps to nil slice", func(t *testing.T) { + mapFn := func(*SKU) SKU { return SKU{} } + if Map(nil, mapFn) != nil { + t.Error() + } + }) + + t.Run("empty slice maps to empty slice", func(t *testing.T) { + mapFn := func(*SKU) SKU { return SKU{} } + if len(Map([]SKU{}, mapFn)) != 0 { + t.Error() + } + }) + + t.Run("identity function keeps slice the same", func(t *testing.T) { + mapFn := func(s *SKU) SKU { return *s } + skuList := make([]SKU, 100) + mapped := Map(skuList, mapFn) + if diff := cmp.Diff(mapped, skuList); diff != "" { + t.Error(diff) + } + }) + + t.Run("map hits each element once", func(t *testing.T) { + counter := 0 + skuList := make([]SKU, 100) + Map(skuList, func(s *SKU) SKU { + counter++ + return SKU{} + }) + + if counter != 100 { + t.Error() + } + }) +} + +func Test_Cache_Get(t *testing.T) { //nolint:funlen + cases := map[string]struct { + sku string + resourceType string + have []*armcompute.ResourceSKU + found bool + }{ + "should return false with no data": { + sku: "foo", + resourceType: "bar", + }, + "should match when found at index=0": { + sku: "foo", + resourceType: "bar", + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr("bar"), + Locations: []*string{to.Ptr("")}, + }, + }, + found: true, + }, + "should match when found at index=1": { + sku: "foo", + resourceType: "bar", + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("other"), + ResourceType: to.Ptr("baz"), + }, + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr("bar"), + Locations: []*string{to.Ptr("")}, + }, + }, + found: true, + }, + "should match regardless of sku capitalization": { + sku: "foo", + resourceType: "bar", + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("other"), + ResourceType: to.Ptr("baz"), + }, + { + Name: to.Ptr("FoO"), + ResourceType: to.Ptr("bar"), + Locations: []*string{to.Ptr("")}, + }, + }, + found: true, + }, + "should return false when no match exists": { + sku: "foo", + resourceType: "bar", + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("other"), + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + cache := &Cache{ + data: Wrap(tc.have), + } + + val, err := cache.Get(context.Background(), tc.sku, tc.resourceType, "") + if tc.found { + if err != nil { + t.Errorf("expected success when trying to Get resource with name %s and resourceType %s, but got error: '%s'", + tc.sku, + tc.resourceType, + err, + ) + } + if val.Name == nil { + t.Fatalf("expected name to be %s, but was nil", tc.sku) + return + } + if !strings.EqualFold(*val.Name, tc.sku) { + t.Fatalf("expected name to be %s, but was %s", tc.sku, *val.Name) + } + if val.ResourceType == nil { + t.Fatalf("expected name to be %s, but was nil", tc.sku) + return + } + if *val.ResourceType != tc.resourceType { + t.Fatalf("expected kind to be %s, but was %s", tc.resourceType, *val.ResourceType) + } + } else if err == nil { + t.Errorf("expected Get to fail with name %s and resourceType %s, but succeeded", tc.sku, tc.resourceType) + } + }) + } +} + +func Test_Cache_GetAvailabilityZones(t *testing.T) { //nolint:funlen + cases := map[string]struct { + have []*armcompute.ResourceSKU + want []string + }{ + "should find 1 result": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + want: []string{"1"}, + }, + "should find 2 results": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("2")}, + }, + }, + }, + }, + want: []string{"1", "2"}, + }, + "should not find due to location mismatch": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("foobar"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("foobar"), + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + want: nil, + }, + "should not find due to location restriction": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + Restrictions: []*armcompute.ResourceSKURestrictions{ + { + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeLocation), + Values: []*string{to.Ptr("baz")}, + }, + }, + }, + }, + want: nil, + }, + "should not find due to zone restriction": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + Restrictions: []*armcompute.ResourceSKURestrictions{ + { + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeZone), + Values: []*string{to.Ptr("baz")}, + RestrictionInfo: &armcompute.ResourceSKURestrictionInfo{ + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + }, + want: nil, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + cache, err := NewStaticCache(Wrap(tc.have), WithLocation("baz")) + if err != nil { + t.Error(err) + } + zones := cache.GetAvailabilityZones(context.Background()) + if diff := cmp.Diff(zones, tc.want, []cmp.Option{ + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + }...); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_Cache_GetVirtualMachineAvailabilityZonesForSize(t *testing.T) { //nolint:funlen + cases := map[string]struct { + have []*armcompute.ResourceSKU + want []string + }{ + "should find 1 result": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + want: []string{"1"}, + }, + "should find 2 results": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1"), to.Ptr("2")}, + }, + }, + }, + }, + want: []string{"1", "2"}, + }, + "should not find due to size mismatch": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foobar"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + want: nil, + }, + "should not find due to location mismatch": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("foobar"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("foobar"), + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + want: nil, + }, + "should not find due to location restriction": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + Restrictions: []*armcompute.ResourceSKURestrictions{ + { + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeLocation), + Values: []*string{to.Ptr("baz")}, + }, + }, + }, + }, + want: nil, + }, + "should not find due to zone restriction": { + have: []*armcompute.ResourceSKU{ + { + Name: to.Ptr("foo"), + ResourceType: to.Ptr(string(VirtualMachines)), + Locations: []*string{ + to.Ptr("baz"), + }, + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + Location: to.Ptr("baz"), + Zones: []*string{to.Ptr("1")}, + }, + }, + Restrictions: []*armcompute.ResourceSKURestrictions{ + { + Type: to.Ptr(armcompute.ResourceSKURestrictionsTypeZone), + Values: []*string{to.Ptr("baz")}, + RestrictionInfo: &armcompute.ResourceSKURestrictionInfo{ + Zones: []*string{to.Ptr("1")}, + }, + }, + }, + }, + }, + want: nil, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + cache, err := NewStaticCache(Wrap(tc.have), WithLocation("baz")) + if err != nil { + t.Error(err) + } + zones := cache.GetVirtualMachineAvailabilityZonesForSize(context.Background(), "foo") + if diff := cmp.Diff(zones, tc.want, []cmp.Option{ + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + }...); diff != "" { + t.Fatal(diff) + } + }) + } +} diff --git a/v2/clients.go b/v2/clients.go new file mode 100644 index 0000000..35da160 --- /dev/null +++ b/v2/clients.go @@ -0,0 +1,38 @@ +package skewer + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/pkg/errors" +) + +// wrappedResourceClient defines a wrapper for the typical Azure client +// signature to collect all resource skus from the iterator returned by NewListPager(). +type wrappedResourceClient struct { + client ResourceClient +} + +func newWrappedResourceClient(client ResourceClient) *wrappedResourceClient { + return &wrappedResourceClient{client} +} + +func (w *wrappedResourceClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { + options := &armcompute.ResourceSKUsClientListOptions{} + if filter != "" { + options.Filter = &filter + } + if includeExtendedLocations != "" { + options.IncludeExtendedLocations = &includeExtendedLocations + } + pager := w.client.NewListPager(options) + var skus []*armcompute.ResourceSKU + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not list resource skus") + } + skus = append(skus, page.Value...) + } + return skus, nil +} diff --git a/v2/const.go b/v2/const.go new file mode 100644 index 0000000..d39808d --- /dev/null +++ b/v2/const.go @@ -0,0 +1,83 @@ +package skewer + +const ( + // VirtualMachines is the . + VirtualMachines = "virtualMachines" + // Disks is a convenience constant to filter resource SKUs to only include disks. + Disks = "disks" +) + +// Supported models an enum of possible boolean values for resource support in the Azure API. +type Supported string + +const ( + // CapabilitySupported is an enum value for the string "True" returned when a SKU supports a binary capability. + CapabilitySupported Supported = "True" + // CapabilityUnsupported is an enum value for the string "False" returned when a SKU does not support a binary capability. + CapabilityUnsupported Supported = "False" +) + +const ( + // EphemeralOSDisk identifies the capability for ephemeral os support. + EphemeralOSDisk = "EphemeralOSDiskSupported" + // AcceleratedNetworking identifies the capability for accelerated networking support. + AcceleratedNetworking = "AcceleratedNetworkingEnabled" + // VCPUs identifies the capability for the number of vCPUS. + VCPUs = "vCPUs" + // GPUs identifies the capability for the number of GPUS. + GPUs = "GPUs" + // MemoryGB identifies the capability for memory capacity. + MemoryGB = "MemoryGB" + // HyperVGenerations identifies the hyper-v generations this vm sku supports. + HyperVGenerations = "HyperVGenerations" + // EncryptionAtHost identifies the capability for accelerated networking support. + EncryptionAtHost = "EncryptionAtHostSupported" + // UltraSSDAvailable identifies the capability for ultra ssd + // enablement. + UltraSSDAvailable = "UltraSSDAvailable" + // CachedDiskBytes identifies the maximum size of the cache disk for + // a vm. + CachedDiskBytes = "CachedDiskBytes" + // MaxResourceVolumeMB identifies the maximum size of the temporary + // disk for a vm. + MaxResourceVolumeMB = "MaxResourceVolumeMB" + // CapabilityPremiumIO identifies the capability for PremiumIO. + CapabilityPremiumIO = "PremiumIO" + // CapabilityCpuArchitectureType identifies the type of CPU architecture (x64,Arm64). + CapabilityCPUArchitectureType = "CpuArchitectureType" + // CapabilityTrustedLaunchDisabled identifes whether TrustedLaunch is disabled. + CapabilityTrustedLaunchDisabled = "TrustedLaunchDisabled" + // CapabilityConfidentialComputingType identifies the type of ConfidentialComputing. + CapabilityConfidentialComputingType = "ConfidentialComputingType" + // ConfidentialComputingTypeSNP denoted the "SNP" ConfidentialComputing. + ConfidentialComputingTypeSNP = "SNP" + // DiskControllerTypes identifies the disk controller types supported by the VM SKU. + DiskControllerTypes = "DiskControllerTypes" + // SupportedEphemeralOSDiskPlacements identifies supported ephemeral OS disk placements. + SupportedEphemeralOSDiskPlacements = "SupportedEphemeralOSDiskPlacements" + // NvmeDiskSizeInMiB identifies the NVMe disk size in MiB. + NvmeDiskSizeInMiB = "NvmeDiskSizeInMiB" +) + +const ( + // HyperVGeneration1 identifies a sku which supports HyperV + // Generation 1. + HyperVGeneration1 = "V1" + // HyperVGeneration2 identifies a sku which supports HyperV + // Generation 2. + HyperVGeneration2 = "V2" +) + +const ( + // DiskControllerSCSI identifies the SCSI disk controller type. + DiskControllerSCSI = "SCSI" + // DiskControllerNVMe identifies the NVMe disk controller type. + DiskControllerNVMe = "NVMe" + // EphemeralDiskPlacementNvme identifies NVMe disk placement for ephemeral OS disk. + EphemeralDiskPlacementNvme = "NvmeDisk" +) + +const ( + ten = 10 + sixtyFour = 64 +) diff --git a/v2/data_test.go b/v2/data_test.go new file mode 100644 index 0000000..d53f749 --- /dev/null +++ b/v2/data_test.go @@ -0,0 +1,375 @@ +package skewer + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +var ( + expectedVirtualMachinesCount = 4 + expectedAvailabilityZones = []string{"1", "2", "3"} + shouldNotBePresentCapabilityNotFoundErr = "ShouldNotBePresentCapabilityNotFound" + premiumIOCapabilityValueParseErr = "PremiumIOCapabilityValueParse: failed to parse string 'False' as int64, error: 'strconv.ParseInt: parsing \"False\": invalid syntax'" //nolint:lll + x64ArchType = "x64" +) + +//nolint:gocyclo,funlen +func Test_Data(t *testing.T) { + dataWrapper, err := newDataWrapper("./testdata/eastus.json") + if err != nil { + t.Error(err) + } + + fakeClient := &fakeClient{ + skus: dataWrapper.Value, + } + + resourceClient := newSuccessfulFakeResourceClient([][]*armcompute.ResourceSKU{dataWrapper.Value}) + + chunkedResourceClient := newSuccessfulFakeResourceClient(chunk(dataWrapper.Value, 10)) + + ctx := context.Background() + + cases := map[string]struct { + newCacheFunc NewCacheFunc + }{ + "resourceClient": { + newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { + return NewCache(ctx, WithResourceClient(resourceClient), WithLocation("eastus")) + }, + }, + "chunkedResourceClient": { + newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { + return NewCache(ctx, WithResourceClient(chunkedResourceClient), WithLocation("eastus")) + }, + }, + "wrappedClient": { + newCacheFunc: func(_ context.Context, _ ...Option) (*Cache, error) { + return NewCache(ctx, WithClient(fakeClient), WithLocation("eastus")) + }, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + cache, err := tc.newCacheFunc(ctx) + if err != nil { + t.Error(err) + } + t.Run("virtual machines", func(t *testing.T) { + t.Run("expect 4 virtual machine skus", func(t *testing.T) { + if len(cache.GetVirtualMachines(ctx)) != expectedVirtualMachinesCount { + t.Errorf("expected %d virtual machine skus but found %d", expectedVirtualMachinesCount, len(cache.GetVirtualMachines(ctx))) + } + }) + + t.Run("standard_d4s_v3", func(t *testing.T) { + errCapabilityValueNil := &ErrCapabilityValueParse{} + errCapabilityNotFound := &ErrCapabilityNotFound{} + + sku, err := cache.Get(ctx, "standard_d4s_v3", VirtualMachines, "eastus") + if err != nil { + t.Errorf("expected to find virtual machine sku standard_d4s_v3") + } + if name := sku.GetName(); !strings.EqualFold(name, "standard_d4s_v3") { + t.Errorf("expected standard_d4s_v3 to have name standard_d4s_v3, got: '%s'", name) + } + if skuFamily := sku.GetFamilyName(); !strings.EqualFold(skuFamily, "standardDSv3Family") { + t.Errorf("expected standard_d4s_v3 to have name standardDSv3Family, got: '%s'", skuFamily) + } + if skuSize := sku.GetSize(); !strings.EqualFold(skuSize, "d4s_v3") { + t.Errorf("expected standard_d4s_v3 to have name d4s_v3 size, got: '%s'", skuSize) + } + if resourceType := sku.GetResourceType(); resourceType != VirtualMachines { + t.Errorf("expected standard_d4s_v3 to have resourceType virtual machine, got: '%s'", resourceType) + } + if cpu, err := sku.VCPU(); cpu != 4 || err != nil { + t.Errorf("expected standard_d4s_v3 to have 4 vCPUs and parse successfully, got value '%d' and error '%s'", cpu, err) + } + if memory, err := sku.Memory(); memory != 16 || err != nil { + t.Errorf("expected standard_d4s_v3 to have 16GB of memory and parse successfully, got value '%f' and error '%s'", memory, err) + } + if quantity, err := sku.GetCapabilityIntegerQuantity("ShouldNotBePresent"); quantity != -1 || !errors.As(err, &errCapabilityNotFound) { + t.Errorf("expected standard_d4s_v3 not to have a non-existent capability, got value '%d' and error '%s'", quantity, err) + } + if quantity, err := sku.GetCapabilityIntegerQuantity("PremiumIO"); quantity != -1 || !errors.As(err, &errCapabilityValueNil) { + t.Errorf("expected standard_d4s_v3 to fail parsing value for boolean premiumIO as int, got value '%d' and error '%s'", quantity, err) + } + if !sku.HasZonalCapability(UltraSSDAvailable) { + t.Errorf("expected standard_d4s_v3 to support ultra ssd") + } + if sku.HasZonalCapability("NotExistingCapability") { + t.Errorf("expected standard_d4s_v3 not to support non-existent capability") + } + if !sku.HasCapability(EphemeralOSDisk) { + t.Errorf("expected standard_d4s_v3 to support ephemeral os") + } + if !sku.IsAcceleratedNetworkingSupported() { + t.Errorf("expected standard_d4s_v3 to support accelerated networking") + } + if cpuArch, err := sku.GetCPUArchitectureType(); err != nil || cpuArch != x64ArchType { + t.Errorf("expected standard_d4s_v3 to have x64 cpuArchitectureType") + } + if !sku.IsPremiumIO() { + t.Errorf("expected standard_d4s_v3 to support PremiumIO") + } + if !sku.IsHyperVGen1Supported() { + t.Errorf("expected standard_d4s_v3 to support hyper v gen1") + } + if !sku.IsHyperVGen2Supported() { + t.Errorf("expected standard_d4s_v3 to support hyper v gen2") + } + if !sku.HasCapability(EncryptionAtHost) { + t.Errorf("expected standard_d4s_v3 to support encryption at host") + } + if !sku.IsAvailable("eastus") { + t.Errorf("expected standard_d4s_v3 to be available in eastus") + } + if sku.IsRestricted("eastus") { + t.Errorf("expected standard_d4s_v3 to be unrestricted in eastus") + } + if sku.IsAvailable("westus2") { + t.Errorf("expected standard_d4s_v3 not to be available in westus2") + } + if sku.IsRestricted("westus2") { + t.Errorf("expected standard_d4s_v3 not to be restricted in westus2") + } + if quantity, err := sku.MaxResourceVolumeMB(); quantity != 32768 || errors.As(err, &errCapabilityNotFound) { + t.Errorf("expected standard_d4s_v3 to have 32768 MB of temporary disk, got value '%d' and error '%s'", quantity, err) + } + if isSupported, err := sku.HasCapabilityWithMinCapacity("MaxResourceVolumeMB", 32768); !isSupported || err != nil { + t.Errorf("expected standard_d4s_v3 to fit 32GB temp disk, got '%t', error: %s", isSupported, err) + } + if isSupported, err := sku.HasCapabilityWithMinCapacity("MaxResourceVolumeMB", 32769); isSupported || err != nil { + t.Errorf("expected standard_d4s_v3 not to fit 32GB +1 byte temp disk, got '%t', error: %s", isSupported, err) + } + hasV1 := !sku.HasCapabilityWithSeparator(HyperVGenerations, "V1") + hasV2 := !sku.HasCapabilityWithSeparator(HyperVGenerations, "V2") + if hasV1 || hasV2 { + t.Errorf("expected standard_d4s_v3 to support hyper-v generation v1 and v2, got v1: '%t' , v2: '%t'", hasV1, hasV2) + } + }) + + t.Run("standard_d2_v2", func(t *testing.T) { + errCapabilityValueNil := &ErrCapabilityValueParse{} + errCapabilityNotFound := &ErrCapabilityNotFound{} + + sku, err := cache.Get(ctx, "Standard_D2_v2", VirtualMachines, "eastus") + if err != nil { + t.Errorf("expected to find virtual machine sku standard_d2_v2") + } + if name := sku.GetName(); !strings.EqualFold(name, "standard_d2_v2") { + t.Errorf("expected standard_d2_v2 to have name standard_d2_v2, got: '%s'", name) + } + if skuFamily := sku.GetFamilyName(); !strings.EqualFold(skuFamily, "standardDv2Family") { + t.Errorf("expected standard_d2_v2 to have name standardDv2Family, got: '%s'", skuFamily) + } + if skuSize := sku.GetSize(); !strings.EqualFold(skuSize, "d2_v2") { + t.Errorf("expected standard_d2_v2 to have name d2_v2 size, got: '%s'", skuSize) + } + if resourceType := sku.GetResourceType(); resourceType != VirtualMachines { + t.Errorf("expected standard_d2_v2 to have resourceType virtual machine, got: '%s'", resourceType) + } + if cpu, err := sku.VCPU(); cpu != 2 || err != nil { + t.Errorf("expected standard_d2_v2 to have 2 vCPUs and parse successfully, got value '%d' and error '%s'", cpu, err) + } + if memory, err := sku.Memory(); memory != 7 || err != nil { + t.Errorf("expected standard_d2_v2 to have 7GB of memory and parse successfully, got value '%f' and error '%s'", memory, err) + } + if quantity, err := sku.GetCapabilityIntegerQuantity("ShouldNotBePresent"); quantity != -1 || + !errors.As(err, &errCapabilityNotFound) || + err.Error() != shouldNotBePresentCapabilityNotFoundErr { + t.Errorf("expected standard_d2_v2 not to have a non-existent capability, got value '%d' and error '%s'", quantity, err) + } + if quantity, err := sku.GetCapabilityIntegerQuantity(CapabilityPremiumIO); quantity != -1 || + !errors.As(err, &errCapabilityValueNil) || + err.Error() != premiumIOCapabilityValueParseErr { + t.Errorf("expected standard_d2_v2 to fail parsing value for boolean premiumIO as int, got value '%d' and error '%s'", quantity, err) + } + if sku.HasZonalCapability(UltraSSDAvailable) { + t.Errorf("expected standard_d2_v2 not to support ultra ssd") + } + if sku.HasZonalCapability("NotExistingCapability") { + t.Errorf("expected standard_d2_v2 not to support non-existent capability") + } + if sku.HasCapability(EphemeralOSDisk) { + t.Errorf("expected standard_d2_v2 not to support ephemeral os") + } + if !sku.IsAcceleratedNetworkingSupported() { + t.Errorf("expected standard_d2_v2 to support accelerated networking") + } + if cpuArch, err := sku.GetCPUArchitectureType(); err != nil || cpuArch != x64ArchType { + t.Errorf("expected standard_d2_v2 to have x64 cpuArchitectureType") + } + if sku.IsPremiumIO() { + t.Errorf("expected standard_d2_v2 to not support PremiumIO") + } + if !sku.IsHyperVGen1Supported() { + t.Errorf("expected standard_d2_v2 to support hyper v gen1") + } + if sku.IsHyperVGen2Supported() { + t.Errorf("expected standard_d2_v2 not to support hyper v gen2") + } + if sku.HasCapability(EncryptionAtHost) { + t.Errorf("expected standard_d2_v2 not to support encryption at host") + } + if !sku.IsAvailable("eastus") { + t.Errorf("expected standard_d2_v2 to be available in eastus") + } + if sku.IsRestricted("eastus") { + t.Errorf("expected standard_d2_v2 to be unrestricted in eastus") + } + if sku.IsAvailable("westus2") { + t.Errorf("expected standard_d2_v2 not to be available in westus2") + } + if sku.IsRestricted("westus2") { + t.Errorf("expected standard_d2_v2 not to be restricted in westus2") + } + if quantity, err := sku.MaxResourceVolumeMB(); quantity != 102400 || errors.As(err, &errCapabilityNotFound) { + t.Errorf("expected standard_d2_v2 to have 102400 MB of temporary disk, got value '%d' and error '%s'", quantity, err) + } + if isSupported, err := sku.HasCapabilityWithMinCapacity("MemoryGB", 1000); isSupported || err != nil { + t.Errorf("expected standard_d2_v2 not to have 1000GB of memory, got '%t', error: %s", isSupported, err) + } + hasV1 := !sku.HasCapabilityWithSeparator(HyperVGenerations, "V1") + hasV2 := sku.HasCapabilityWithSeparator(HyperVGenerations, "V2") + if hasV1 || hasV2 { + t.Errorf("expected standard_d2_v2 to support hyper-v generation v1 but not v2, got v1: '%t' , v2: '%t'", hasV1, hasV2) + } + }) + + t.Run("Standard_NV6", func(t *testing.T) { + errCapabilityValueNil := &ErrCapabilityValueParse{} + errCapabilityNotFound := &ErrCapabilityNotFound{} + + sku, err := cache.Get(ctx, "Standard_NV6", VirtualMachines, "eastus") + if err != nil { + t.Errorf("expected to find virtual machine sku Standard_NV6") + } + if name := sku.GetName(); !strings.EqualFold(name, "standard_nv6") { + t.Errorf("expected standard_nv6 to have name standard_nv6, got: '%s'", name) + } + if skuFamily := sku.GetFamilyName(); !strings.EqualFold(skuFamily, "standardNVFamily") { + t.Errorf("expected standard_nv6 to have name standardNVFamily, got: '%s'", skuFamily) + } + if skuSize := sku.GetSize(); !strings.EqualFold(skuSize, "nv6") { + t.Errorf("expected standard_nv6 to have name nv6 size, got: '%s'", skuSize) + } + if resourceType := sku.GetResourceType(); resourceType != VirtualMachines { + t.Errorf("expected standard_nv6 to have resourceType virtual machine, got: '%s'", resourceType) + } + if cpu, err := sku.VCPU(); cpu != 6 || err != nil { + t.Errorf("expected standard_nv6 to have 6 vCPUs and parse successfully, got value '%d' and error '%s'", cpu, err) + } + if cpu, err := sku.GPU(); cpu != 1 || err != nil { + t.Errorf("expected standard_nv6 to have 1 GPUs and parse successfully, got value '%d' and error '%s'", cpu, err) + } + if memory, err := sku.Memory(); memory != 56 || err != nil { + t.Errorf("expected standard_nv6 to have 56GB of memory and parse successfully, got value '%f' and error '%s'", memory, err) + } + if quantity, err := sku.GetCapabilityIntegerQuantity("ShouldNotBePresent"); quantity != -1 || + !errors.As(err, &errCapabilityNotFound) || + err.Error() != shouldNotBePresentCapabilityNotFoundErr { + t.Errorf("expected standard_nv6 not to have a non-existent capability, got value '%d' and error '%s'", quantity, err) + } + if quantity, err := sku.GetCapabilityIntegerQuantity(CapabilityPremiumIO); quantity != -1 || + !errors.As(err, &errCapabilityValueNil) || + err.Error() != premiumIOCapabilityValueParseErr { + t.Errorf("expected standard_nv6 to fail parsing value for boolean premiumIO as int, got value '%d' and error '%s'", quantity, err) + } + if sku.HasZonalCapability(UltraSSDAvailable) { + t.Errorf("expected standard_nv6 not to support ultra ssd") + } + if sku.HasZonalCapability("NotExistingCapability") { + t.Errorf("expected standard_nv6 not to support non-existent capability") + } + if sku.HasCapability(EphemeralOSDisk) { + t.Errorf("expected standard_nv6 not to support ephemeral os") + } + if sku.IsAcceleratedNetworkingSupported() { + t.Errorf("expected standard_nv6 to not support accelerated networking") + } + if cpuArch, err := sku.GetCPUArchitectureType(); err != nil || cpuArch != x64ArchType { + t.Errorf("expected standard_nv6 to have x64 cpuArchitectureType") + } + if sku.IsPremiumIO() { + t.Errorf("expected standard_nv6 to not support PremiumIO") + } + if !sku.IsHyperVGen1Supported() { + t.Errorf("expected standard_nv6 to support hyper v gen1") + } + if sku.IsHyperVGen2Supported() { + t.Errorf("expected standard_nv6 not to support hyper v gen2") + } + if sku.HasCapability(EncryptionAtHost) { + t.Errorf("expected standard_nv6 not to support encryption at host") + } + if !sku.IsAvailable("eastus") { + t.Errorf("expected standard_nv6 to be available in eastus") + } + if sku.IsRestricted("eastus") { + t.Errorf("expected standard_nv6 to be unrestricted in eastus") + } + if sku.IsAvailable("westus2") { + t.Errorf("expected standard_nv6 not to be available in westus2") + } + if sku.IsRestricted("westus2") { + t.Errorf("expected standard_nv6 not to be restricted in westus2") + } + if quantity, err := sku.MaxResourceVolumeMB(); quantity != 389120 || errors.As(err, &errCapabilityNotFound) { + t.Errorf("expected standard_nv6 to have 389120 MB of temporary disk, got value '%d' and error '%s'", quantity, err) + } + if isSupported, err := sku.HasCapabilityWithMinCapacity("MemoryGB", 1000); isSupported || err != nil { + t.Errorf("expected standard_nv6 not to have 1000GB of memory, got '%t', error: %s", isSupported, err) + } + hasV1 := !sku.HasCapabilityWithSeparator(HyperVGenerations, "V1") + hasV2 := sku.HasCapabilityWithSeparator(HyperVGenerations, "V2") + if hasV1 || hasV2 { + t.Errorf("expected standard_nv6 to support hyper-v generation v1 but not v2, got v1: '%t' , v2: '%t'", hasV1, hasV2) + } + }) + + t.Run("standard_D13_v2_promo", func(t *testing.T) { + errCapabilityNotFound := &ErrCapabilityNotFound{} + sku, err := cache.Get(ctx, "standard_D13_v2_promo", VirtualMachines, "eastus") + if err != nil { + t.Errorf("expected to find virtual machine sku standard_D13_v2_promo") + } + if sku.IsAvailable("eastus") { + t.Errorf("expected standard_D13_v2_promo to be unavailable in eastus") + } + if !sku.IsRestricted("eastus") { + t.Errorf("expected standard_D13_v2_promo to be restricted in eastus") + } + if sku.IsAvailable("westus2") { + t.Errorf("expected standard_D13_v2_promo not to be available in westus2") + } + if sku.IsRestricted("westus2") { + t.Errorf("expected standard_D13_v2_promo not to be restricted in westus2") + } + if cpuArch, err := sku.GetCPUArchitectureType(); !errors.As(err, &errCapabilityNotFound) || cpuArch != "" { + t.Errorf("expected standard_D13_v2_promo to not have cpuArchitectureType, got %s as cpuArchType with error as %s", cpuArch, err) + } + }) + }) + + t.Run("availability zones", func(t *testing.T) { + if diff := cmp.Diff(cache.GetAvailabilityZones(ctx), expectedAvailabilityZones, []cmp.Option{ + cmpopts.EquateEmpty(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + }...); diff != "" { + t.Errorf("expected and actual availability zones mismatch: %s", diff) + } + }) + }) + } +} diff --git a/v2/disk.go b/v2/disk.go new file mode 100644 index 0000000..5fffbcd --- /dev/null +++ b/v2/disk.go @@ -0,0 +1,25 @@ +package skewer + +// HasSCSISupport determines if a SKU supports SCSI disk controller type. +// If no disk controller types are declared, it assumes SCSI is supported for backward compatibility. +func (s *SKU) HasSCSISupport() bool { + declaresSCSI := s.HasCapabilityWithSeparator(DiskControllerTypes, DiskControllerSCSI) + declaresNothing := !(declaresSCSI || s.HasNVMeSupport()) + return declaresSCSI || declaresNothing +} + +// HasNVMeSupport determines if a SKU supports NVMe disk controller type. +func (s *SKU) HasNVMeSupport() bool { + return s.HasCapabilityWithSeparator(DiskControllerTypes, DiskControllerNVMe) +} + +// SupportsNVMeEphemeralOSDisk determines if a SKU supports NVMe placement for ephemeral OS disk. +func (s *SKU) SupportsNVMeEphemeralOSDisk() bool { + return s.HasCapabilityWithSeparator(SupportedEphemeralOSDiskPlacements, EphemeralDiskPlacementNvme) +} + +// NVMeDiskSizeInMiB returns the NVMe disk size in MiB for the SKU. +// Returns an error if the capability is not found, nil, or cannot be parsed. +func (s *SKU) NVMeDiskSizeInMiB() (int64, error) { + return s.GetCapabilityIntegerQuantity(NvmeDiskSizeInMiB) +} diff --git a/v2/disk_test.go b/v2/disk_test.go new file mode 100644 index 0000000..d353c8f --- /dev/null +++ b/v2/disk_test.go @@ -0,0 +1,272 @@ +package skewer + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +func Test_SKU_HasSCSISupport(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + expect bool + }{ + "empty capability list should return true (backward compatibility)": { + sku: armcompute.ResourceSKU{}, + expect: true, + }, + "no disk controller capability should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, + }, + expect: true, + }, + "SCSI only should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI"), + }, + }, + }, + expect: true, + }, + "SCSI and NVMe should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI,NVMe"), + }, + }, + }, + expect: true, + }, + "NVMe only should return false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("NVMe"), + }, + }, + }, + expect: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + actual := sku.HasSCSISupport() + if actual != tc.expect { + t.Fatalf("expected %v but got %v", tc.expect, actual) + } + }) + } +} + +func Test_SKU_HasNVMeSupport(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + expect bool + }{ + "empty capability list should return false": { + sku: armcompute.ResourceSKU{}, + expect: false, + }, + "no disk controller capability should return false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, + }, + expect: false, + }, + "SCSI only should return false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI"), + }, + }, + }, + expect: false, + }, + "SCSI and NVMe should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI,NVMe"), + }, + }, + }, + expect: true, + }, + "NVMe only should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("NVMe"), + }, + }, + }, + expect: true, + }, + "NVMe in mixed case should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(DiskControllerTypes), + Value: to.Ptr("SCSI,NVMe,Other"), + }, + }, + }, + expect: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + actual := sku.HasNVMeSupport() + if actual != tc.expect { + t.Fatalf("expected %v but got %v", tc.expect, actual) + } + }) + } +} + +func Test_SKU_SupportsNVMeEphemeralOSDisk(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + expect bool + }{ + "empty capability list should return false": { + sku: armcompute.ResourceSKU{}, + expect: false, + }, + "no ephemeral placement capability should return false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("vCPUs"), + Value: to.Ptr("8"), + }, + }, + }, + expect: false, + }, + "ResourceDisk only should return false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(SupportedEphemeralOSDiskPlacements), + Value: to.Ptr("ResourceDisk"), + }, + }, + }, + expect: false, + }, + "NvmeDisk should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(SupportedEphemeralOSDiskPlacements), + Value: to.Ptr("NvmeDisk"), + }, + }, + }, + expect: true, + }, + "ResourceDisk and NvmeDisk should return true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(SupportedEphemeralOSDiskPlacements), + Value: to.Ptr("ResourceDisk,NvmeDisk"), + }, + }, + }, + expect: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + actual := sku.SupportsNVMeEphemeralOSDisk() + if actual != tc.expect { + t.Fatalf("expected %v but got %v", tc.expect, actual) + } + }) + } +} + +func Test_SKU_NVMeDiskSizeInMiB(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + expect int64 + err string + }{ + "empty capability list should return error": { + sku: armcompute.ResourceSKU{}, + err: (&ErrCapabilityNotFound{NvmeDiskSizeInMiB}).Error(), + }, + "no NVMe disk size capability should return error": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("vCPUs"), + Value: to.Ptr("8"), + }, + }, + }, + err: (&ErrCapabilityNotFound{NvmeDiskSizeInMiB}).Error(), + }, + "valid NVMe disk size should return value": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(NvmeDiskSizeInMiB), + Value: to.Ptr("1024000"), + }, + }, + }, + expect: 1024000, + }, + "invalid NVMe disk size should return parse error": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(NvmeDiskSizeInMiB), + Value: to.Ptr("not-a-number"), + }, + }, + }, + err: "NvmeDiskSizeInMiBCapabilityValueParse: failed to parse string 'not-a-number' as int64, error: 'strconv.ParseInt: parsing \"not-a-number\": invalid syntax'", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + actual, err := sku.NVMeDiskSizeInMiB() + if tc.err != "" { + if err == nil || err.Error() != tc.err { + t.Fatalf("expected error '%s' but got '%v'", tc.err, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if actual != tc.expect { + t.Fatalf("expected %d but got %d", tc.expect, actual) + } + } + }) + } +} diff --git a/example/example.go b/v2/example/example.go similarity index 100% rename from example/example.go rename to v2/example/example.go diff --git a/v2/fakes_test.go b/v2/fakes_test.go new file mode 100644 index 0000000..f3eab3c --- /dev/null +++ b/v2/fakes_test.go @@ -0,0 +1,113 @@ +package skewer + +import ( + "context" + "encoding/json" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +// dataWrapper is a convenience wrapper for deserializing json testdata +type dataWrapper struct { + Value []*armcompute.ResourceSKU `json:"value,omitempty"` +} + +// newDataWrapper takes a path to a list of compute skus and parses them +// to a dataWrapper for use in fake clients +func newDataWrapper(path string) (*dataWrapper, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + wrapper := new(dataWrapper) + if err := json.Unmarshal(data, wrapper); err != nil { + return nil, err + } + + return wrapper, nil +} + +// fakeClient is close to the simplest fake client implementation usable +// by the cache. It does not use pagination like Azure clients. +type fakeClient struct { + skus []*armcompute.ResourceSKU + err error +} + +var _ client = &fakeClient{} + +func (f *fakeClient) List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) { + if f.err != nil { + return nil, f.err + } + return f.skus, nil +} + +// fakeResourceClient is a fake client for the real Azure types. It +// returns a result iterator and can test against arbitrary sequences of +// return pages, injecting failure. +type fakeResourceClient struct { + skuLists [][]*armcompute.ResourceSKU + err error +} + +func (f *fakeResourceClient) NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] { + pageCount := 0 + pager := runtime.NewPager(runtime.PagingHandler[armcompute.ResourceSKUsClientListResponse]{ + More: func(current armcompute.ResourceSKUsClientListResponse) bool { + return pageCount < len(f.skuLists) + }, + Fetcher: func(ctx context.Context, current *armcompute.ResourceSKUsClientListResponse) (armcompute.ResourceSKUsClientListResponse, error) { + if f.err != nil { + return armcompute.ResourceSKUsClientListResponse{}, f.err + } + if pageCount >= len(f.skuLists) { + return armcompute.ResourceSKUsClientListResponse{}, nil + } + pageCount += 1 + return armcompute.ResourceSKUsClientListResponse{ + ResourceSKUsResult: armcompute.ResourceSKUsResult{ + Value: f.skuLists[pageCount-1], + }, + }, nil + }, + }) + return pager +} + +//nolint:deadcode,unused +func newFailingFakeResourceClient(reterr error) *fakeResourceClient { + return &fakeResourceClient{ + skuLists: [][]*armcompute.ResourceSKU{{}}, + err: reterr, + } +} + +// newSuccessfulFakeResourceClient takes a list of sku lists and returns +// a ResourceClient which iterates over all of them, mapping each sku +// list to a page of values. +func newSuccessfulFakeResourceClient(skuLists [][]*armcompute.ResourceSKU) *fakeResourceClient { + return &fakeResourceClient{ + skuLists: skuLists, + err: nil, + } +} + +// chunk divides a list into count pieces. +func chunk(skus []*armcompute.ResourceSKU, count int) [][]*armcompute.ResourceSKU { + divided := [][]*armcompute.ResourceSKU{} + size := (len(skus) + count - 1) / count + for i := 0; i < len(skus); i += size { + end := i + size + + if end > len(skus) { + end = len(skus) + } + + divided = append(divided, skus[i:end]) + } + return divided +} diff --git a/v2/hack/generate_vmsize_testdata.go b/v2/hack/generate_vmsize_testdata.go new file mode 100644 index 0000000..a4eecd0 --- /dev/null +++ b/v2/hack/generate_vmsize_testdata.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "fmt" + "os" + "text/template" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/skewer/v2/testdata" +) + +func getSKUs(subscriptionID, region string) (map[string]testdata.SKUInfo, error) { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, err + } + + client, err := armcompute.NewResourceSKUsClient(subscriptionID, cred, nil) + if err != nil { + return nil, err + } + + ctx := context.Background() + filter := fmt.Sprintf("location eq '%s'", region) + pager := client.NewListPager(&armcompute.ResourceSKUsClientListOptions{Filter: &filter}) + + skus := map[string]testdata.SKUInfo{} + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + for _, v := range page.Value { + if v.ResourceType != nil && *v.ResourceType == "virtualMachines" { + if _, ok := skus[*v.Name]; !ok { + skuInfo := testdata.SKUInfo{ + Size: *v.Size, + } + skus[*v.Name] = skuInfo + } + } + } + } + + return skus, nil +} + +const templateCode = `package testdata + +type SKUInfo struct { + Size string +} + +var SKUData = map[string]SKUInfo{ +{{- range $key, $value := .}} + "{{ $key }}": { + Size: "{{ $value.Size }}", + }, +{{- end }} +} +` + +func generateAndSaveFile(skus map[string]testdata.SKUInfo) error { + file, err := os.Create("../testdata/generated_vmsize_testdata.go") + if err != nil { + return err + } + defer file.Close() + + tmpl, err := template.New("skus_template").Parse(templateCode) + if err != nil { + return err + } + + err = tmpl.Execute(file, skus) + if err != nil { + return err + } + + return nil +} + +func main() { + // Get the subscription ID from the environment variable or use a default value + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + + // Get the region from the environment variable or use a default value + region := os.Getenv("AZURE_REGION") + if region == "" { + region = "eastus" // Default region if not provided in the environment variable + } + + skus, err := getSKUs(subscriptionID, region) + if err != nil { + fmt.Println("Error fetching SKUs:", err) + return + } + + err = generateAndSaveFile(skus) + if err != nil { + fmt.Println("Error generating and saving file:", err) + return + } + + fmt.Println("Generated and saved skudata/skus_generated.go successfully!") +} diff --git a/v2/interface.go b/v2/interface.go new file mode 100644 index 0000000..e57aaa9 --- /dev/null +++ b/v2/interface.go @@ -0,0 +1,20 @@ +package skewer + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +// ResourceClient is the required Azure client interface used to populate skewer's data. +type ResourceClient interface { + NewListPager(options *armcompute.ResourceSKUsClientListOptions) *runtime.Pager[armcompute.ResourceSKUsClientListResponse] +} + +var _ ResourceClient = &armcompute.ResourceSKUsClient{} + +// client defines the internal interface required by the skewer Cache. +type client interface { + List(ctx context.Context, filter, includeExtendedLocations string) ([]*armcompute.ResourceSKU, error) +} diff --git a/v2/sku.go b/v2/sku.go new file mode 100644 index 0000000..816450e --- /dev/null +++ b/v2/sku.go @@ -0,0 +1,546 @@ +package skewer + +import ( + "fmt" + "strconv" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/pkg/errors" +) + +// SKU wraps an Azure compute SKU with richer functionality +type SKU armcompute.ResourceSKU + +// ErrCapabilityNotFound will be returned when a capability could not be +// found, even without a value. +type ErrCapabilityNotFound struct { + capability string +} + +func (e *ErrCapabilityNotFound) Error() string { + return e.capability + "CapabilityNotFound" +} + +// ErrCapabilityValueNil will be returned when a capability was found by +// name but the value was nil. +type ErrCapabilityValueNil struct { + capability string +} + +func (e *ErrCapabilityValueNil) Error() string { + return e.capability + "CapabilityValueNil" +} + +// ErrCapabilityValueParse will be returned when a capability was found by +// name but there was error parsing the capability. +type ErrCapabilityValueParse struct { + capability string + value string + err error +} + +func (e *ErrCapabilityValueParse) Error() string { + return fmt.Sprintf("%sCapabilityValueParse: failed to parse string '%s' as int64, error: '%s'", e.capability, e.value, e.err) +} + +// VCPU returns the number of vCPUs this SKU supports. +func (s *SKU) VCPU() (int64, error) { + return s.GetCapabilityIntegerQuantity(VCPUs) +} + +// GPU returns the number of GPU this SKU supports. +func (s *SKU) GPU() (int64, error) { + return s.GetCapabilityIntegerQuantity(GPUs) +} + +// Memory returns the amount of memory this SKU supports. +func (s *SKU) Memory() (float64, error) { + return s.GetCapabilityFloatQuantity(MemoryGB) +} + +// MaxCachedDiskBytes returns the number of bytes available for the +// cache if it exists on this VM size. +func (s *SKU) MaxCachedDiskBytes() (int64, error) { + return s.GetCapabilityIntegerQuantity(CachedDiskBytes) +} + +// MaxResourceVolumeMB returns the number of bytes available for the +// cache if it exists on this VM size. +func (s *SKU) MaxResourceVolumeMB() (int64, error) { + return s.GetCapabilityIntegerQuantity(MaxResourceVolumeMB) +} + +// IsEncryptionAtHostSupported returns true when Encryption at Host is +// supported for the VM size. +func (s *SKU) IsEncryptionAtHostSupported() bool { + return s.HasCapability(EncryptionAtHost) +} + +// From ultra SSD documentation +// https://docs.microsoft.com/en-us/azure/virtual-machines/disks-enable-ultra-ssd +// Ultra SSD can be either supported on +// - "Single VMs" without availability zone support, or +// - On availability zones +// So provide functions to test both cases + +// IsUltraSSDAvailableWithoutAvailabilityZone returns true when a VM size has ultra SSD enabled +// in the region +func (s *SKU) IsUltraSSDAvailableWithoutAvailabilityZone() bool { + return s.HasCapability(UltraSSDAvailable) +} + +// IsUltraSSDAvailableInAvailabilityZone returns true when a VM size has ultra SSD enabled +// in the given availability zone +func (s *SKU) IsUltraSSDAvailableInAvailabilityZone(zone string) bool { + return s.HasCapabilityInZone(UltraSSDAvailable, zone) +} + +// IsUltraSSDAvailable returns true when a VM size has ultra SSD enabled +// in at least 1 unrestricted zone. +// +// Deprecated: use either IsUltraSSDAvailableWithoutAvailabilityZone or IsUltraSSDAvailableInAvailabilityZone +func (s *SKU) IsUltraSSDAvailable() bool { + return s.HasZonalCapability(UltraSSDAvailable) +} + +// IsEphemeralOSDiskSupported returns true when the VM size supports +// ephemeral OS. +func (s *SKU) IsEphemeralOSDiskSupported() bool { + return s.HasCapability(EphemeralOSDisk) +} + +// IsAcceleratedNetworkingSupported returns true when the VM size supports +// accelerated networking. +func (s *SKU) IsAcceleratedNetworkingSupported() bool { + return s.HasCapability(AcceleratedNetworking) +} + +// IsPremiumIO returns true when the VM size supports PremiumIO. +func (s *SKU) IsPremiumIO() bool { + return s.HasCapability(CapabilityPremiumIO) +} + +// IsHyperVGen1Supported returns true when the VM size supports +// accelerated networking. +func (s *SKU) IsHyperVGen1Supported() bool { + return s.HasCapabilityWithSeparator(HyperVGenerations, HyperVGeneration1) +} + +// IsHyperVGen2Supported returns true when the VM size supports +// accelerated networking. +func (s *SKU) IsHyperVGen2Supported() bool { + return s.HasCapabilityWithSeparator(HyperVGenerations, HyperVGeneration2) +} + +// GetCPUArchitectureType returns cpu arch for the VM size. +// It errors if value is nil or not found. +func (s *SKU) GetCPUArchitectureType() (string, error) { + return s.GetCapabilityString(CapabilityCPUArchitectureType) +} + +// GetCapabilityIntegerQuantity retrieves and parses the value of an +// integer numeric capability with the provided name. It errors if the +// capability is not found, the value was nil, or the value could not be +// parsed as an integer. +func (s *SKU) GetCapabilityIntegerQuantity(name string) (int64, error) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && *capability.Name == name { + if capability.Value != nil { + intVal, err := strconv.ParseInt(*capability.Value, ten, sixtyFour) + if err != nil { + return -1, &ErrCapabilityValueParse{name, *capability.Value, err} + } + return intVal, nil + } + return -1, &ErrCapabilityValueNil{name} + } + } + return -1, &ErrCapabilityNotFound{name} +} + +// GetCapabilityFloatQuantity retrieves and parses the value of a +// floating point numeric capability with the provided name. It errors +// if the capability is not found, the value was nil, or the value could +// not be parsed as an integer. +func (s *SKU) GetCapabilityFloatQuantity(name string) (float64, error) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && *capability.Name == name { + if capability.Value != nil { + intVal, err := strconv.ParseFloat(*capability.Value, sixtyFour) + if err != nil { + return -1, &ErrCapabilityValueParse{name, *capability.Value, err} + } + return intVal, nil + } + return -1, &ErrCapabilityValueNil{name} + } + } + return -1, &ErrCapabilityNotFound{name} +} + +// GetCapabilityString retrieves string capability with the provided name. +// It errors if the capability is not found or the value was nil +func (s *SKU) GetCapabilityString(name string) (string, error) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && *capability.Name == name { + if capability.Value != nil { + return *capability.Value, nil + } + return "", &ErrCapabilityValueNil{name} + } + } + return "", &ErrCapabilityNotFound{name} +} + +// HasCapability return true for a capability which can be either +// supported or not. Examples include "EphemeralOSDiskSupported", +// "EncryptionAtHostSupported", "AcceleratedNetworkingEnabled", and +// "RdmaEnabled" +func (s *SKU) HasCapability(name string) bool { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + return capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) + } + } + return false +} + +// HasZonalCapability return true for a capability which can be either +// supported or not. Examples include "UltraSSDAvailable". +// This function only checks that zone details suggest support: it will +// return true for a whole location even when only one zone supports the +// feature. Currently, the only real scenario that appears to use +// zoneDetails is UltraSSDAvailable which always lists all regions as +// available. +// For per zone capability check, use "HasCapabilityInZone" +func (s *SKU) HasZonalCapability(name string) bool { + for _, locationInfo := range s.LocationInfo { + if locationInfo == nil { + continue + } + for _, zoneDetails := range locationInfo.ZoneDetails { + if zoneDetails == nil { + continue + } + for _, capability := range zoneDetails.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + if capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) { + return true + } + } + } + } + } + return false +} + +// HasCapabilityInZone return true if the specified capability name is supported in the +// specified zone. +func (s *SKU) HasCapabilityInZone(name, zone string) bool { + for _, locationInfo := range s.LocationInfo { + if locationInfo == nil { + continue + } + for _, zoneDetails := range locationInfo.ZoneDetails { + if zoneDetails == nil { + continue + } + foundZone := false + for _, zoneName := range zoneDetails.Name { + if zoneName != nil && strings.EqualFold(zone, *zoneName) { + foundZone = true + break + } + } + if !foundZone { + continue + } + + for _, capability := range zoneDetails.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + if capability.Value != nil && strings.EqualFold(*capability.Value, string(CapabilitySupported)) { + return true + } + } + } + } + } + return false +} + +// HasCapabilityWithSeparator return true for a capability which may be +// exposed as a comma-separated list. We check that the list contains +// the desired substring. An example is "HyperVGenerations" which may be +// "V1,V2" +func (s *SKU) HasCapabilityWithSeparator(name, value string) bool { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + return capability.Value != nil && strings.Contains(normalizeLocation(*capability.Value), normalizeLocation(value)) + } + } + return false +} + +// HasCapabilityWithMinCapacity returns true when the SKU has a +// capability with the requested name, and the value is greater than or +// equal to the desired value. +// "MaxResourceVolumeMB", "OSVhdSizeMB", "vCPUs", +// "MemoryGB","MaxDataDiskCount", "CombinedTempDiskAndCachedIOPS", +// "CombinedTempDiskAndCachedReadBytesPerSecond", +// "CombinedTempDiskAndCachedWriteBytesPerSecond", "UncachedDiskIOPS", +// and "UncachedDiskBytesPerSecond" +func (s *SKU) HasCapabilityWithMinCapacity(name string, value int64) (bool, error) { + for _, capability := range s.Capabilities { + if capability != nil && capability.Name != nil && strings.EqualFold(*capability.Name, name) { + if capability.Value != nil { + intVal, err := strconv.ParseInt(*capability.Value, ten, sixtyFour) + if err != nil { + return false, errors.Wrapf(err, "failed to parse string '%s' as int64", *capability.Value) + } + if intVal >= value { + return true, nil + } + } + return false, nil + } + } + return false, nil +} + +// IsAvailable returns true when the requested location matches one on +// the sku, and there are no total restrictions on the location. +func (s *SKU) IsAvailable(location string) bool { + for _, locationInfo := range s.LocationInfo { + if locationInfo != nil && locationInfo.Location != nil { + if locationEquals(*locationInfo.Location, location) { + for _, restriction := range s.Restrictions { + // Can't deploy to any zones in this location. We're done. + if restriction != nil && restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { + return false + } + } + return true + } + } + } + return false +} + +// IsRestricted returns true when a location restriction exists for +// this SKU. +func (s *SKU) IsRestricted(location string) bool { + for _, restriction := range s.Restrictions { + if restriction == nil || restriction.Values == nil { + continue + } + for _, candidate := range restriction.Values { + // Can't deploy in this location. We're done. + if candidate != nil && locationEquals(*candidate, location) && restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { + return true + } + } + } + return false +} + +// IsResourceType returns true when the wrapped SKU has the provided +// value as its resource type. This may be used to filter using values +// such as "virtualMachines", "disks", "availabilitySets", "snapshots", +// and "hostGroups/hosts". +func (s *SKU) IsResourceType(t string) bool { + return s.ResourceType != nil && strings.EqualFold(*s.ResourceType, t) +} + +// GetResourceType returns the name of this resource sku. It normalizes pointers +// to the empty string for comparison purposes. For example, +// "virtualMachines" for a virtual machine. +func (s *SKU) GetResourceType() string { + if s.ResourceType == nil { + return "" + } + return *s.ResourceType +} + +// GetName returns the name of this resource sku. It normalizes pointers +// to the empty string for comparison purposes. For example, +// "Standard_D8s_v3" for a virtual machine. +func (s *SKU) GetName() string { + if s.Name == nil { + return "" + } + + return *s.Name +} + +// GetFamilyName returns the family name of this resource sku. It normalizes pointers +// to the empty string for comparison purposes. For example, +// "standardDSv2Family" for a virtual machine. +func (s *SKU) GetFamilyName() string { + if s.Family == nil { + return "" + } + + return *s.Family +} + +// GetSize returns the size of this resource sku. It normalizes pointers +// to the empty string for comparison purposes. For example, +// "M416ms_v2" for a virtual machine. +func (s *SKU) GetSize() string { + if s.Size == nil { + return "" + } + + return *s.Size +} + +func (s *SKU) GetVMSize() (*VMSizeType, error) { + return GetVMSize(s.GetSize()) +} + +// GetLocation returns the location for a given SKU. +func (s *SKU) GetLocation() (string, error) { + if s.Locations == nil { + return "", fmt.Errorf("sku had nil location array") + } + + if len(s.Locations) < 1 { + return "", fmt.Errorf("sku had no locations") + } + + if len(s.Locations) > 1 { + return "", fmt.Errorf("sku had multiple locations, refusing to disambiguate") + } + + if s.Locations[0] == nil { + return "", fmt.Errorf("sku had nil location") + } + + return *s.Locations[0], nil +} + +// HasLocation returns true if the given sku exposes this region for deployment. +func (s *SKU) HasLocation(location string) bool { + for _, candidate := range s.Locations { + if candidate != nil && locationEquals(*candidate, location) { + return true + } + } + + return false +} + +// HasLocationRestriction returns true if the location is restricted for +// this sku. +func (s *SKU) HasLocationRestriction(location string) bool { + for _, restriction := range s.Restrictions { + if restriction.Type != nil && *restriction.Type != armcompute.ResourceSKURestrictionsTypeLocation { + continue + } + if restriction.Values == nil { + continue + } + for _, candidate := range restriction.Values { + if candidate != nil && locationEquals(*candidate, location) { + return true + } + } + } + + return false +} + +// IsConfidentialComputingTypeSNP return true if ConfidentialComputingType is SNP for this sku. +func (s *SKU) IsConfidentialComputingTypeSNP() (bool, error) { + return s.HasCapabilityWithSeparator(CapabilityConfidentialComputingType, ConfidentialComputingTypeSNP), nil +} + +// Official documentation for Trusted Launch states: +// The response will be similar to the following form: +// IsTrustedLaunchEnabled True in the output indicates that the Generation 2 VM size does not support Trusted launch. +// If it's a Generation 2 VM size and TrustedLaunchDisabled is not part of the output, +// it implies that Trusted launch is supported for that VM size. +func (s *SKU) IsTrustedLaunchEnabled() (bool, error) { + if s.IsHyperVGen2Supported() { + if !s.HasCapabilityWithSeparator(CapabilityTrustedLaunchDisabled, string(CapabilitySupported)) { + return true, nil + } + } + return false, nil +} + +// AvailabilityZones returns the list of Availability Zones which have this resource SKU available and unrestricted. +func (s *SKU) AvailabilityZones(location string) map[string]bool { //nolint:gocyclo + if s.LocationInfo == nil { + return nil + } + + // Use map for easy deletion and iteration + availableZones := make(map[string]bool) + restrictedZones := make(map[string]bool) + + for _, locationInfo := range s.LocationInfo { + if locationInfo == nil || locationInfo.Location == nil { + continue + } + if locationEquals(*locationInfo.Location, location) { + // add all zones + for _, zone := range locationInfo.Zones { + if zone != nil { + availableZones[*zone] = true + } + } + + // iterate restrictions, remove any restricted zones for this location + for _, restriction := range s.Restrictions { + if restriction != nil { + for _, candidate := range restriction.Values { + if candidate != nil && locationEquals(*candidate, location) { + if restriction.Type != nil && *restriction.Type == armcompute.ResourceSKURestrictionsTypeLocation { + // Can't deploy in this location. We're done. + return nil + } + + if restriction.RestrictionInfo != nil { + // remove restricted zones + for _, zone := range restriction.RestrictionInfo.Zones { + if zone != nil { + restrictedZones[*zone] = true + } + } + } + } + } + } + } + } + } + + for zone := range restrictedZones { + delete(availableZones, zone) + } + + return availableZones +} + +// Equal returns true when two skus have the same location, type, and name. +func (s *SKU) Equal(other *SKU) bool { + location, localErr := s.GetLocation() + otherLocation, otherErr := other.GetLocation() + return strings.EqualFold(s.GetResourceType(), other.GetResourceType()) && + strings.EqualFold(s.GetName(), other.GetName()) && + locationEquals(location, otherLocation) && + localErr != nil && + otherErr != nil +} + +// MemberOf returns true if the SKU's name is in the list of SKUs. +func (s *SKU) MemberOf(skuList []SKU) bool { + for _, sku := range skuList { + if s.GetName() == sku.GetName() { + return true + } + } + return false +} diff --git a/v2/sku_test.go b/v2/sku_test.go new file mode 100644 index 0000000..d28a00f --- /dev/null +++ b/v2/sku_test.go @@ -0,0 +1,568 @@ +package skewer + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/google/go-cmp/cmp" +) + +func Test_SKU_GetCapabilityQuantity(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + capability string + expect int64 + err string + }{ + "empty capability list should return capability not found": { + sku: armcompute.ResourceSKU{}, + capability: "", + err: (&ErrCapabilityNotFound{""}).Error(), + }, + "empty capability should not match sku with empty list of capabilities": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, + }, + capability: "", + err: (&ErrCapabilityNotFound{""}).Error(), + }, + "empty capability should fail to parse when not integer": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(""), + Value: to.Ptr("False"), + }, + }, + }, + capability: "", + err: "CapabilityValueParse: failed to parse string 'False' as int64, error: 'strconv.ParseInt: parsing \"False\": invalid syntax'", //nolint:lll + }, + "foo capability should return successfully with integer": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("100"), + }, + }, + }, + capability: "foo", + expect: 100, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + quantity, err := sku.GetCapabilityIntegerQuantity(tc.capability) + if tc.err != "" { + if err == nil { + t.Errorf("expected failure with error '%s' but did not occur", tc.err) + } + if diff := cmp.Diff(tc.err, err.Error()); diff != "" { + t.Error(diff) + } + } else { + if err != nil { + t.Errorf("expected success but failure occurred with error '%s'", err) + } + if diff := cmp.Diff(tc.expect, quantity); diff != "" { + t.Error(diff) + } + } + }) + } +} + +func Test_SKU_HasCapability(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + capability string + expect bool + }{ + "empty capability should not match empty sku": { + sku: armcompute.ResourceSKU{}, + capability: "", + }, + "empty capability should not match sku with empty list of capabilities": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, + }, + capability: "", + }, + "empty capability should not match when present and false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(""), + Value: to.Ptr("False"), + }, + }, + }, + capability: "", + }, + "empty capability should not match when present and weird value": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(""), + Value: to.Ptr("foobar"), + }, + }, + }, + capability: "", + }, + "foo capability should not match when false": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("False"), + }, + }, + }, + capability: "foo", + }, + "foo capability should match when true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("True"), + }, + }, + }, + capability: "foo", + expect: true, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + if diff := cmp.Diff(tc.expect, sku.HasCapability(tc.capability)); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_SKU_HasCapabilityWithMinCapacity(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + capability string + capacity int64 + expect bool + err error + }{ + "empty capability should not match empty sku": { + sku: armcompute.ResourceSKU{}, + capability: "", + }, + "empty capability should not match sku with empty list of capabilities": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{}, + }, + capability: "", + }, + "empty capability should error when present and weird value": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(""), + Value: to.Ptr("foobar"), + }, + }, + }, + capability: "", + err: fmt.Errorf("failed to parse string 'foobar' as int64: strconv.ParseInt: parsing \"foobar\": invalid syntax"), + }, + "empty capability should match when present with zero capacity and requesting zero": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr(""), + Value: to.Ptr("0"), + }, + }, + }, + capability: "", + expect: true, + }, + "foo capability should not match when present and less than capacity": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("100"), + }, + }, + }, + capability: "foo", + capacity: 200, + }, + "foo capability should match when true": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("10"), + }, + }, + }, + capability: "foo", + capacity: 5, + expect: true, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + got, err := sku.HasCapabilityWithMinCapacity(tc.capability, tc.capacity) + if tc.err != nil { + if diff := cmp.Diff(tc.err.Error(), err.Error()); diff != "" { + t.Error(diff) + } + } else { + if diff := cmp.Diff(tc.expect, got); diff != "" { + t.Error(diff) + } + } + }) + } +} + +func Test_SKU_GetResourceTypeAndName(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + expectName string + expectResourceType string + }{ + "nil resourceType should return empty string": { + sku: armcompute.ResourceSKU{}, + expectResourceType: "", + expectName: "", + }, + "empty resourceType should return empty string": { + sku: armcompute.ResourceSKU{ + Name: to.Ptr(""), + ResourceType: to.Ptr(""), + }, + expectResourceType: "", + expectName: "", + }, + "populated resourceType should return correctly": { + sku: armcompute.ResourceSKU{ + Name: to.Ptr("foo"), + ResourceType: to.Ptr("foo"), + }, + expectResourceType: "foo", + expectName: "foo", + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + if diff := cmp.Diff(tc.expectName, sku.GetName()); diff != "" { + t.Errorf("mismatched name\n%s", diff) + } + if diff := cmp.Diff(tc.expectResourceType, sku.GetResourceType()); diff != "" { + t.Errorf("mismatched resourceType\n%s", diff) + } + }) + } +} + +func Test_SKU_IsResourceType(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + resourceType string + expect bool + }{ + "nil resourceType should not match anything": { + sku: armcompute.ResourceSKU{}, + resourceType: "", + }, + "empty resourceType should match empty string": { + sku: armcompute.ResourceSKU{ + ResourceType: to.Ptr(""), + }, + resourceType: "", + expect: true, + }, + "empty resourceType should not match non-empty string": { + sku: armcompute.ResourceSKU{ + ResourceType: to.Ptr(""), + }, + resourceType: "foo", + }, + "populated resourceType should match itself": { + sku: armcompute.ResourceSKU{ + ResourceType: to.Ptr("foo"), + }, + resourceType: "foo", + expect: true, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + if diff := cmp.Diff(tc.expect, sku.IsResourceType(tc.resourceType)); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_SKU_GetLocation(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + expect string + expectErr string + }{ + "nil locations should return empty string": { + sku: armcompute.ResourceSKU{}, + expect: "", + }, + "empty array of locations return empty string": { + sku: armcompute.ResourceSKU{ + Locations: []*string{}, + }, + expect: "", + }, + "single empty value should return empty string": { + sku: armcompute.ResourceSKU{ + Locations: []*string{ + to.Ptr(""), + }, + }, + expect: "", + }, + "populated location should return correctly": { + sku: armcompute.ResourceSKU{ + Locations: []*string{ + to.Ptr("foo"), + }, + }, + expect: "foo", + }, + "should return error with multiple choices": { + sku: armcompute.ResourceSKU{ + Locations: []*string{ + to.Ptr("bar"), + to.Ptr("foo"), + }, + }, + expectErr: "sku had multiple locations, refusing to disambiguate", + }, + "should return error with no choices": { + sku: armcompute.ResourceSKU{ + Locations: []*string{}, + }, + expectErr: "sku had no locations", + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + got, err := sku.GetLocation() + if tc.expectErr != "" { + if err == nil { + t.Errorf("expected error '%s', but got none", tc.expectErr) + } + if err.Error() != tc.expectErr { + t.Errorf("expected error '%s', but got '%s'", tc.expectErr, err.Error()) + } + } + if diff := cmp.Diff(tc.expect, got); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_SKU_AvailabilityZones(t *testing.T) {} + +//nolint:funlen +func Test_SKU_HasCapabilityInZone(t *testing.T) { + cases := map[string]struct { + sku armcompute.ResourceSKU + capability string + zone string + expect bool + }{ + "should return false when capability is false": { + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + { + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("False"), + }, + }, + }, + }, + }, + }, + }, + capability: "foo", + zone: "1", + expect: false, + }, + "should return false when zone doesn't match": { + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + { + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("True"), + }, + }, + }, + }, + }, + }, + }, + capability: "foo", + zone: "2", + expect: false, + }, + "should not return true when the capability is not set in availability zone but set on sku capability": { + sku: armcompute.ResourceSKU{ + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("True"), + }, + }, + }, + capability: "foo", + zone: "1", + expect: false, + }, + "should return true when capability and zone match": { + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + { + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("True"), + }, + }, + }, + }, + }, + }, + }, + capability: "foo", + zone: "1", + expect: true, + }, + "should return true when capability and zone match for zone 3": { + sku: armcompute.ResourceSKU{ + LocationInfo: []*armcompute.ResourceSKULocationInfo{ + { + ZoneDetails: []*armcompute.ResourceSKUZoneDetails{ + { + Name: []*string{to.Ptr("1"), to.Ptr("3")}, + Capabilities: []*armcompute.ResourceSKUCapabilities{ + { + Name: to.Ptr("foo"), + Value: to.Ptr("True"), + }, + }, + }, + }, + }, + }, + }, + capability: "foo", + zone: "3", + expect: true, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + sku := SKU(tc.sku) + if diff := cmp.Diff(tc.expect, sku.HasCapabilityInZone(tc.capability, tc.zone)); diff != "" { + t.Error(diff) + } + }) + } +} + +// Test_SKU_MemberOf tests the SKU MemberOf method +func Test_SKU_Includes(t *testing.T) { + cases := map[string]struct { + skuList []SKU + sku SKU + expect bool + }{ + "empty list should not include": { + skuList: []SKU{}, + sku: SKU{ + Name: to.Ptr("foo"), + }, + expect: false, + }, + "missing name should not include": { + skuList: []SKU{ + { + Name: to.Ptr("foo"), + }, + }, + sku: SKU{ + Name: to.Ptr("bar"), + }, + expect: false, + }, + "name is included": { + skuList: []SKU{ + { + Name: to.Ptr("foo"), + }, + { + Name: to.Ptr("bar"), + }, + }, + sku: SKU{ + Name: to.Ptr("bar"), + }, + expect: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + if diff := cmp.Diff(tc.expect, tc.sku.MemberOf(tc.skuList)); diff != "" { + t.Error(diff) + } + }) + } +} diff --git a/v2/strings.go b/v2/strings.go new file mode 100644 index 0000000..133a568 --- /dev/null +++ b/v2/strings.go @@ -0,0 +1,20 @@ +package skewer + +import ( + "strings" + "unicode" +) + +func normalizeLocation(input string) string { + var output string + for _, c := range input { + if !unicode.IsSpace(c) { + output += string(c) + } + } + return strings.ToLower(output) +} + +func locationEquals(a, b string) bool { + return normalizeLocation(a) == normalizeLocation(b) +} diff --git a/v2/testdata/eastus.json b/v2/testdata/eastus.json new file mode 100644 index 0000000..c666294 --- /dev/null +++ b/v2/testdata/eastus.json @@ -0,0 +1,534 @@ +{ + "value":[ + { + "resourceType": "virtualMachines", + "name": "Standard_D2_v2", + "tier": "Standard", + "size": "D2_v2", + "family": "standardDv2Family", + "kind": null, + "capacity": null, + "locations": [ + "eastus" + ], + "locationInfo": [ + { + "location": "eastus", + "zoneDetails": [], + "zones": [ + "2", + "3", + "1" + ] + } + ], + "apiVersions": null, + "costs": null, + "capabilities": [ + { + "name": "MaxResourceVolumeMB", + "value": "102400" + }, + { + "name": "OSVhdSizeMB", + "value": "1047552" + }, + { + "name": "vCPUs", + "value": "2" + }, + { + "name": "MemoryPreservingMaintenanceSupported", + "value": "True" + }, + { + "name": "HyperVGenerations", + "value": "V1" + }, + { + "name": "MemoryGB", + "value": "7" + }, + { + "name": "MaxDataDiskCount", + "value": "8" + }, + { + "name": "CpuArchitectureType", + "value": "x64" + }, + { + "name": "LowPriorityCapable", + "value": "True" + }, + { + "name": "PremiumIO", + "value": "False" + }, + { + "name": "VMDeploymentTypes", + "value": "IaaS,PaaS" + }, + { + "name": "vCPUsAvailable", + "value": "2" + }, + { + "name": "ACUs", + "value": "210" + }, + { + "name": "vCPUsPerCore", + "value": "1" + }, + { + "name": "CombinedTempDiskAndCachedIOPS", + "value": "6000" + }, + { + "name": "CombinedTempDiskAndCachedReadBytesPerSecond", + "value": "97517568" + }, + { + "name": "CombinedTempDiskAndCachedWriteBytesPerSecond", + "value": "48234496" + }, + { + "name": "EphemeralOSDiskSupported", + "value": "False" + }, + { + "name": "EncryptionAtHostSupported", + "value": "False" + }, + { + "name": "CapacityReservationSupported", + "value": "False" + }, + { + "name": "AcceleratedNetworkingEnabled", + "value": "True" + }, + { + "name": "RdmaEnabled", + "value": "False" + }, + { + "name": "MaxNetworkInterfaces", + "value": "2" + } + ], + "restrictions": [] + }, + { + "resourceType": "virtualMachines", + "name": "Standard_D13_v2_Promo", + "tier": "Standard", + "size": "D13_v2_Promo", + "family": "standardDv2PromoFamily", + "locations": [ + "eastus" + ], + "locationInfo": [ + { + "location": "eastus", + "zones": [ + "3", + "2", + "1" + ], + "zoneDetails": [] + } + ], + "capabilities": [ + { + "name": "MaxResourceVolumeMB", + "value": "409600" + }, + { + "name": "OSVhdSizeMB", + "value": "1047552" + }, + { + "name": "vCPUs", + "value": "8" + }, + { + "name": "HyperVGenerations", + "value": "V1" + }, + { + "name": "MemoryGB", + "value": "56" + }, + { + "name": "MaxDataDiskCount", + "value": "32" + }, + { + "name": "LowPriorityCapable", + "value": "False" + }, + { + "name": "PremiumIO", + "value": "False" + }, + { + "name": "VMDeploymentTypes", + "value": "IaaS" + }, + { + "name": "vCPUsAvailable", + "value": "8" + }, + { + "name": "ACUs", + "value": "210" + }, + { + "name": "vCPUsPerCore", + "value": "1" + }, + { + "name": "CombinedTempDiskAndCachedIOPS", + "value": "24000" + }, + { + "name": "CombinedTempDiskAndCachedReadBytesPerSecond", + "value": "393216000" + }, + { + "name": "CombinedTempDiskAndCachedWriteBytesPerSecond", + "value": "196083712" + }, + { + "name": "EphemeralOSDiskSupported", + "value": "False" + }, + { + "name": "EncryptionAtHostSupported", + "value": "False" + }, + { + "name": "AcceleratedNetworkingEnabled", + "value": "True" + }, + { + "name": "RdmaEnabled", + "value": "False" + }, + { + "name": "MaxNetworkInterfaces", + "value": "8" + } + ], + "restrictions": [ + { + "type": "Location", + "values": [ + "eastus" + ], + "restrictionInfo": { + "locations": [ + "eastus" + ] + }, + "reasonCode": "NotAvailableForSubscription" + }, + { + "type": "Zone", + "values": [ + "eastus" + ], + "restrictionInfo": { + "locations": [ + "eastus" + ], + "zones": [ + "1", + "2", + "3" + ] + }, + "reasonCode": "NotAvailableForSubscription" + } + ] + }, + { + "resourceType": "virtualMachines", + "name": "Standard_D4s_v3", + "tier": "Standard", + "size": "D4s_v3", + "family": "standardDSv3Family", + "kind": null, + "capacity": null, + "locations": [ + "eastus" + ], + "locationInfo": [ + { + "location": "eastus", + "zoneDetails": [ + { + "Name": [ + "2", + "3", + "1" + ], + "capabilities": [ + { + "name": "UltraSSDAvailable", + "value": "True" + } + ], + "name": null + } + ], + "zones": [ + "2", + "3", + "1" + ] + } + ], + "apiVersions": null, + "costs": null, + "capabilities": [ + { + "name": "MaxResourceVolumeMB", + "value": "32768" + }, + { + "name": "OSVhdSizeMB", + "value": "1047552" + }, + { + "name": "vCPUs", + "value": "4" + }, + { + "name": "MemoryPreservingMaintenanceSupported", + "value": "True" + }, + { + "name": "HyperVGenerations", + "value": "V1,V2" + }, + { + "name": "MemoryGB", + "value": "16" + }, + { + "name": "MaxDataDiskCount", + "value": "8" + }, + { + "name": "CpuArchitectureType", + "value": "x64" + }, + { + "name": "LowPriorityCapable", + "value": "True" + }, + { + "name": "PremiumIO", + "value": "True" + }, + { + "name": "VMDeploymentTypes", + "value": "IaaS" + }, + { + "name": "vCPUsAvailable", + "value": "4" + }, + { + "name": "ACUs", + "value": "160" + }, + { + "name": "vCPUsPerCore", + "value": "2" + }, + { + "name": "CombinedTempDiskAndCachedIOPS", + "value": "8000" + }, + { + "name": "CombinedTempDiskAndCachedReadBytesPerSecond", + "value": "67108864" + }, + { + "name": "CombinedTempDiskAndCachedWriteBytesPerSecond", + "value": "67108864" + }, + { + "name": "CachedDiskBytes", + "value": "107374182400" + }, + { + "name": "UncachedDiskIOPS", + "value": "6400" + }, + { + "name": "UncachedDiskBytesPerSecond", + "value": "100663296" + }, + { + "name": "EphemeralOSDiskSupported", + "value": "True" + }, + { + "name": "EncryptionAtHostSupported", + "value": "True" + }, + { + "name": "CapacityReservationSupported", + "value": "False" + }, + { + "name": "AcceleratedNetworkingEnabled", + "value": "True" + }, + { + "name": "RdmaEnabled", + "value": "False" + }, + { + "name": "MaxNetworkInterfaces", + "value": "2" + } + ], + "restrictions": [] + }, + { + "resourceType": "virtualMachines", + "name": "Standard_NV6", + "tier": "Standard", + "size": "NV6", + "family": "standardNVFamily", + "kind": null, + "capacity": null, + "locations": [ + "eastus" + ], + "locationInfo": [ + { + "location": "eastus", + "zoneDetails": [], + "zones": [ + "2", + "3" + ] + } + ], + "apiVersions": null, + "costs": null, + "capabilities": [ + { + "name": "MaxResourceVolumeMB", + "value": "389120" + }, + { + "name": "OSVhdSizeMB", + "value": "1047552" + }, + { + "name": "vCPUs", + "value": "6" + }, + { + "name": "MemoryPreservingMaintenanceSupported", + "value": "False" + }, + { + "name": "HyperVGenerations", + "value": "V1" + }, + { + "name": "MemoryGB", + "value": "56" + }, + { + "name": "MaxDataDiskCount", + "value": "24" + }, + { + "name": "CpuArchitectureType", + "value": "x64" + }, + { + "name": "LowPriorityCapable", + "value": "True" + }, + { + "name": "PremiumIO", + "value": "False" + }, + { + "name": "VMDeploymentTypes", + "value": "IaaS" + }, + { + "name": "vCPUsAvailable", + "value": "6" + }, + { + "name": "GPUs", + "value": "1" + }, + { + "name": "vCPUsPerCore", + "value": "1" + }, + { + "name": "RetirementDateUtc", + "value": "08/31/2023" + }, + { + "name": "EphemeralOSDiskSupported", + "value": "False" + }, + { + "name": "EncryptionAtHostSupported", + "value": "False" + }, + { + "name": "CapacityReservationSupported", + "value": "False" + }, + { + "name": "AcceleratedNetworkingEnabled", + "value": "False" + }, + { + "name": "RdmaEnabled", + "value": "False" + }, + { + "name": "MaxNetworkInterfaces", + "value": "2" + } + ], + "restrictions": [ + { + "reasonCode": "NotAvailableForSubscription", + "restrictionInfo": { + "locations": [ + "eastus" + ], + "zones": [ + "1", + "2", + "3" + ] + }, + "type": "Zone", + "values": [ + "eastus" + ] + } + ] + } + ] + } \ No newline at end of file diff --git a/v2/testdata/generated_vmsize_testdata.go b/v2/testdata/generated_vmsize_testdata.go new file mode 100644 index 0000000..87ba50d --- /dev/null +++ b/v2/testdata/generated_vmsize_testdata.go @@ -0,0 +1,2426 @@ +package testdata + +type SKUInfo struct { + Size string +} + +var SKUData = map[string]SKUInfo{ + "Basic_A0": { + Size: "A0", + }, + "Basic_A1": { + Size: "A1", + }, + "Basic_A2": { + Size: "A2", + }, + "Basic_A3": { + Size: "A3", + }, + "Basic_A4": { + Size: "A4", + }, + "Standard_A0": { + Size: "A0", + }, + "Standard_A1": { + Size: "A1", + }, + "Standard_A1_v2": { + Size: "A1_v2", + }, + "Standard_A2": { + Size: "A2", + }, + "Standard_A2_v2": { + Size: "A2_v2", + }, + "Standard_A2m_v2": { + Size: "A2m_v2", + }, + "Standard_A3": { + Size: "A3", + }, + "Standard_A4": { + Size: "A4", + }, + "Standard_A4_v2": { + Size: "A4_v2", + }, + "Standard_A4m_v2": { + Size: "A4m_v2", + }, + "Standard_A5": { + Size: "A5", + }, + "Standard_A6": { + Size: "A6", + }, + "Standard_A7": { + Size: "A7", + }, + "Standard_A8_v2": { + Size: "A8_v2", + }, + "Standard_A8m_v2": { + Size: "A8m_v2", + }, + "Standard_B12ms": { + Size: "B12ms", + }, + "Standard_B16als_v2": { + Size: "B16als_v2", + }, + "Standard_B16as_v2": { + Size: "B16as_v2", + }, + "Standard_B16ls_v2": { + Size: "B16ls_v2", + }, + "Standard_B16ms": { + Size: "B16ms", + }, + "Standard_B16pls_v2": { + Size: "B16pls_v2", + }, + "Standard_B16ps_v2": { + Size: "B16ps_v2", + }, + "Standard_B16s_v2": { + Size: "B16s_v2", + }, + "Standard_B1ls": { + Size: "B1ls", + }, + "Standard_B1ms": { + Size: "B1ms", + }, + "Standard_B1s": { + Size: "B1s", + }, + "Standard_B20ms": { + Size: "B20ms", + }, + "Standard_B2als_v2": { + Size: "B2als_v2", + }, + "Standard_B2as_v2": { + Size: "B2as_v2", + }, + "Standard_B2ats_v2": { + Size: "B2ats_v2", + }, + "Standard_B2ls_v2": { + Size: "B2ls_v2", + }, + "Standard_B2ms": { + Size: "B2ms", + }, + "Standard_B2pls_v2": { + Size: "B2pls_v2", + }, + "Standard_B2ps_v2": { + Size: "B2ps_v2", + }, + "Standard_B2pts_v2": { + Size: "B2pts_v2", + }, + "Standard_B2s": { + Size: "B2s", + }, + "Standard_B2s_v2": { + Size: "B2s_v2", + }, + "Standard_B2ts_v2": { + Size: "B2ts_v2", + }, + "Standard_B32als_v2": { + Size: "B32als_v2", + }, + "Standard_B32as_v2": { + Size: "B32as_v2", + }, + "Standard_B32ls_v2": { + Size: "B32ls_v2", + }, + "Standard_B32s_v2": { + Size: "B32s_v2", + }, + "Standard_B4als_v2": { + Size: "B4als_v2", + }, + "Standard_B4as_v2": { + Size: "B4as_v2", + }, + "Standard_B4ls_v2": { + Size: "B4ls_v2", + }, + "Standard_B4ms": { + Size: "B4ms", + }, + "Standard_B4pls_v2": { + Size: "B4pls_v2", + }, + "Standard_B4ps_v2": { + Size: "B4ps_v2", + }, + "Standard_B4s_v2": { + Size: "B4s_v2", + }, + "Standard_B8als_v2": { + Size: "B8als_v2", + }, + "Standard_B8as_v2": { + Size: "B8as_v2", + }, + "Standard_B8ls_v2": { + Size: "B8ls_v2", + }, + "Standard_B8ms": { + Size: "B8ms", + }, + "Standard_B8pls_v2": { + Size: "B8pls_v2", + }, + "Standard_B8ps_v2": { + Size: "B8ps_v2", + }, + "Standard_B8s_v2": { + Size: "B8s_v2", + }, + "Standard_D1": { + Size: "D1", + }, + "Standard_D11": { + Size: "D11", + }, + "Standard_D11_v2": { + Size: "D11_v2", + }, + "Standard_D11_v2_Promo": { + Size: "D11_v2_Promo", + }, + "Standard_D12": { + Size: "D12", + }, + "Standard_D12_v2": { + Size: "D12_v2", + }, + "Standard_D12_v2_Promo": { + Size: "D12_v2_Promo", + }, + "Standard_D13": { + Size: "D13", + }, + "Standard_D13_v2": { + Size: "D13_v2", + }, + "Standard_D13_v2_Promo": { + Size: "D13_v2_Promo", + }, + "Standard_D14": { + Size: "D14", + }, + "Standard_D14_v2": { + Size: "D14_v2", + }, + "Standard_D14_v2_Promo": { + Size: "D14_v2_Promo", + }, + "Standard_D15_v2": { + Size: "D15_v2", + }, + "Standard_D16_v3": { + Size: "D16_v3", + }, + "Standard_D16_v4": { + Size: "D16_v4", + }, + "Standard_D16_v5": { + Size: "D16_v5", + }, + "Standard_D16a_v3": { + Size: "D16a_v3", + }, + "Standard_D16a_v4": { + Size: "D16a_v4", + }, + "Standard_D16ads_v5": { + Size: "D16ads_v5", + }, + "Standard_D16as_v3": { + Size: "D16as_v3", + }, + "Standard_D16as_v4": { + Size: "D16as_v4", + }, + "Standard_D16as_v5": { + Size: "D16as_v5", + }, + "Standard_D16d_v4": { + Size: "D16d_v4", + }, + "Standard_D16d_v5": { + Size: "D16d_v5", + }, + "Standard_D16ds_v4": { + Size: "D16ds_v4", + }, + "Standard_D16ds_v5": { + Size: "D16ds_v5", + }, + "Standard_D16lds_v5": { + Size: "D16lds_v5", + }, + "Standard_D16ls_v5": { + Size: "D16ls_v5", + }, + "Standard_D16pds_v5": { + Size: "D16pds_v5", + }, + "Standard_D16plds_v5": { + Size: "D16plds_v5", + }, + "Standard_D16pls_v5": { + Size: "D16pls_v5", + }, + "Standard_D16ps_v5": { + Size: "D16ps_v5", + }, + "Standard_D16s_v3": { + Size: "D16s_v3", + }, + "Standard_D16s_v4": { + Size: "D16s_v4", + }, + "Standard_D16s_v5": { + Size: "D16s_v5", + }, + "Standard_D1_v2": { + Size: "D1_v2", + }, + "Standard_D2": { + Size: "D2", + }, + "Standard_D2_v2": { + Size: "D2_v2", + }, + "Standard_D2_v2_Promo": { + Size: "D2_v2_Promo", + }, + "Standard_D2_v3": { + Size: "D2_v3", + }, + "Standard_D2_v4": { + Size: "D2_v4", + }, + "Standard_D2_v5": { + Size: "D2_v5", + }, + "Standard_D2a_v3": { + Size: "D2a_v3", + }, + "Standard_D2a_v4": { + Size: "D2a_v4", + }, + "Standard_D2ads_v5": { + Size: "D2ads_v5", + }, + "Standard_D2as_v3": { + Size: "D2as_v3", + }, + "Standard_D2as_v4": { + Size: "D2as_v4", + }, + "Standard_D2as_v5": { + Size: "D2as_v5", + }, + "Standard_D2d_v4": { + Size: "D2d_v4", + }, + "Standard_D2d_v5": { + Size: "D2d_v5", + }, + "Standard_D2ds_v4": { + Size: "D2ds_v4", + }, + "Standard_D2ds_v5": { + Size: "D2ds_v5", + }, + "Standard_D2lds_v5": { + Size: "D2lds_v5", + }, + "Standard_D2ls_v5": { + Size: "D2ls_v5", + }, + "Standard_D2pds_v5": { + Size: "D2pds_v5", + }, + "Standard_D2plds_v5": { + Size: "D2plds_v5", + }, + "Standard_D2pls_v5": { + Size: "D2pls_v5", + }, + "Standard_D2ps_v5": { + Size: "D2ps_v5", + }, + "Standard_D2s_v3": { + Size: "D2s_v3", + }, + "Standard_D2s_v4": { + Size: "D2s_v4", + }, + "Standard_D2s_v5": { + Size: "D2s_v5", + }, + "Standard_D3": { + Size: "D3", + }, + "Standard_D32_v3": { + Size: "D32_v3", + }, + "Standard_D32_v4": { + Size: "D32_v4", + }, + "Standard_D32_v5": { + Size: "D32_v5", + }, + "Standard_D32a_v3": { + Size: "D32a_v3", + }, + "Standard_D32a_v4": { + Size: "D32a_v4", + }, + "Standard_D32ads_v5": { + Size: "D32ads_v5", + }, + "Standard_D32as_v3": { + Size: "D32as_v3", + }, + "Standard_D32as_v4": { + Size: "D32as_v4", + }, + "Standard_D32as_v5": { + Size: "D32as_v5", + }, + "Standard_D32d_v4": { + Size: "D32d_v4", + }, + "Standard_D32d_v5": { + Size: "D32d_v5", + }, + "Standard_D32ds_v4": { + Size: "D32ds_v4", + }, + "Standard_D32ds_v5": { + Size: "D32ds_v5", + }, + "Standard_D32lds_v5": { + Size: "D32lds_v5", + }, + "Standard_D32ls_v5": { + Size: "D32ls_v5", + }, + "Standard_D32pds_v5": { + Size: "D32pds_v5", + }, + "Standard_D32plds_v5": { + Size: "D32plds_v5", + }, + "Standard_D32pls_v5": { + Size: "D32pls_v5", + }, + "Standard_D32ps_v5": { + Size: "D32ps_v5", + }, + "Standard_D32s_v3": { + Size: "D32s_v3", + }, + "Standard_D32s_v4": { + Size: "D32s_v4", + }, + "Standard_D32s_v5": { + Size: "D32s_v5", + }, + "Standard_D3_v2": { + Size: "D3_v2", + }, + "Standard_D3_v2_Promo": { + Size: "D3_v2_Promo", + }, + "Standard_D4": { + Size: "D4", + }, + "Standard_D48_v3": { + Size: "D48_v3", + }, + "Standard_D48_v4": { + Size: "D48_v4", + }, + "Standard_D48_v5": { + Size: "D48_v5", + }, + "Standard_D48a_v3": { + Size: "D48a_v3", + }, + "Standard_D48a_v4": { + Size: "D48a_v4", + }, + "Standard_D48ads_v5": { + Size: "D48ads_v5", + }, + "Standard_D48as_v3": { + Size: "D48as_v3", + }, + "Standard_D48as_v4": { + Size: "D48as_v4", + }, + "Standard_D48as_v5": { + Size: "D48as_v5", + }, + "Standard_D48d_v4": { + Size: "D48d_v4", + }, + "Standard_D48d_v5": { + Size: "D48d_v5", + }, + "Standard_D48ds_v4": { + Size: "D48ds_v4", + }, + "Standard_D48ds_v5": { + Size: "D48ds_v5", + }, + "Standard_D48lds_v5": { + Size: "D48lds_v5", + }, + "Standard_D48ls_v5": { + Size: "D48ls_v5", + }, + "Standard_D48pds_v5": { + Size: "D48pds_v5", + }, + "Standard_D48plds_v5": { + Size: "D48plds_v5", + }, + "Standard_D48pls_v5": { + Size: "D48pls_v5", + }, + "Standard_D48ps_v5": { + Size: "D48ps_v5", + }, + "Standard_D48s_v3": { + Size: "D48s_v3", + }, + "Standard_D48s_v4": { + Size: "D48s_v4", + }, + "Standard_D48s_v5": { + Size: "D48s_v5", + }, + "Standard_D4_v2": { + Size: "D4_v2", + }, + "Standard_D4_v2_Promo": { + Size: "D4_v2_Promo", + }, + "Standard_D4_v3": { + Size: "D4_v3", + }, + "Standard_D4_v4": { + Size: "D4_v4", + }, + "Standard_D4_v5": { + Size: "D4_v5", + }, + "Standard_D4a_v3": { + Size: "D4a_v3", + }, + "Standard_D4a_v4": { + Size: "D4a_v4", + }, + "Standard_D4ads_v5": { + Size: "D4ads_v5", + }, + "Standard_D4as_v3": { + Size: "D4as_v3", + }, + "Standard_D4as_v4": { + Size: "D4as_v4", + }, + "Standard_D4as_v5": { + Size: "D4as_v5", + }, + "Standard_D4d_v4": { + Size: "D4d_v4", + }, + "Standard_D4d_v5": { + Size: "D4d_v5", + }, + "Standard_D4ds_v4": { + Size: "D4ds_v4", + }, + "Standard_D4ds_v5": { + Size: "D4ds_v5", + }, + "Standard_D4lds_v5": { + Size: "D4lds_v5", + }, + "Standard_D4ls_v5": { + Size: "D4ls_v5", + }, + "Standard_D4pds_v5": { + Size: "D4pds_v5", + }, + "Standard_D4plds_v5": { + Size: "D4plds_v5", + }, + "Standard_D4pls_v5": { + Size: "D4pls_v5", + }, + "Standard_D4ps_v5": { + Size: "D4ps_v5", + }, + "Standard_D4s_v3": { + Size: "D4s_v3", + }, + "Standard_D4s_v4": { + Size: "D4s_v4", + }, + "Standard_D4s_v5": { + Size: "D4s_v5", + }, + "Standard_D5_v2": { + Size: "D5_v2", + }, + "Standard_D5_v2_Promo": { + Size: "D5_v2_Promo", + }, + "Standard_D64_v3": { + Size: "D64_v3", + }, + "Standard_D64_v4": { + Size: "D64_v4", + }, + "Standard_D64_v5": { + Size: "D64_v5", + }, + "Standard_D64a_v3": { + Size: "D64a_v3", + }, + "Standard_D64a_v4": { + Size: "D64a_v4", + }, + "Standard_D64ads_v5": { + Size: "D64ads_v5", + }, + "Standard_D64as_v3": { + Size: "D64as_v3", + }, + "Standard_D64as_v4": { + Size: "D64as_v4", + }, + "Standard_D64as_v5": { + Size: "D64as_v5", + }, + "Standard_D64d_v4": { + Size: "D64d_v4", + }, + "Standard_D64d_v5": { + Size: "D64d_v5", + }, + "Standard_D64ds_v4": { + Size: "D64ds_v4", + }, + "Standard_D64ds_v5": { + Size: "D64ds_v5", + }, + "Standard_D64lds_v5": { + Size: "D64lds_v5", + }, + "Standard_D64ls_v5": { + Size: "D64ls_v5", + }, + "Standard_D64pds_v5": { + Size: "D64pds_v5", + }, + "Standard_D64plds_v5": { + Size: "D64plds_v5", + }, + "Standard_D64pls_v5": { + Size: "D64pls_v5", + }, + "Standard_D64ps_v5": { + Size: "D64ps_v5", + }, + "Standard_D64s_v3": { + Size: "D64s_v3", + }, + "Standard_D64s_v4": { + Size: "D64s_v4", + }, + "Standard_D64s_v5": { + Size: "D64s_v5", + }, + "Standard_D8_v3": { + Size: "D8_v3", + }, + "Standard_D8_v4": { + Size: "D8_v4", + }, + "Standard_D8_v5": { + Size: "D8_v5", + }, + "Standard_D8a_v3": { + Size: "D8a_v3", + }, + "Standard_D8a_v4": { + Size: "D8a_v4", + }, + "Standard_D8ads_v5": { + Size: "D8ads_v5", + }, + "Standard_D8as_v3": { + Size: "D8as_v3", + }, + "Standard_D8as_v4": { + Size: "D8as_v4", + }, + "Standard_D8as_v5": { + Size: "D8as_v5", + }, + "Standard_D8d_v4": { + Size: "D8d_v4", + }, + "Standard_D8d_v5": { + Size: "D8d_v5", + }, + "Standard_D8ds_v4": { + Size: "D8ds_v4", + }, + "Standard_D8ds_v5": { + Size: "D8ds_v5", + }, + "Standard_D8lds_v5": { + Size: "D8lds_v5", + }, + "Standard_D8ls_v5": { + Size: "D8ls_v5", + }, + "Standard_D8pds_v5": { + Size: "D8pds_v5", + }, + "Standard_D8plds_v5": { + Size: "D8plds_v5", + }, + "Standard_D8pls_v5": { + Size: "D8pls_v5", + }, + "Standard_D8ps_v5": { + Size: "D8ps_v5", + }, + "Standard_D8s_v3": { + Size: "D8s_v3", + }, + "Standard_D8s_v4": { + Size: "D8s_v4", + }, + "Standard_D8s_v5": { + Size: "D8s_v5", + }, + "Standard_D96_v5": { + Size: "D96_v5", + }, + "Standard_D96a_v4": { + Size: "D96a_v4", + }, + "Standard_D96ads_v5": { + Size: "D96ads_v5", + }, + "Standard_D96as_v4": { + Size: "D96as_v4", + }, + "Standard_D96as_v5": { + Size: "D96as_v5", + }, + "Standard_D96d_v5": { + Size: "D96d_v5", + }, + "Standard_D96ds_v5": { + Size: "D96ds_v5", + }, + "Standard_D96lds_v5": { + Size: "D96lds_v5", + }, + "Standard_D96ls_v5": { + Size: "D96ls_v5", + }, + "Standard_D96s_v5": { + Size: "D96s_v5", + }, + "Standard_DC16ads_cc_v5": { + Size: "DC16ads_cc_v5", + }, + "Standard_DC16ads_v5": { + Size: "DC16ads_v5", + }, + "Standard_DC16as_cc_v5": { + Size: "DC16as_cc_v5", + }, + "Standard_DC16as_v5": { + Size: "DC16as_v5", + }, + "Standard_DC16ds_v3": { + Size: "DC16ds_v3", + }, + "Standard_DC16s_v3": { + Size: "DC16s_v3", + }, + "Standard_DC1ds_v3": { + Size: "DC1ds_v3", + }, + "Standard_DC1s_v2": { + Size: "DC1s_v2", + }, + "Standard_DC1s_v3": { + Size: "DC1s_v3", + }, + "Standard_DC24ds_v3": { + Size: "DC24ds_v3", + }, + "Standard_DC24s_v3": { + Size: "DC24s_v3", + }, + "Standard_DC2ads_v5": { + Size: "DC2ads_v5", + }, + "Standard_DC2as_v5": { + Size: "DC2as_v5", + }, + "Standard_DC2ds_v3": { + Size: "DC2ds_v3", + }, + "Standard_DC2s": { + Size: "DC2s", + }, + "Standard_DC2s_v2": { + Size: "DC2s_v2", + }, + "Standard_DC2s_v3": { + Size: "DC2s_v3", + }, + "Standard_DC32ads_cc_v5": { + Size: "DC32ads_cc_v5", + }, + "Standard_DC32ads_v5": { + Size: "DC32ads_v5", + }, + "Standard_DC32as_cc_v5": { + Size: "DC32as_cc_v5", + }, + "Standard_DC32as_v5": { + Size: "DC32as_v5", + }, + "Standard_DC32ds_v3": { + Size: "DC32ds_v3", + }, + "Standard_DC32s_v3": { + Size: "DC32s_v3", + }, + "Standard_DC48ads_cc_v5": { + Size: "DC48ads_cc_v5", + }, + "Standard_DC48ads_v5": { + Size: "DC48ads_v5", + }, + "Standard_DC48as_cc_v5": { + Size: "DC48as_cc_v5", + }, + "Standard_DC48as_v5": { + Size: "DC48as_v5", + }, + "Standard_DC48ds_v3": { + Size: "DC48ds_v3", + }, + "Standard_DC48s_v3": { + Size: "DC48s_v3", + }, + "Standard_DC4ads_cc_v5": { + Size: "DC4ads_cc_v5", + }, + "Standard_DC4ads_v5": { + Size: "DC4ads_v5", + }, + "Standard_DC4as_cc_v5": { + Size: "DC4as_cc_v5", + }, + "Standard_DC4as_v5": { + Size: "DC4as_v5", + }, + "Standard_DC4ds_v3": { + Size: "DC4ds_v3", + }, + "Standard_DC4s": { + Size: "DC4s", + }, + "Standard_DC4s_v2": { + Size: "DC4s_v2", + }, + "Standard_DC4s_v3": { + Size: "DC4s_v3", + }, + "Standard_DC64ads_cc_v5": { + Size: "DC64ads_cc_v5", + }, + "Standard_DC64ads_v5": { + Size: "DC64ads_v5", + }, + "Standard_DC64as_cc_v5": { + Size: "DC64as_cc_v5", + }, + "Standard_DC64as_v5": { + Size: "DC64as_v5", + }, + "Standard_DC8_v2": { + Size: "DC8_v2", + }, + "Standard_DC8ads_cc_v5": { + Size: "DC8ads_cc_v5", + }, + "Standard_DC8ads_v5": { + Size: "DC8ads_v5", + }, + "Standard_DC8as_cc_v5": { + Size: "DC8as_cc_v5", + }, + "Standard_DC8as_v5": { + Size: "DC8as_v5", + }, + "Standard_DC8ds_v3": { + Size: "DC8ds_v3", + }, + "Standard_DC8s_v3": { + Size: "DC8s_v3", + }, + "Standard_DC96ads_cc_v5": { + Size: "DC96ads_cc_v5", + }, + "Standard_DC96ads_v5": { + Size: "DC96ads_v5", + }, + "Standard_DC96as_cc_v5": { + Size: "DC96as_cc_v5", + }, + "Standard_DC96as_v5": { + Size: "DC96as_v5", + }, + "Standard_DS1": { + Size: "DS1", + }, + "Standard_DS11": { + Size: "DS11", + }, + "Standard_DS11-1_v2": { + Size: "DS11-1_v2", + }, + "Standard_DS11_v2": { + Size: "DS11_v2", + }, + "Standard_DS11_v2_Promo": { + Size: "DS11_v2_Promo", + }, + "Standard_DS12": { + Size: "DS12", + }, + "Standard_DS12-1_v2": { + Size: "DS12-1_v2", + }, + "Standard_DS12-2_v2": { + Size: "DS12-2_v2", + }, + "Standard_DS12_v2": { + Size: "DS12_v2", + }, + "Standard_DS12_v2_Promo": { + Size: "DS12_v2_Promo", + }, + "Standard_DS13": { + Size: "DS13", + }, + "Standard_DS13-2_v2": { + Size: "DS13-2_v2", + }, + "Standard_DS13-4_v2": { + Size: "DS13-4_v2", + }, + "Standard_DS13_v2": { + Size: "DS13_v2", + }, + "Standard_DS13_v2_Promo": { + Size: "DS13_v2_Promo", + }, + "Standard_DS14": { + Size: "DS14", + }, + "Standard_DS14-4_v2": { + Size: "DS14-4_v2", + }, + "Standard_DS14-8_v2": { + Size: "DS14-8_v2", + }, + "Standard_DS14_v2": { + Size: "DS14_v2", + }, + "Standard_DS14_v2_Promo": { + Size: "DS14_v2_Promo", + }, + "Standard_DS15_v2": { + Size: "DS15_v2", + }, + "Standard_DS1_v2": { + Size: "DS1_v2", + }, + "Standard_DS2": { + Size: "DS2", + }, + "Standard_DS2_v2": { + Size: "DS2_v2", + }, + "Standard_DS2_v2_Promo": { + Size: "DS2_v2_Promo", + }, + "Standard_DS3": { + Size: "DS3", + }, + "Standard_DS3_v2": { + Size: "DS3_v2", + }, + "Standard_DS3_v2_Promo": { + Size: "DS3_v2_Promo", + }, + "Standard_DS4": { + Size: "DS4", + }, + "Standard_DS4_v2": { + Size: "DS4_v2", + }, + "Standard_DS4_v2_Promo": { + Size: "DS4_v2_Promo", + }, + "Standard_DS5_v2": { + Size: "DS5_v2", + }, + "Standard_DS5_v2_Promo": { + Size: "DS5_v2_Promo", + }, + "Standard_E104i_v5": { + Size: "E104i_v5", + }, + "Standard_E104id_v5": { + Size: "E104id_v5", + }, + "Standard_E104ids_v5": { + Size: "E104ids_v5", + }, + "Standard_E104is_v5": { + Size: "E104is_v5", + }, + "Standard_E112iads_v5": { + Size: "E112iads_v5", + }, + "Standard_E112ias_v5": { + Size: "E112ias_v5", + }, + "Standard_E112ibds_v5": { + Size: "E112ibds_v5", + }, + "Standard_E112ibs_v5": { + Size: "E112ibs_v5", + }, + "Standard_E16-4ads_v5": { + Size: "E16-4ads_v5", + }, + "Standard_E16-4as_v4": { + Size: "E16-4as_v4", + }, + "Standard_E16-4as_v5": { + Size: "E16-4as_v5", + }, + "Standard_E16-4ds_v4": { + Size: "E16-4ds_v4", + }, + "Standard_E16-4ds_v5": { + Size: "E16-4ds_v5", + }, + "Standard_E16-4s_v3": { + Size: "E16-4s_v3", + }, + "Standard_E16-4s_v4": { + Size: "E16-4s_v4", + }, + "Standard_E16-4s_v5": { + Size: "E16-4s_v5", + }, + "Standard_E16-8ads_v5": { + Size: "E16-8ads_v5", + }, + "Standard_E16-8as_v4": { + Size: "E16-8as_v4", + }, + "Standard_E16-8as_v5": { + Size: "E16-8as_v5", + }, + "Standard_E16-8ds_v4": { + Size: "E16-8ds_v4", + }, + "Standard_E16-8ds_v5": { + Size: "E16-8ds_v5", + }, + "Standard_E16-8s_v3": { + Size: "E16-8s_v3", + }, + "Standard_E16-8s_v4": { + Size: "E16-8s_v4", + }, + "Standard_E16-8s_v5": { + Size: "E16-8s_v5", + }, + "Standard_E16_v3": { + Size: "E16_v3", + }, + "Standard_E16_v4": { + Size: "E16_v4", + }, + "Standard_E16_v5": { + Size: "E16_v5", + }, + "Standard_E16a_v4": { + Size: "E16a_v4", + }, + "Standard_E16ads_v5": { + Size: "E16ads_v5", + }, + "Standard_E16as_v4": { + Size: "E16as_v4", + }, + "Standard_E16as_v5": { + Size: "E16as_v5", + }, + "Standard_E16bds_v5": { + Size: "E16bds_v5", + }, + "Standard_E16bs_v5": { + Size: "E16bs_v5", + }, + "Standard_E16d_v4": { + Size: "E16d_v4", + }, + "Standard_E16d_v5": { + Size: "E16d_v5", + }, + "Standard_E16ds_v4": { + Size: "E16ds_v4", + }, + "Standard_E16ds_v5": { + Size: "E16ds_v5", + }, + "Standard_E16pds_v5": { + Size: "E16pds_v5", + }, + "Standard_E16ps_v5": { + Size: "E16ps_v5", + }, + "Standard_E16s_v3": { + Size: "E16s_v3", + }, + "Standard_E16s_v4": { + Size: "E16s_v4", + }, + "Standard_E16s_v5": { + Size: "E16s_v5", + }, + "Standard_E20_v3": { + Size: "E20_v3", + }, + "Standard_E20_v4": { + Size: "E20_v4", + }, + "Standard_E20_v5": { + Size: "E20_v5", + }, + "Standard_E20a_v4": { + Size: "E20a_v4", + }, + "Standard_E20ads_v5": { + Size: "E20ads_v5", + }, + "Standard_E20as_v4": { + Size: "E20as_v4", + }, + "Standard_E20as_v5": { + Size: "E20as_v5", + }, + "Standard_E20d_v4": { + Size: "E20d_v4", + }, + "Standard_E20d_v5": { + Size: "E20d_v5", + }, + "Standard_E20ds_v4": { + Size: "E20ds_v4", + }, + "Standard_E20ds_v5": { + Size: "E20ds_v5", + }, + "Standard_E20pds_v5": { + Size: "E20pds_v5", + }, + "Standard_E20ps_v5": { + Size: "E20ps_v5", + }, + "Standard_E20s_v3": { + Size: "E20s_v3", + }, + "Standard_E20s_v4": { + Size: "E20s_v4", + }, + "Standard_E20s_v5": { + Size: "E20s_v5", + }, + "Standard_E2_v3": { + Size: "E2_v3", + }, + "Standard_E2_v4": { + Size: "E2_v4", + }, + "Standard_E2_v5": { + Size: "E2_v5", + }, + "Standard_E2a_v4": { + Size: "E2a_v4", + }, + "Standard_E2ads_v5": { + Size: "E2ads_v5", + }, + "Standard_E2as_v4": { + Size: "E2as_v4", + }, + "Standard_E2as_v5": { + Size: "E2as_v5", + }, + "Standard_E2bds_v5": { + Size: "E2bds_v5", + }, + "Standard_E2bs_v5": { + Size: "E2bs_v5", + }, + "Standard_E2d_v4": { + Size: "E2d_v4", + }, + "Standard_E2d_v5": { + Size: "E2d_v5", + }, + "Standard_E2ds_v4": { + Size: "E2ds_v4", + }, + "Standard_E2ds_v5": { + Size: "E2ds_v5", + }, + "Standard_E2pds_v5": { + Size: "E2pds_v5", + }, + "Standard_E2ps_v5": { + Size: "E2ps_v5", + }, + "Standard_E2s_v3": { + Size: "E2s_v3", + }, + "Standard_E2s_v4": { + Size: "E2s_v4", + }, + "Standard_E2s_v5": { + Size: "E2s_v5", + }, + "Standard_E32-16ads_v5": { + Size: "E32-16ads_v5", + }, + "Standard_E32-16as_v4": { + Size: "E32-16as_v4", + }, + "Standard_E32-16as_v5": { + Size: "E32-16as_v5", + }, + "Standard_E32-16ds_v4": { + Size: "E32-16ds_v4", + }, + "Standard_E32-16ds_v5": { + Size: "E32-16ds_v5", + }, + "Standard_E32-16s_v3": { + Size: "E32-16s_v3", + }, + "Standard_E32-16s_v4": { + Size: "E32-16s_v4", + }, + "Standard_E32-16s_v5": { + Size: "E32-16s_v5", + }, + "Standard_E32-8ads_v5": { + Size: "E32-8ads_v5", + }, + "Standard_E32-8as_v4": { + Size: "E32-8as_v4", + }, + "Standard_E32-8as_v5": { + Size: "E32-8as_v5", + }, + "Standard_E32-8ds_v4": { + Size: "E32-8ds_v4", + }, + "Standard_E32-8ds_v5": { + Size: "E32-8ds_v5", + }, + "Standard_E32-8s_v3": { + Size: "E32-8s_v3", + }, + "Standard_E32-8s_v4": { + Size: "E32-8s_v4", + }, + "Standard_E32-8s_v5": { + Size: "E32-8s_v5", + }, + "Standard_E32_v3": { + Size: "E32_v3", + }, + "Standard_E32_v4": { + Size: "E32_v4", + }, + "Standard_E32_v5": { + Size: "E32_v5", + }, + "Standard_E32a_v4": { + Size: "E32a_v4", + }, + "Standard_E32ads_v5": { + Size: "E32ads_v5", + }, + "Standard_E32as_v4": { + Size: "E32as_v4", + }, + "Standard_E32as_v5": { + Size: "E32as_v5", + }, + "Standard_E32bds_v5": { + Size: "E32bds_v5", + }, + "Standard_E32bs_v5": { + Size: "E32bs_v5", + }, + "Standard_E32d_v4": { + Size: "E32d_v4", + }, + "Standard_E32d_v5": { + Size: "E32d_v5", + }, + "Standard_E32ds_v4": { + Size: "E32ds_v4", + }, + "Standard_E32ds_v5": { + Size: "E32ds_v5", + }, + "Standard_E32pds_v5": { + Size: "E32pds_v5", + }, + "Standard_E32ps_v5": { + Size: "E32ps_v5", + }, + "Standard_E32s_v3": { + Size: "E32s_v3", + }, + "Standard_E32s_v4": { + Size: "E32s_v4", + }, + "Standard_E32s_v5": { + Size: "E32s_v5", + }, + "Standard_E4-2ads_v5": { + Size: "E4-2ads_v5", + }, + "Standard_E4-2as_v4": { + Size: "E4-2as_v4", + }, + "Standard_E4-2as_v5": { + Size: "E4-2as_v5", + }, + "Standard_E4-2ds_v4": { + Size: "E4-2ds_v4", + }, + "Standard_E4-2ds_v5": { + Size: "E4-2ds_v5", + }, + "Standard_E4-2s_v3": { + Size: "E4-2s_v3", + }, + "Standard_E4-2s_v4": { + Size: "E4-2s_v4", + }, + "Standard_E4-2s_v5": { + Size: "E4-2s_v5", + }, + "Standard_E48_v3": { + Size: "E48_v3", + }, + "Standard_E48_v4": { + Size: "E48_v4", + }, + "Standard_E48_v5": { + Size: "E48_v5", + }, + "Standard_E48a_v4": { + Size: "E48a_v4", + }, + "Standard_E48ads_v5": { + Size: "E48ads_v5", + }, + "Standard_E48as_v4": { + Size: "E48as_v4", + }, + "Standard_E48as_v5": { + Size: "E48as_v5", + }, + "Standard_E48bds_v5": { + Size: "E48bds_v5", + }, + "Standard_E48bs_v5": { + Size: "E48bs_v5", + }, + "Standard_E48d_v4": { + Size: "E48d_v4", + }, + "Standard_E48d_v5": { + Size: "E48d_v5", + }, + "Standard_E48ds_v4": { + Size: "E48ds_v4", + }, + "Standard_E48ds_v5": { + Size: "E48ds_v5", + }, + "Standard_E48s_v3": { + Size: "E48s_v3", + }, + "Standard_E48s_v4": { + Size: "E48s_v4", + }, + "Standard_E48s_v5": { + Size: "E48s_v5", + }, + "Standard_E4_v3": { + Size: "E4_v3", + }, + "Standard_E4_v4": { + Size: "E4_v4", + }, + "Standard_E4_v5": { + Size: "E4_v5", + }, + "Standard_E4a_v4": { + Size: "E4a_v4", + }, + "Standard_E4ads_v5": { + Size: "E4ads_v5", + }, + "Standard_E4as_v4": { + Size: "E4as_v4", + }, + "Standard_E4as_v5": { + Size: "E4as_v5", + }, + "Standard_E4bds_v5": { + Size: "E4bds_v5", + }, + "Standard_E4bs_v5": { + Size: "E4bs_v5", + }, + "Standard_E4d_v4": { + Size: "E4d_v4", + }, + "Standard_E4d_v5": { + Size: "E4d_v5", + }, + "Standard_E4ds_v4": { + Size: "E4ds_v4", + }, + "Standard_E4ds_v5": { + Size: "E4ds_v5", + }, + "Standard_E4pds_v5": { + Size: "E4pds_v5", + }, + "Standard_E4ps_v5": { + Size: "E4ps_v5", + }, + "Standard_E4s_v3": { + Size: "E4s_v3", + }, + "Standard_E4s_v4": { + Size: "E4s_v4", + }, + "Standard_E4s_v5": { + Size: "E4s_v5", + }, + "Standard_E64-16ads_v5": { + Size: "E64-16ads_v5", + }, + "Standard_E64-16as_v4": { + Size: "E64-16as_v4", + }, + "Standard_E64-16as_v5": { + Size: "E64-16as_v5", + }, + "Standard_E64-16ds_v4": { + Size: "E64-16ds_v4", + }, + "Standard_E64-16ds_v5": { + Size: "E64-16ds_v5", + }, + "Standard_E64-16s_v3": { + Size: "E64-16s_v3", + }, + "Standard_E64-16s_v4": { + Size: "E64-16s_v4", + }, + "Standard_E64-16s_v5": { + Size: "E64-16s_v5", + }, + "Standard_E64-32ads_v5": { + Size: "E64-32ads_v5", + }, + "Standard_E64-32as_v4": { + Size: "E64-32as_v4", + }, + "Standard_E64-32as_v5": { + Size: "E64-32as_v5", + }, + "Standard_E64-32ds_v4": { + Size: "E64-32ds_v4", + }, + "Standard_E64-32ds_v5": { + Size: "E64-32ds_v5", + }, + "Standard_E64-32s_v3": { + Size: "E64-32s_v3", + }, + "Standard_E64-32s_v4": { + Size: "E64-32s_v4", + }, + "Standard_E64-32s_v5": { + Size: "E64-32s_v5", + }, + "Standard_E64_v3": { + Size: "E64_v3", + }, + "Standard_E64_v4": { + Size: "E64_v4", + }, + "Standard_E64_v5": { + Size: "E64_v5", + }, + "Standard_E64a_v4": { + Size: "E64a_v4", + }, + "Standard_E64ads_v5": { + Size: "E64ads_v5", + }, + "Standard_E64as_v4": { + Size: "E64as_v4", + }, + "Standard_E64as_v5": { + Size: "E64as_v5", + }, + "Standard_E64bds_v5": { + Size: "E64bds_v5", + }, + "Standard_E64bs_v5": { + Size: "E64bs_v5", + }, + "Standard_E64d_v4": { + Size: "E64d_v4", + }, + "Standard_E64d_v5": { + Size: "E64d_v5", + }, + "Standard_E64ds_v4": { + Size: "E64ds_v4", + }, + "Standard_E64ds_v5": { + Size: "E64ds_v5", + }, + "Standard_E64i_v3": { + Size: "E64i_v3", + }, + "Standard_E64is_v3": { + Size: "E64is_v3", + }, + "Standard_E64s_v3": { + Size: "E64s_v3", + }, + "Standard_E64s_v4": { + Size: "E64s_v4", + }, + "Standard_E64s_v5": { + Size: "E64s_v5", + }, + "Standard_E8-2ads_v5": { + Size: "E8-2ads_v5", + }, + "Standard_E8-2as_v4": { + Size: "E8-2as_v4", + }, + "Standard_E8-2as_v5": { + Size: "E8-2as_v5", + }, + "Standard_E8-2ds_v4": { + Size: "E8-2ds_v4", + }, + "Standard_E8-2ds_v5": { + Size: "E8-2ds_v5", + }, + "Standard_E8-2s_v3": { + Size: "E8-2s_v3", + }, + "Standard_E8-2s_v4": { + Size: "E8-2s_v4", + }, + "Standard_E8-2s_v5": { + Size: "E8-2s_v5", + }, + "Standard_E8-4ads_v5": { + Size: "E8-4ads_v5", + }, + "Standard_E8-4as_v4": { + Size: "E8-4as_v4", + }, + "Standard_E8-4as_v5": { + Size: "E8-4as_v5", + }, + "Standard_E8-4ds_v4": { + Size: "E8-4ds_v4", + }, + "Standard_E8-4ds_v5": { + Size: "E8-4ds_v5", + }, + "Standard_E8-4s_v3": { + Size: "E8-4s_v3", + }, + "Standard_E8-4s_v4": { + Size: "E8-4s_v4", + }, + "Standard_E8-4s_v5": { + Size: "E8-4s_v5", + }, + "Standard_E80ids_v4": { + Size: "E80ids_v4", + }, + "Standard_E80is_v4": { + Size: "E80is_v4", + }, + "Standard_E8_v3": { + Size: "E8_v3", + }, + "Standard_E8_v4": { + Size: "E8_v4", + }, + "Standard_E8_v5": { + Size: "E8_v5", + }, + "Standard_E8a_v4": { + Size: "E8a_v4", + }, + "Standard_E8ads_v5": { + Size: "E8ads_v5", + }, + "Standard_E8as_v4": { + Size: "E8as_v4", + }, + "Standard_E8as_v5": { + Size: "E8as_v5", + }, + "Standard_E8bds_v5": { + Size: "E8bds_v5", + }, + "Standard_E8bs_v5": { + Size: "E8bs_v5", + }, + "Standard_E8d_v4": { + Size: "E8d_v4", + }, + "Standard_E8d_v5": { + Size: "E8d_v5", + }, + "Standard_E8ds_v4": { + Size: "E8ds_v4", + }, + "Standard_E8ds_v5": { + Size: "E8ds_v5", + }, + "Standard_E8pds_v5": { + Size: "E8pds_v5", + }, + "Standard_E8ps_v5": { + Size: "E8ps_v5", + }, + "Standard_E8s_v3": { + Size: "E8s_v3", + }, + "Standard_E8s_v4": { + Size: "E8s_v4", + }, + "Standard_E8s_v5": { + Size: "E8s_v5", + }, + "Standard_E96-24ads_v5": { + Size: "E96-24ads_v5", + }, + "Standard_E96-24as_v4": { + Size: "E96-24as_v4", + }, + "Standard_E96-24as_v5": { + Size: "E96-24as_v5", + }, + "Standard_E96-24ds_v5": { + Size: "E96-24ds_v5", + }, + "Standard_E96-24s_v5": { + Size: "E96-24s_v5", + }, + "Standard_E96-48ads_v5": { + Size: "E96-48ads_v5", + }, + "Standard_E96-48as_v4": { + Size: "E96-48as_v4", + }, + "Standard_E96-48as_v5": { + Size: "E96-48as_v5", + }, + "Standard_E96-48ds_v5": { + Size: "E96-48ds_v5", + }, + "Standard_E96-48s_v5": { + Size: "E96-48s_v5", + }, + "Standard_E96_v5": { + Size: "E96_v5", + }, + "Standard_E96a_v4": { + Size: "E96a_v4", + }, + "Standard_E96ads_v5": { + Size: "E96ads_v5", + }, + "Standard_E96as_v4": { + Size: "E96as_v4", + }, + "Standard_E96as_v5": { + Size: "E96as_v5", + }, + "Standard_E96bds_v5": { + Size: "E96bds_v5", + }, + "Standard_E96bs_v5": { + Size: "E96bs_v5", + }, + "Standard_E96d_v5": { + Size: "E96d_v5", + }, + "Standard_E96ds_v5": { + Size: "E96ds_v5", + }, + "Standard_E96ias_v4": { + Size: "E96ias_v4", + }, + "Standard_E96s_v5": { + Size: "E96s_v5", + }, + "Standard_EC16ads_cc_v5": { + Size: "EC16ads_cc_v5", + }, + "Standard_EC16ads_v5": { + Size: "EC16ads_v5", + }, + "Standard_EC16as_cc_v5": { + Size: "EC16as_cc_v5", + }, + "Standard_EC16as_v5": { + Size: "EC16as_v5", + }, + "Standard_EC20ads_cc_v5": { + Size: "EC20ads_cc_v5", + }, + "Standard_EC20ads_v5": { + Size: "EC20ads_v5", + }, + "Standard_EC20as_cc_v5": { + Size: "EC20as_cc_v5", + }, + "Standard_EC20as_v5": { + Size: "EC20as_v5", + }, + "Standard_EC2ads_v5": { + Size: "EC2ads_v5", + }, + "Standard_EC2as_v5": { + Size: "EC2as_v5", + }, + "Standard_EC32ads_cc_v5": { + Size: "EC32ads_cc_v5", + }, + "Standard_EC32ads_v5": { + Size: "EC32ads_v5", + }, + "Standard_EC32as_cc_v5": { + Size: "EC32as_cc_v5", + }, + "Standard_EC32as_v5": { + Size: "EC32as_v5", + }, + "Standard_EC48ads_cc_v5": { + Size: "EC48ads_cc_v5", + }, + "Standard_EC48ads_v5": { + Size: "EC48ads_v5", + }, + "Standard_EC48as_cc_v5": { + Size: "EC48as_cc_v5", + }, + "Standard_EC48as_v5": { + Size: "EC48as_v5", + }, + "Standard_EC4ads_cc_v5": { + Size: "EC4ads_cc_v5", + }, + "Standard_EC4ads_v5": { + Size: "EC4ads_v5", + }, + "Standard_EC4as_cc_v5": { + Size: "EC4as_cc_v5", + }, + "Standard_EC4as_v5": { + Size: "EC4as_v5", + }, + "Standard_EC64ads_cc_v5": { + Size: "EC64ads_cc_v5", + }, + "Standard_EC64ads_v5": { + Size: "EC64ads_v5", + }, + "Standard_EC64as_cc_v5": { + Size: "EC64as_cc_v5", + }, + "Standard_EC64as_v5": { + Size: "EC64as_v5", + }, + "Standard_EC8ads_cc_v5": { + Size: "EC8ads_cc_v5", + }, + "Standard_EC8ads_v5": { + Size: "EC8ads_v5", + }, + "Standard_EC8as_cc_v5": { + Size: "EC8as_cc_v5", + }, + "Standard_EC8as_v5": { + Size: "EC8as_v5", + }, + "Standard_EC96ads_cc_v5": { + Size: "EC96ads_cc_v5", + }, + "Standard_EC96ads_v5": { + Size: "EC96ads_v5", + }, + "Standard_EC96as_cc_v5": { + Size: "EC96as_cc_v5", + }, + "Standard_EC96as_v5": { + Size: "EC96as_v5", + }, + "Standard_EC96iads_v5": { + Size: "EC96iads_v5", + }, + "Standard_EC96ias_v5": { + Size: "EC96ias_v5", + }, + "Standard_F1": { + Size: "F1", + }, + "Standard_F16": { + Size: "F16", + }, + "Standard_F16s": { + Size: "F16s", + }, + "Standard_F16s_v2": { + Size: "F16s_v2", + }, + "Standard_F1s": { + Size: "F1s", + }, + "Standard_F2": { + Size: "F2", + }, + "Standard_F2s": { + Size: "F2s", + }, + "Standard_F2s_v2": { + Size: "F2s_v2", + }, + "Standard_F32s_v2": { + Size: "F32s_v2", + }, + "Standard_F4": { + Size: "F4", + }, + "Standard_F48s_v2": { + Size: "F48s_v2", + }, + "Standard_F4s": { + Size: "F4s", + }, + "Standard_F4s_v2": { + Size: "F4s_v2", + }, + "Standard_F64s_v2": { + Size: "F64s_v2", + }, + "Standard_F72s_v2": { + Size: "F72s_v2", + }, + "Standard_F8": { + Size: "F8", + }, + "Standard_F8s": { + Size: "F8s", + }, + "Standard_F8s_v2": { + Size: "F8s_v2", + }, + "Standard_FX12mds": { + Size: "FX12mds", + }, + "Standard_FX24mds": { + Size: "FX24mds", + }, + "Standard_FX36mds": { + Size: "FX36mds", + }, + "Standard_FX48mds": { + Size: "FX48mds", + }, + "Standard_FX4mds": { + Size: "FX4mds", + }, + "Standard_G1": { + Size: "G1", + }, + "Standard_G2": { + Size: "G2", + }, + "Standard_G3": { + Size: "G3", + }, + "Standard_G4": { + Size: "G4", + }, + "Standard_G5": { + Size: "G5", + }, + "Standard_GS1": { + Size: "GS1", + }, + "Standard_GS2": { + Size: "GS2", + }, + "Standard_GS3": { + Size: "GS3", + }, + "Standard_GS4": { + Size: "GS4", + }, + "Standard_GS4-4": { + Size: "GS4-4", + }, + "Standard_GS4-8": { + Size: "GS4-8", + }, + "Standard_GS5": { + Size: "GS5", + }, + "Standard_GS5-16": { + Size: "GS5-16", + }, + "Standard_GS5-8": { + Size: "GS5-8", + }, + "Standard_HB120-16rs_v2": { + Size: "HB120-16rs_v2", + }, + "Standard_HB120-16rs_v3": { + Size: "HB120-16rs_v3", + }, + "Standard_HB120-32rs_v2": { + Size: "HB120-32rs_v2", + }, + "Standard_HB120-32rs_v3": { + Size: "HB120-32rs_v3", + }, + "Standard_HB120-64rs_v2": { + Size: "HB120-64rs_v2", + }, + "Standard_HB120-64rs_v3": { + Size: "HB120-64rs_v3", + }, + "Standard_HB120-96rs_v2": { + Size: "HB120-96rs_v2", + }, + "Standard_HB120-96rs_v3": { + Size: "HB120-96rs_v3", + }, + "Standard_HB120rs_v2": { + Size: "HB120rs_v2", + }, + "Standard_HB120rs_v3": { + Size: "HB120rs_v3", + }, + "Standard_HB176-144rs_v4": { + Size: "HB176-144rs_v4", + }, + "Standard_HB176-24rs_v4": { + Size: "HB176-24rs_v4", + }, + "Standard_HB176-48rs_v4": { + Size: "HB176-48rs_v4", + }, + "Standard_HB176-96rs_v4": { + Size: "HB176-96rs_v4", + }, + "Standard_HB176rs_v4": { + Size: "HB176rs_v4", + }, + "Standard_HB60-15rs": { + Size: "HB60-15rs", + }, + "Standard_HB60-30rs": { + Size: "HB60-30rs", + }, + "Standard_HB60-45rs": { + Size: "HB60-45rs", + }, + "Standard_HB60rs": { + Size: "HB60rs", + }, + "Standard_HC44-16rs": { + Size: "HC44-16rs", + }, + "Standard_HC44-32rs": { + Size: "HC44-32rs", + }, + "Standard_HC44rs": { + Size: "HC44rs", + }, + "Standard_HX176-144rs": { + Size: "HX176-144rs", + }, + "Standard_HX176-24rs": { + Size: "HX176-24rs", + }, + "Standard_HX176-48rs": { + Size: "HX176-48rs", + }, + "Standard_HX176-96rs": { + Size: "HX176-96rs", + }, + "Standard_HX176rs": { + Size: "HX176rs", + }, + "Standard_L16as_v3": { + Size: "L16as_v3", + }, + "Standard_L16s": { + Size: "L16s", + }, + "Standard_L16s_v2": { + Size: "L16s_v2", + }, + "Standard_L16s_v3": { + Size: "L16s_v3", + }, + "Standard_L32as_v3": { + Size: "L32as_v3", + }, + "Standard_L32s": { + Size: "L32s", + }, + "Standard_L32s_v2": { + Size: "L32s_v2", + }, + "Standard_L32s_v3": { + Size: "L32s_v3", + }, + "Standard_L48as_v3": { + Size: "L48as_v3", + }, + "Standard_L48s_v2": { + Size: "L48s_v2", + }, + "Standard_L48s_v3": { + Size: "L48s_v3", + }, + "Standard_L4s": { + Size: "L4s", + }, + "Standard_L64as_v3": { + Size: "L64as_v3", + }, + "Standard_L64s_v2": { + Size: "L64s_v2", + }, + "Standard_L64s_v3": { + Size: "L64s_v3", + }, + "Standard_L80as_v3": { + Size: "L80as_v3", + }, + "Standard_L80s_v2": { + Size: "L80s_v2", + }, + "Standard_L80s_v3": { + Size: "L80s_v3", + }, + "Standard_L8as_v3": { + Size: "L8as_v3", + }, + "Standard_L8s": { + Size: "L8s", + }, + "Standard_L8s_v2": { + Size: "L8s_v2", + }, + "Standard_L8s_v3": { + Size: "L8s_v3", + }, + "Standard_M128": { + Size: "M128", + }, + "Standard_M128-32ms": { + Size: "M128-32ms", + }, + "Standard_M128-64ms": { + Size: "M128-64ms", + }, + "Standard_M128dms_v2": { + Size: "M128dms_v2", + }, + "Standard_M128ds_v2": { + Size: "M128ds_v2", + }, + "Standard_M128m": { + Size: "M128m", + }, + "Standard_M128ms": { + Size: "M128ms", + }, + "Standard_M128ms_v2": { + Size: "M128ms_v2", + }, + "Standard_M128s": { + Size: "M128s", + }, + "Standard_M128s_v2": { + Size: "M128s_v2", + }, + "Standard_M16-4ms": { + Size: "M16-4ms", + }, + "Standard_M16-8ms": { + Size: "M16-8ms", + }, + "Standard_M16ms": { + Size: "M16ms", + }, + "Standard_M192idms_v2": { + Size: "M192idms_v2", + }, + "Standard_M192ids_v2": { + Size: "M192ids_v2", + }, + "Standard_M192ims_v2": { + Size: "M192ims_v2", + }, + "Standard_M192is_v2": { + Size: "M192is_v2", + }, + "Standard_M208ms_v2": { + Size: "M208ms_v2", + }, + "Standard_M208s_v2": { + Size: "M208s_v2", + }, + "Standard_M32-16ms": { + Size: "M32-16ms", + }, + "Standard_M32-8ms": { + Size: "M32-8ms", + }, + "Standard_M32dms_v2": { + Size: "M32dms_v2", + }, + "Standard_M32ls": { + Size: "M32ls", + }, + "Standard_M32ms": { + Size: "M32ms", + }, + "Standard_M32ms_v2": { + Size: "M32ms_v2", + }, + "Standard_M32ts": { + Size: "M32ts", + }, + "Standard_M416-208ms_v2": { + Size: "M416-208ms_v2", + }, + "Standard_M416-208s_v2": { + Size: "M416-208s_v2", + }, + "Standard_M416ms_v2": { + Size: "M416ms_v2", + }, + "Standard_M416s_8_v2": { + Size: "M416s_8_v2", + }, + "Standard_M416s_v2": { + Size: "M416s_v2", + }, + "Standard_M64": { + Size: "M64", + }, + "Standard_M64-16ms": { + Size: "M64-16ms", + }, + "Standard_M64-32ms": { + Size: "M64-32ms", + }, + "Standard_M64dms_v2": { + Size: "M64dms_v2", + }, + "Standard_M64ds_v2": { + Size: "M64ds_v2", + }, + "Standard_M64ls": { + Size: "M64ls", + }, + "Standard_M64m": { + Size: "M64m", + }, + "Standard_M64ms": { + Size: "M64ms", + }, + "Standard_M64ms_v2": { + Size: "M64ms_v2", + }, + "Standard_M64s": { + Size: "M64s", + }, + "Standard_M64s_v2": { + Size: "M64s_v2", + }, + "Standard_M8-2ms": { + Size: "M8-2ms", + }, + "Standard_M8-4ms": { + Size: "M8-4ms", + }, + "Standard_M8ms": { + Size: "M8ms", + }, + "Standard_NC12": { + Size: "NC12", + }, + "Standard_NC12_Promo": { + Size: "NC12_Promo", + }, + "Standard_NC12s_v2": { + Size: "NC12s_v2", + }, + "Standard_NC12s_v3": { + Size: "NC12s_v3", + }, + "Standard_NC16ads_A10_v4": { + Size: "NC16ads_A10_v4", + }, + "Standard_NC16as_T4_v3": { + Size: "NC16as_T4_v3", + }, + "Standard_NC24": { + Size: "NC24", + }, + "Standard_NC24_Promo": { + Size: "NC24_Promo", + }, + "Standard_NC24ads_A100_v4": { + Size: "NC24ads_A100_v4", + }, + "Standard_NC24r": { + Size: "NC24r", + }, + "Standard_NC24r_Promo": { + Size: "NC24r_Promo", + }, + "Standard_NC24rs_v2": { + Size: "NC24rs_v2", + }, + "Standard_NC24rs_v3": { + Size: "NC24rs_v3", + }, + "Standard_NC24s_v2": { + Size: "NC24s_v2", + }, + "Standard_NC24s_v3": { + Size: "NC24s_v3", + }, + "Standard_NC32ads_A10_v4": { + Size: "NC32ads_A10_v4", + }, + "Standard_NC48ads_A100_v4": { + Size: "NC48ads_A100_v4", + }, + "Standard_NC4as_T4_v3": { + Size: "NC4as_T4_v3", + }, + "Standard_NC6": { + Size: "NC6", + }, + "Standard_NC64as_T4_v3": { + Size: "NC64as_T4_v3", + }, + "Standard_NC6_Promo": { + Size: "NC6_Promo", + }, + "Standard_NC6s_v2": { + Size: "NC6s_v2", + }, + "Standard_NC6s_v3": { + Size: "NC6s_v3", + }, + "Standard_NC8ads_A10_v4": { + Size: "NC8ads_A10_v4", + }, + "Standard_NC8as_T4_v3": { + Size: "NC8as_T4_v3", + }, + "Standard_NC96ads_A100_v4": { + Size: "NC96ads_A100_v4", + }, + "Standard_ND12s": { + Size: "ND12s", + }, + "Standard_ND24rs": { + Size: "ND24rs", + }, + "Standard_ND24s": { + Size: "ND24s", + }, + "Standard_ND40rs_v2": { + Size: "ND40rs_v2", + }, + "Standard_ND40s_v3": { + Size: "ND40s_v3", + }, + "Standard_ND6s": { + Size: "ND6s", + }, + "Standard_ND96amsr_A100_v4": { + Size: "ND96amsr_A100_v4", + }, + "Standard_ND96asr_v4": { + Size: "ND96asr_v4", + }, + "Standard_NP10s": { + Size: "NP10s", + }, + "Standard_NP20s": { + Size: "NP20s", + }, + "Standard_NP40s": { + Size: "NP40s", + }, + "Standard_NV12": { + Size: "NV12", + }, + "Standard_NV12_Promo": { + Size: "NV12_Promo", + }, + "Standard_NV12ads_A10_v5": { + Size: "NV12ads_A10_v5", + }, + "Standard_NV12s_v2": { + Size: "NV12s_v2", + }, + "Standard_NV12s_v3": { + Size: "NV12s_v3", + }, + "Standard_NV16as_v4": { + Size: "NV16as_v4", + }, + "Standard_NV18ads_A10_v5": { + Size: "NV18ads_A10_v5", + }, + "Standard_NV24": { + Size: "NV24", + }, + "Standard_NV24_Promo": { + Size: "NV24_Promo", + }, + "Standard_NV24s_v2": { + Size: "NV24s_v2", + }, + "Standard_NV24s_v3": { + Size: "NV24s_v3", + }, + "Standard_NV32as_v4": { + Size: "NV32as_v4", + }, + "Standard_NV36adms_A10_v5": { + Size: "NV36adms_A10_v5", + }, + "Standard_NV36ads_A10_v5": { + Size: "NV36ads_A10_v5", + }, + "Standard_NV48s_v3": { + Size: "NV48s_v3", + }, + "Standard_NV4as_v4": { + Size: "NV4as_v4", + }, + "Standard_NV6": { + Size: "NV6", + }, + "Standard_NV6_Promo": { + Size: "NV6_Promo", + }, + "Standard_NV6ads_A10_v5": { + Size: "NV6ads_A10_v5", + }, + "Standard_NV6s_v2": { + Size: "NV6s_v2", + }, + "Standard_NV72ads_A10_v5": { + Size: "NV72ads_A10_v5", + }, + "Standard_NV8as_v4": { + Size: "NV8as_v4", + }, + "Standard_PB6s": { + Size: "PB6s", + }, +} diff --git a/v2/vmsize.go b/v2/vmsize.go new file mode 100644 index 0000000..3d2d028 --- /dev/null +++ b/v2/vmsize.go @@ -0,0 +1,160 @@ +package skewer + +import ( + "fmt" + "regexp" + "strconv" +) + +// This file adds support for more capabilities based on VM naming conventions that includes vmsize parsing. +// VM naming conventions are documented at: https://docs.microsoft.com/en-us/azure/virtual-machines/vm-naming-conventions +// Note: Some common capabilities like familyName and VCPUs, which can also be +// fetched using the ResourceSKU API, are not included here. They can be found in sku.go. + +var skuSizeScheme = regexp.MustCompile( + `^([A-Z])([A-Z]?)([A-Z]?)([0-9]+)-?((?:[0-9]+)?)((?:[abcdeilmtspPr]+|C+|NP)?)_?(?:([A-Z][0-9]+)_?)?(_cc_)?(_[0-9]+_)?(_MI300X_)?(_H100_)?((?:[vV][1-9])?)?(_Promo)?$`, +) + +// unParsableVMSizes map holds vmSize strings that cannot be easily parsed with skuSizeScheme. +var unParsableVMSizes = map[string]VMSizeType{ + "M416s_8_v2": { + Family: "M", + Subfamily: nil, + Cpus: "416", + CpusConstrained: nil, + AdditiveFeatures: []rune{'s'}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "v2", + PromoVersion: false, + Series: "Ms_v2", + }, +} + +type VMSizeType struct { + Family string + Subfamily *string + Cpus string + CpusConstrained *string + AdditiveFeatures []rune + AcceleratorType *string + ConfidentialChildCapability bool + Version string + PromoVersion bool + MI300Series bool + H100Series bool + Series string +} + +// parseVMSize parses the VM size and returns the parts as a map. +func parseVMSize(vmSizeName string) ([]string, error) { + parts := skuSizeScheme.FindStringSubmatch(vmSizeName) + if len(parts) < 10 { + return nil, fmt.Errorf("could not parse VM size %s", vmSizeName) + } + return parts, nil +} + +// GetVMSize is a helper function used by GetVMSize() in sku.go +func GetVMSize(vmSizeName string) (*VMSizeType, error) { + vmSize := VMSizeType{} + + parts, err := parseVMSize(vmSizeName) + if err != nil { + if vmSizeVal, ok := unParsableVMSizes[vmSizeName]; ok { + return &vmSizeVal, nil + } + return nil, err + } + + // [Family] - ([A-Z]): Captures a single uppercase letter. + vmSize.Family = parts[1] + + // [Sub-family]* - ([A-Z]?): Optionally captures another uppercase letter. + if len(parts[2]) > 0 { + var subfamilyStr string + if len(parts[3]) > 0 { + subfamilyStr = parts[2] + parts[3] + } else { + subfamilyStr = parts[2] + } + vmSize.Subfamily = &subfamilyStr + } + + // [# of vCPUs] - ([0-9]+): Captures one or more digits. + vmSize.Cpus = parts[4] + + // [Constrained vCPUs]* + // -?: Optionally captures a hyphen. + // ((?:[0-9]+)?): Optionally captures another sequence of one or more digits. + if len(parts[5]) > 0 { + _, err := strconv.Atoi(parts[5]) + if err != nil { + return nil, fmt.Errorf("converting constrained CPUs, %w", err) + } + vmSize.CpusConstrained = &parts[5] + } + + // [Additive Features] + // ((?:[abcdilmtspPr]+|C+|NP)?): Captures a sequence of letters representing certain attributes. + // It can capture combinations like 'abcdilmtspPr' or 'C+' or 'NP'. + vmSize.AdditiveFeatures = []rune(parts[6]) + + // [Accelerator Type]* + // _?: Optionally captures an underscore. + // (?:([A-Z][0-9]+)_?)?: Optionally captures a pattern that starts with an uppercase letter followed by digits, + // followed by an optional underscore. + if len(parts[7]) > 0 { + vmSize.AcceleratorType = &parts[7] + } + + // [Confidential Child Capability]* - only AKS + // (_cc_)?: Optionally captures the string "cc" with underscores on both sides. + if parts[8] == "_cc_" { + vmSize.ConfidentialChildCapability = true + } + + // parts slice at index 8 disambiguates more enhanced memory and I/O capabilities + // for Standard M memory-optimized VM series. + // For example: + // 1 in Standard_M96s_1_v3 + // and 2 in Standard_M96s_2_v3 + // Ref: https://learn.microsoft.com/en-us/azure/virtual-machines/msv3-mdsv3-medium-series + + // [MI300X]* + // (_MI300X_)?: Optionally captures the string "_MI300X". + // This is used to identify the MI300 series of VMs. + if parts[10] == "MI300X" { + vmSize.MI300Series = true + } + + // [H100]* + // (_H100_)?: Optionally captures the string "_H100". + // This is used to identify the H100 series of VMs. + if parts[11] == "H100" { + vmSize.H100Series = true + } + + // [Version]* + // Optionally captures the pattern 'v' or 'V' followed by a digit from 1 to 9. + vmSize.Version = parts[12] + + // [Promo]* + // (_Promo)?: Optionally captures the string "_Promo". + if parts[13] == "_Promo" { + vmSize.PromoVersion = true + } + + // [Series] + subfamily := "" + if vmSize.Subfamily != nil { + subfamily = *vmSize.Subfamily + } + version := "" + if len(vmSize.Version) > 0 { + version = "_" + vmSize.Version + } + vmSize.Series = vmSize.Family + subfamily + string(vmSize.AdditiveFeatures) + version + + return &vmSize, nil +} diff --git a/v2/vmsize_test.go b/v2/vmsize_test.go new file mode 100644 index 0000000..03e6bfd --- /dev/null +++ b/v2/vmsize_test.go @@ -0,0 +1,199 @@ +package skewer + +import ( + "fmt" + "testing" + + "github.com/Azure/skewer/v2/testdata" + "github.com/stretchr/testify/assert" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" +) + +// TestParseVMSize tests the parseVMSize function. It uses testdata from generated_vmsize_testdata.go +// This test validates the parsing capability and not the actual values. +func TestParseVMSize(t *testing.T) { + total := len(testdata.SKUData) + fail := 0 + for skuName, tc := range testdata.SKUData { + if _, err := parseVMSize(tc.Size); err != nil { + if _, ok := unParsableVMSizes[tc.Size]; !ok { + t.Errorf("parsing fails for for sku %s, err: %v", skuName, err) + fail += 1 + } + } + } + t.Logf("Passed SKUs: %d, Failed SKUs: %d", total-fail, fail) +} + +// Define the test cases for get() methods in vmsize.go +var testCases = []struct { + name string + size string + expectedVM *VMSizeType + err error +}{ + { + name: "Standard_NV16as_v4", + size: "NV16as_v4", + expectedVM: &VMSizeType{ + Family: "N", + Subfamily: to.Ptr("V"), + Cpus: "16", + CpusConstrained: nil, + AdditiveFeatures: []rune{'a', 's'}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "v4", + PromoVersion: false, + Series: "NVas_v4", + }, + err: nil, + }, + { + name: "Standard_M16ms", + size: "M16ms_v2", + expectedVM: &VMSizeType{ + Family: "M", + Subfamily: nil, + Cpus: "16", + CpusConstrained: nil, + AdditiveFeatures: []rune{'m', 's'}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "v2", + PromoVersion: false, + Series: "Mms_v2", + }, + err: nil, + }, + { + name: "Standard_NC4as_T4_v3", + size: "NC4as_T4_v3", + expectedVM: &VMSizeType{ + Family: "N", + Subfamily: to.Ptr("C"), + Cpus: "4", + CpusConstrained: nil, + AdditiveFeatures: []rune{'a', 's'}, + AcceleratorType: to.Ptr("T4"), + ConfidentialChildCapability: false, + Version: "v3", + PromoVersion: false, + Series: "NCas_v3", + }, + err: nil, + }, + { + name: "Standard_M8-2ms", + size: "M8-2ms_v2", + expectedVM: &VMSizeType{ + Family: "M", + Subfamily: nil, + Cpus: "8", + CpusConstrained: to.Ptr("2"), + AdditiveFeatures: []rune{'m', 's'}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "v2", + PromoVersion: false, + Series: "Mms_v2", + }, + err: nil, + }, + { + name: "Standard_A4_v2", + size: "A4_v2", + expectedVM: &VMSizeType{ + Family: "A", + Subfamily: nil, + Cpus: "4", + CpusConstrained: nil, + AdditiveFeatures: []rune{}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "v2", + PromoVersion: false, + Series: "A_v2", + }, + err: nil, + }, + { + name: "Standard_EC48as_cc_v5", + size: "EC48as_cc_v5", + expectedVM: &VMSizeType{ + Family: "E", + Subfamily: to.Ptr("C"), + Cpus: "48", + CpusConstrained: nil, + AdditiveFeatures: []rune{'a', 's'}, + AcceleratorType: nil, + ConfidentialChildCapability: true, + Version: "v5", + PromoVersion: false, + Series: "ECas_v5", + }, + err: nil, + }, + { + name: "Standard_NV24", + size: "NV24", + expectedVM: &VMSizeType{ + Family: "N", + Subfamily: to.Ptr("V"), + Cpus: "24", + CpusConstrained: nil, + AdditiveFeatures: []rune{}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "", + PromoVersion: false, + Series: "NV", + }, + err: nil, + }, + { + name: "Standard_D3_v2_Promo", + size: "D3_v2_Promo", + expectedVM: &VMSizeType{ + Family: "D", + Subfamily: nil, + Cpus: "3", + CpusConstrained: nil, + AdditiveFeatures: []rune{}, + AcceleratorType: nil, + ConfidentialChildCapability: false, + Version: "v2", + PromoVersion: true, + Series: "D_v2", + }, + err: nil, + }, + { + name: "Standard_inValid", + size: "inValid", + expectedVM: nil, + err: fmt.Errorf("could not parse VM size inValid"), + }, +} + +// Test_GetVMSize tests the GetVMSize() function. +func Test_GetVMSize(t *testing.T) { + a := assert.New(t) + for _, test := range testCases { + vmSize, err := GetVMSize(test.size) + a.Equal(test.err, err) + if err != nil { + continue + } + a.Equal(test.expectedVM.Family, vmSize.Family) + a.Equal(test.expectedVM.Subfamily, vmSize.Subfamily) + a.Equal(test.expectedVM.Cpus, vmSize.Cpus) + a.Equal(test.expectedVM.CpusConstrained, vmSize.CpusConstrained) + a.Equal(test.expectedVM.AdditiveFeatures, vmSize.AdditiveFeatures) + a.Equal(test.expectedVM.AcceleratorType, vmSize.AcceleratorType) + a.Equal(test.expectedVM.ConfidentialChildCapability, vmSize.ConfidentialChildCapability) + a.Equal(test.expectedVM.Version, vmSize.Version) + a.Equal(test.expectedVM.PromoVersion, vmSize.PromoVersion) + } +} diff --git a/v2/wrap.go b/v2/wrap.go new file mode 100644 index 0000000..c899580 --- /dev/null +++ b/v2/wrap.go @@ -0,0 +1,15 @@ +package skewer + +import "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + +// Wrap takes an array of compute resource skus and wraps them into an +// array of our richer type. +func Wrap(in []*armcompute.ResourceSKU) []SKU { + out := make([]SKU, len(in)) + for index, value := range in { + if value != nil { + out[index] = SKU(*value) + } + } + return out +} diff --git a/vmsize.go b/vmsize.go index 3d2d028..bcd1bec 100644 --- a/vmsize.go +++ b/vmsize.go @@ -49,7 +49,7 @@ type VMSizeType struct { // parseVMSize parses the VM size and returns the parts as a map. func parseVMSize(vmSizeName string) ([]string, error) { parts := skuSizeScheme.FindStringSubmatch(vmSizeName) - if len(parts) < 10 { + if parts == nil || len(parts) < 10 { return nil, fmt.Errorf("could not parse VM size %s", vmSizeName) } return parts, nil diff --git a/vmsize_test.go b/vmsize_test.go index 03e6bfd..5d711b5 100644 --- a/vmsize_test.go +++ b/vmsize_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/Azure/skewer/v2/testdata" + "github.com/Azure/skewer/testdata" "github.com/stretchr/testify/assert" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" diff --git a/wrap.go b/wrap.go index c899580..711555d 100644 --- a/wrap.go +++ b/wrap.go @@ -1,15 +1,13 @@ package skewer -import "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +import "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2022-08-01/compute" //nolint:staticcheck // Wrap takes an array of compute resource skus and wraps them into an // array of our richer type. -func Wrap(in []*armcompute.ResourceSKU) []SKU { +func Wrap(in []compute.ResourceSku) []SKU { out := make([]SKU, len(in)) for index, value := range in { - if value != nil { - out[index] = SKU(*value) - } + out[index] = SKU(value) } return out }