sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/resourceskus/cache.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package resourceskus
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
    28  	"github.com/pkg/errors"
    29  	"k8s.io/utils/ptr"
    30  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    31  	"sigs.k8s.io/cluster-api-provider-azure/util/cache/ttllru"
    32  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    33  )
    34  
    35  // Cache loads resource SKUs at the beginning of reconcile to expose
    36  // features available on compute resources. It exposes convenience
    37  // functionality for trawling Azure SKU capabilities. It may be adapted
    38  // to periodically refresh data in the background.
    39  type Cache struct {
    40  	client Client
    41  
    42  	// location is the Azure location for which this cache stores sku info.
    43  	// we do lookup once per reconcile for the given cluster/location.
    44  	location string
    45  
    46  	// data is the cached sku information from Azure.
    47  	// synchronization required if data is cached across reconcile calls, (i.e., refreshed in background as Runnable via mgr.Add(...))
    48  	data []armcompute.ResourceSKU
    49  }
    50  
    51  // Cacher describes the ability to get and to add items to cache.
    52  type Cacher interface {
    53  	Get(key interface{}) (value interface{}, ok bool)
    54  	Add(key interface{}, value interface{}) bool
    55  }
    56  
    57  // NewCacheFunc allows for mocking out the underlying client.
    58  type NewCacheFunc func(azure.Authorizer, string) *Cache
    59  
    60  var (
    61  	_           Client = &AzureClient{}
    62  	doOnce      sync.Once
    63  	clientCache Cacher
    64  )
    65  
    66  // newCache instantiates a cache and initializes its contents.
    67  func newCache(auth azure.Authorizer, location string) (*Cache, error) {
    68  	cli, err := NewClient(auth)
    69  	if err != nil {
    70  		return nil, errors.Wrap(err, "failed to create resourceskus client")
    71  	}
    72  	return &Cache{
    73  		client:   cli,
    74  		location: location,
    75  	}, nil
    76  }
    77  
    78  // GetCache either creates a new SKUs cache or returns an existing one based on the location + Authorizer HashKey().
    79  func GetCache(auth azure.Authorizer, location string) (*Cache, error) {
    80  	var err error
    81  	doOnce.Do(func() {
    82  		clientCache, err = ttllru.New(128, 24*time.Hour)
    83  	})
    84  
    85  	if err != nil {
    86  		return nil, errors.Wrap(err, "failed creating LRU cache for resourceSKUs cache")
    87  	}
    88  
    89  	key := location + "_" + auth.HashKey()
    90  	c, ok := clientCache.Get(key)
    91  	if ok {
    92  		return c.(*Cache), nil
    93  	}
    94  
    95  	c, err = newCache(auth, location)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	_ = clientCache.Add(key, c)
   100  	return c.(*Cache), nil
   101  }
   102  
   103  // NewStaticCache initializes a cache with data and no ability to refresh. Used for testing.
   104  func NewStaticCache(data []armcompute.ResourceSKU, location string) *Cache {
   105  	return &Cache{
   106  		data:     data,
   107  		location: location,
   108  	}
   109  }
   110  
   111  func (c *Cache) refresh(ctx context.Context, location string) error {
   112  	ctx, _, done := tele.StartSpanWithLogger(ctx, "resourceskus.Cache.refresh")
   113  	defer done()
   114  
   115  	data, err := c.client.List(ctx, fmt.Sprintf("location eq '%s'", location))
   116  	if err != nil {
   117  		return errors.Wrap(err, "failed to refresh resource sku cache")
   118  	}
   119  
   120  	c.data = data
   121  
   122  	return nil
   123  }
   124  
   125  // Get returns a resource SKU with the provided name and category. It
   126  // returns an error if we could not find a match. We should consider
   127  // enhancing this function to handle restrictions (e.g. SKU not
   128  // supported in region), which is why it returns an error and not a
   129  // boolean.
   130  func (c *Cache) Get(ctx context.Context, name string, kind ResourceType) (SKU, error) {
   131  	ctx, _, done := tele.StartSpanWithLogger(ctx, "resourceskus.Cache.Get")
   132  	defer done()
   133  
   134  	if c.data == nil {
   135  		if err := c.refresh(ctx, c.location); err != nil {
   136  			return SKU{}, err
   137  		}
   138  	}
   139  
   140  	for _, sku := range c.data {
   141  		if sku.Name != nil && *sku.Name == name {
   142  			return SKU(sku), nil
   143  		}
   144  	}
   145  	return SKU{}, azure.WithTerminalError(fmt.Errorf("resource sku with name '%s' and category '%s' not found in location '%s'", name, string(kind), c.location))
   146  }
   147  
   148  // Map invokes a function over all cached values.
   149  func (c *Cache) Map(ctx context.Context, mapFn func(sku SKU)) error {
   150  	ctx, _, done := tele.StartSpanWithLogger(ctx, "resourceskus.Cache.Map")
   151  	defer done()
   152  
   153  	if c.data == nil {
   154  		if err := c.refresh(ctx, c.location); err != nil {
   155  			return err
   156  		}
   157  	}
   158  
   159  	for i := range c.data {
   160  		val := SKU(c.data[i])
   161  		mapFn(val)
   162  	}
   163  
   164  	return nil
   165  }
   166  
   167  // GetZones looks at all virtual machine sizes and returns the unique
   168  // set of zones into which some machine size may deploy. It removes
   169  // restricted virtual machine sizes and duplicates.
   170  func (c *Cache) GetZones(ctx context.Context, location string) ([]string, error) {
   171  	ctx, _, done := tele.StartSpanWithLogger(ctx, "resourceskus.Cache.GetZones")
   172  	defer done()
   173  
   174  	var allZones = make(map[string]bool)
   175  	mapFn := func(sku SKU) {
   176  		// Look for VMs only
   177  		if sku.ResourceType != nil && strings.EqualFold(*sku.ResourceType, string(VirtualMachines)) {
   178  			// find matching location
   179  			for _, locationInfo := range sku.LocationInfo {
   180  				if !strings.EqualFold(*locationInfo.Location, location) {
   181  					continue
   182  				}
   183  				// Use map for easy deletion and iteration
   184  				availableZones := make(map[string]bool)
   185  
   186  				// add all zones
   187  				for _, zone := range locationInfo.Zones {
   188  					availableZones[*zone] = true
   189  				}
   190  
   191  				if sku.Restrictions != nil {
   192  					for _, restriction := range sku.Restrictions {
   193  						// Can't deploy anything in this subscription in this location. Bail out.
   194  						if ptr.Deref(restriction.Type, "") == armcompute.ResourceSKURestrictionsTypeLocation {
   195  							availableZones = nil
   196  							break
   197  						}
   198  
   199  						// remove restricted zones
   200  						for _, restrictedZone := range restriction.RestrictionInfo.Zones {
   201  							delete(availableZones, *restrictedZone)
   202  						}
   203  					}
   204  				}
   205  
   206  				// add to global list, if any exist. it's okay for the final list to be empty.
   207  				// that means the region may not support AZ yet.
   208  				for zone := range availableZones {
   209  					allZones[zone] = true
   210  				}
   211  
   212  				break
   213  			}
   214  		}
   215  	}
   216  
   217  	if err := c.Map(ctx, mapFn); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	var zones = make([]string, 0, len(allZones))
   222  	for zone := range allZones {
   223  		zones = append(zones, zone)
   224  	}
   225  
   226  	// lexical sort for testing
   227  	sort.Strings(zones)
   228  
   229  	return zones, nil
   230  }
   231  
   232  // GetZonesWithVMSize returns available zones for a virtual machine size in the given location.
   233  func (c *Cache) GetZonesWithVMSize(ctx context.Context, size, location string) ([]string, error) {
   234  	ctx, _, done := tele.StartSpanWithLogger(ctx, "resourceskus.Cache.GetZonesWithVMSize")
   235  	defer done()
   236  
   237  	var allZones = make(map[string]bool)
   238  	mapFn := func(sku SKU) {
   239  		if sku.Name != nil && strings.EqualFold(*sku.Name, size) && sku.ResourceType != nil && strings.EqualFold(*sku.ResourceType, string(VirtualMachines)) {
   240  			// find matching location
   241  			for _, locationInfo := range sku.LocationInfo {
   242  				if !strings.EqualFold(*locationInfo.Location, location) {
   243  					continue
   244  				}
   245  				// Use map for easy deletion and iteration
   246  				availableZones := make(map[string]bool)
   247  
   248  				// add all zones
   249  				for _, zone := range locationInfo.Zones {
   250  					availableZones[*zone] = true
   251  				}
   252  
   253  				if sku.Restrictions != nil {
   254  					for _, restriction := range sku.Restrictions {
   255  						// Can't deploy anything in this subscription in this location. Bail out.
   256  						if ptr.Deref(restriction.Type, "") == armcompute.ResourceSKURestrictionsTypeLocation {
   257  							availableZones = nil
   258  							break
   259  						}
   260  
   261  						// remove restricted zones
   262  						for _, restrictedZone := range restriction.RestrictionInfo.Zones {
   263  							delete(availableZones, *restrictedZone)
   264  						}
   265  					}
   266  				}
   267  
   268  				// add to global list, if any exist. it's okay for the final list to be empty.
   269  				// that means the region may not support AZ yet.
   270  				for zone := range availableZones {
   271  					allZones[zone] = true
   272  				}
   273  
   274  				break
   275  			}
   276  		}
   277  	}
   278  
   279  	if err := c.Map(ctx, mapFn); err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	var zones = make([]string, 0, len(allZones))
   284  	for zone := range allZones {
   285  		zones = append(zones, zone)
   286  	}
   287  
   288  	// lexical sort for testing
   289  	sort.Strings(zones)
   290  
   291  	return zones, nil
   292  }