From cf507ebcee99d40136b53dbed2d444b5d0606bb1 Mon Sep 17 00:00:00 2001 From: Alexander Eldeib Date: Thu, 28 Jul 2022 15:34:07 +0200 Subject: [PATCH] demo: capability matching on vm sizes --- const.go | 5 ++++ data_test.go | 11 ++++++++ fakes_test.go | 4 +-- go.mod | 18 ++++++++++--- go.sum | 6 ++++- match.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ sku.go | 10 +++---- sku_test.go | 1 + 8 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 match.go diff --git a/const.go b/const.go index 1be0005..7ff4b79 100644 --- a/const.go +++ b/const.go @@ -49,3 +49,8 @@ const ( // Generation 2. HyperVGeneration2 = "V2" ) + +const ( + base10 = 10 + base64 = 64 +) diff --git a/data_test.go b/data_test.go index d9bf854..66fd7e5 100644 --- a/data_test.go +++ b/data_test.go @@ -9,6 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/sanity-io/litter" ) var ( @@ -91,6 +92,16 @@ func Test_Data(t *testing.T) { t.Error(err) } t.Run("virtual machines", func(t *testing.T) { + t.Run("match capability logic", func(t *testing.T) { + sku, err := cache.Get(ctx, "standard_d4s_v3", VirtualMachines, "eastus") + if err != nil { + t.Errorf("expected to find virtual machine sku standard_d4s_v3") + } + + match := Match(ctx, cache, &sku, "eastus") + litter.Dump(match) + }) + t.Run("expect 377 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))) diff --git a/fakes_test.go b/fakes_test.go index af0ba4b..90f8762 100644 --- a/fakes_test.go +++ b/fakes_test.go @@ -3,7 +3,7 @@ package skewer import ( "context" "encoding/json" - "io/ioutil" + "os" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute" ) @@ -16,7 +16,7 @@ type dataWrapper struct { // 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 := ioutil.ReadFile(path) + data, err := os.ReadFile(path) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 580e894..3fddff6 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,24 @@ module github.com/Azure/skewer -go 1.13 +go 1.18 require ( github.com/Azure/azure-sdk-for-go v46.0.0+incompatible - github.com/Azure/go-autorest/autorest v0.11.4 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.2 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 - github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect github.com/google/go-cmp v0.5.1 github.com/pkg/errors v0.9.1 + github.com/sanity-io/litter v1.5.5 +) + +require ( + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.4 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.2 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.0 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect ) diff --git a/go.sum b/go.sum index 1449975..05e6f1c 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,7 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/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 v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -28,13 +29,16 @@ github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/match.go b/match.go new file mode 100644 index 0000000..edfb635 --- /dev/null +++ b/match.go @@ -0,0 +1,72 @@ +package skewer + +import ( + "context" + "fmt" +) + +func Match(ctx context.Context, cache *Cache, sku *SKU, location string) *SKU { + sizes := cache.List(ctx, ResourceTypeFilter(sku.GetResourceType()), LocationFilter(normalizeLocation(location))) + + capabilities := map[string]string{} + for _, capability := range *sku.Capabilities { + if capability.Name != nil { + if capability.Value != nil { + capabilities[*capability.Name] = *capability.Value + } else { + capabilities[*capability.Name] = "" + } + } + } + + for i := range sizes { + candidate := &sizes[i] + if candidate.GetName() == sku.GetName() { + continue + } + if allCapabilitiesMatch(candidate, capabilities) { + return candidate + } + } + + return nil +} + +func allCapabilitiesMatch(sku *SKU, capabilities map[string]string) bool { + matched := 0 + desired := len(capabilities) + for _, capability := range *sku.Capabilities { + if capability.Name != nil { + // TODO(ace): this is not actually accurate, really for each + // capability, we should decide whether you need subset, exact match, + // or numerically greater/less than. + if capabilitiesToIgnore[*capability.Name] { + continue + } + if capability.Value != nil { + // TODO(ace): this is far too strict and results in basically zero matches. + if capabilities[*capability.Name] != *capability.Value { + fmt.Printf("failed on capability %s=%s\n", *capability.Name, *capability.Value) + return false + } + matched++ + } else { + val, ok := capabilities[*capability.Name] + if !ok || val != "" { + fmt.Printf("failed on capability %s with no value\n", *capability.Name) + return false + } + matched++ + } + } + } + + if matched != desired { + fmt.Printf("failed to find all desired capabilities want %d got %d\n", desired, matched) + } + return matched == desired +} + +var capabilitiesToIgnore = map[string]bool{ + MaxResourceVolumeMB: true, +} diff --git a/sku.go b/sku.go index 167c330..987aeb9 100644 --- a/sku.go +++ b/sku.go @@ -94,7 +94,7 @@ func (s *SKU) IsUltraSSDAvailableInAvailabilityZone(zone string) bool { // IsUltraSSDAvailable returns true when a VM size has ultra SSD enabled // in at least 1 unrestricted zone. // -// Deprecated. Use either IsUltraSSDAvailableWithoutAvailabilityZone or IsUltraSSDAvailableInAvailabilityZone +// Deprecated: Use either IsUltraSSDAvailableWithoutAvailabilityZone or IsUltraSSDAvailableInAvailabilityZone func (s *SKU) IsUltraSSDAvailable() bool { return s.HasZonalCapability(UltraSSDAvailable) } @@ -128,7 +128,7 @@ func (s *SKU) GetCapabilityIntegerQuantity(name string) (int64, error) { for _, capability := range *s.Capabilities { if capability.Name != nil && *capability.Name == name { if capability.Value != nil { - intVal, err := strconv.ParseInt(*capability.Value, 10, 64) + intVal, err := strconv.ParseInt(*capability.Value, base10, base64) if err != nil { return -1, &ErrCapabilityValueParse{name, *capability.Value, err} } @@ -151,7 +151,7 @@ func (s *SKU) GetCapabilityFloatQuantity(name string) (float64, error) { for _, capability := range *s.Capabilities { if capability.Name != nil && *capability.Name == name { if capability.Value != nil { - intVal, err := strconv.ParseFloat(*capability.Value, 64) + intVal, err := strconv.ParseFloat(*capability.Value, base64) if err != nil { return -1, &ErrCapabilityValueParse{name, *capability.Value, err} } @@ -213,7 +213,7 @@ 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 string, zone string) bool { +func (s *SKU) HasCapabilityInZone(name, zone string) bool { if s.LocationInfo == nil { return false } @@ -281,7 +281,7 @@ func (s *SKU) HasCapabilityWithMinCapacity(name string, value int64) (bool, erro for _, capability := range *s.Capabilities { if capability.Name != nil && strings.EqualFold(*capability.Name, name) { if capability.Value != nil { - intVal, err := strconv.ParseInt(*capability.Value, 10, 64) + intVal, err := strconv.ParseInt(*capability.Value, base10, base64) if err != nil { return false, errors.Wrapf(err, "failed to parse string '%s' as int64", *capability.Value) } diff --git a/sku_test.go b/sku_test.go index edb2df4..085728f 100644 --- a/sku_test.go +++ b/sku_test.go @@ -396,6 +396,7 @@ func Test_SKU_GetLocation(t *testing.T) { func Test_SKU_AvailabilityZones(t *testing.T) {} +//nolint:funlen func Test_SKU_HasCapabilityInZone(t *testing.T) { cases := map[string]struct { sku compute.ResourceSku