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 }