-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathinstancetype.go
More file actions
438 lines (382 loc) · 14.1 KB
/
Copy pathinstancetype.go
File metadata and controls
438 lines (382 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
package v1
import (
"context"
"errors"
"fmt"
"reflect"
"slices"
"strings"
"time"
"github.com/alecthomas/units"
"github.com/bojanz/currency"
"github.com/google/go-cmp/cmp"
)
type Manufacturer string
const (
ManufacturerNVIDIA Manufacturer = "NVIDIA"
ManufacturerIntel Manufacturer = "Intel"
ManufacturerUnknown Manufacturer = "unknown"
)
func GetManufacturer(manufacturer string) Manufacturer {
switch strings.ToLower(manufacturer) {
case "nvidia":
return ManufacturerNVIDIA
case "intel":
return ManufacturerIntel
default:
return ManufacturerUnknown
}
}
type Architecture string
const (
ArchitectureX86_64 Architecture = "x86_64"
ArchitectureARM64 Architecture = "arm64"
ArchitectureUnknown Architecture = "unknown"
)
func GetArchitecture(architecture string) Architecture {
switch strings.ToLower(architecture) {
case "x86_64":
return ArchitectureX86_64
case "arm64":
return ArchitectureARM64
default:
return ArchitectureUnknown
}
}
type InstanceTypeID string
type InstanceType struct {
ID InstanceTypeID // this id should be unique across all regions and stable
Location string
AvailableAzs []string
SubLocation string
Type string
SupportedGPUs []GPU
SupportedStorage []Storage
ElasticRootVolume bool
SupportedUsageClasses []string
Memory units.Base2Bytes
MaximumNetworkInterfaces int32
NetworkPerformance string
SupportedNumCores []int32
DefaultCores int32
VCPU int32
SupportedArchitectures []Architecture
ClockSpeedInGhz float64
Quota InstanceTypeQuota
Stoppable bool
Rebootable bool
VariablePrice bool
Preemptible bool
IsAvailable bool
BasePrice *currency.Amount
SubLocationTypeChangeable bool
IsContainer bool
UserPrivilegeEscalationDisabled bool
NotPrivileged bool
EstimatedDeployTime *time.Duration
Provider string
Cloud string
CanModifyFirewallRules bool
}
func MakeGenericInstanceTypeID(instanceType InstanceType) InstanceTypeID {
if instanceType.ID != "" {
return instanceType.ID
}
subLoc := noSubLocation
if len(instanceType.AvailableAzs) > 0 {
subLoc = instanceType.AvailableAzs[0]
}
return InstanceTypeID(fmt.Sprintf("%s-%s-%s", instanceType.Location, subLoc, instanceType.Type))
}
func MakeGenericInstanceTypeIDFromInstance(instance Instance) InstanceTypeID {
if instance.InstanceTypeID != "" {
return instance.InstanceTypeID
}
subLoc := noSubLocation
if instance.SubLocation != "" {
subLoc = instance.SubLocation
}
return InstanceTypeID(fmt.Sprintf("%s-%s-%s", instance.Location, subLoc, instance.InstanceType))
}
type GPU struct {
Count int32
Memory units.Base2Bytes
MemoryDetails string // "", "HBM", "GDDR", "DDR", etc.
NetworkDetails string // "PCIe", "SXM4", "SXM5", etc.
Manufacturer Manufacturer
Name string
Type string
}
type InstanceTypeQuota struct {
OnDemand Quota
Spot Quota
Reserved Quota
}
type CloudInstanceType interface {
GetInstanceTypes(ctx context.Context, args GetInstanceTypeArgs) ([]InstanceType, error)
GetInstanceTypePollTime() time.Duration
CloudLocation
}
type GetInstanceTypeArgs struct {
Locations LocationsFilter
InstanceTypes []string
GPUManufactererFilter *GPUManufacturerFilter // nil means all GPU manufacturers are allowed
CloudFilter *CloudFilter // nil means all clouds are allowed
ArchitectureFilter *ArchitectureFilter // nil means all architectures are allowed
}
type GPUManufacturerFilter struct {
// If IncludeGPUManufacturers is provided, only the GPU manufacturers in the list will be included
IncludeGPUManufacturers []Manufacturer
// If ExcludeGPUManufacturers is provided, the GPU manufacturers in the list will be excluded
ExcludeGPUManufacturers []Manufacturer
}
func (f *GPUManufacturerFilter) IsAllowed(manufacturer Manufacturer) bool {
if f.IncludeGPUManufacturers != nil && !slices.Contains(f.IncludeGPUManufacturers, manufacturer) {
return false
}
if f.ExcludeGPUManufacturers != nil && slices.Contains(f.ExcludeGPUManufacturers, manufacturer) {
return false
}
return true
}
// CloudFilter allows for filtering of instance types by cloud.
type CloudFilter struct {
// If IncludeClouds is provided, only the clouds in the list will be included
IncludeClouds []string
// If ExcludeClouds is provided, the clouds in the list will be excluded
ExcludeClouds []string
}
func (f *CloudFilter) IsAllowed(cloud string) bool {
if f.IncludeClouds != nil && !slices.Contains(f.IncludeClouds, cloud) {
return false
}
if f.ExcludeClouds != nil && slices.Contains(f.ExcludeClouds, cloud) {
return false
}
return true
}
// ArchitectureFilter allows for filtering of instance types by architecture.
type ArchitectureFilter struct {
// If IncludeArchitectures is provided, only the architectures in the list will be included
IncludeArchitectures []Architecture
// If ExcludeArchitectures is provided, the architectures in the list will be excluded
ExcludeArchitectures []Architecture
}
func (f *ArchitectureFilter) IsAllowed(architecture Architecture) bool {
if f.IncludeArchitectures != nil && !slices.Contains(f.IncludeArchitectures, architecture) {
return false
}
if f.ExcludeArchitectures != nil && slices.Contains(f.ExcludeArchitectures, architecture) {
return false
}
return true
}
// ValidateGetInstanceTypes validates that the GetInstanceTypes functionality works correctly
// by testing that filtering by specific instance types returns the expected results
func ValidateGetInstanceTypes(ctx context.Context, client CloudInstanceType) error { //nolint:funlen,gocyclo // todo refactor
// Get all instance types first
allTypes, err := client.GetInstanceTypes(ctx, GetInstanceTypeArgs{})
if err != nil {
return fmt.Errorf("failed to get all instance types: %w", err)
}
if len(allTypes) == 0 {
return errors.New("no instance types available for validation")
}
// Test 1: Deterministic results - multiple calls should return the same results (order-insensitive)
allTypes2, err := client.GetInstanceTypes(ctx, GetInstanceTypeArgs{})
if err != nil {
return fmt.Errorf("failed to get all instance types on second call: %w", err)
}
// Remove volatile fields for comparison
normalizedTypes1 := normalizeInstanceTypes(allTypes)
normalizedTypes2 := normalizeInstanceTypes(allTypes2)
// Build maps keyed by ID for order-insensitive comparison
map1 := make(map[InstanceTypeID]InstanceType)
for _, t := range normalizedTypes1 {
map1[t.ID] = t
}
map2 := make(map[InstanceTypeID]InstanceType)
for _, t := range normalizedTypes2 {
map2[t.ID] = t
}
// Compare keys
if len(map1) != len(map2) {
return fmt.Errorf("instance types are not deterministic between calls: different number of types (%d vs %d)", len(map1), len(map2))
}
for id, t1 := range map1 {
t2, ok := map2[id]
if !ok {
return fmt.Errorf("instance type ID %s present in first call but missing in second", id)
}
if !reflect.DeepEqual(t1, t2) {
diff := cmp.Diff(t1, t2)
fmt.Printf("Instance type with ID %s differs between calls. Diff:\n%s\n", id, diff)
return fmt.Errorf("instance type with ID %s differs between calls", id)
}
}
// Test 2: ID stability and uniqueness
idMap := make(map[InstanceTypeID]InstanceType)
for _, instanceType := range allTypes {
if existing, exists := idMap[instanceType.ID]; exists {
return fmt.Errorf("duplicate instance type ID found: %s (types: %s, %s)",
instanceType.ID, existing.Type, instanceType.Type)
}
idMap[instanceType.ID] = instanceType
}
// Test 3: Filtering by instance type name
firstType := allTypes[0]
filteredTypes, err := client.GetInstanceTypes(ctx, GetInstanceTypeArgs{
InstanceTypes: []string{firstType.Type},
})
if err != nil {
return fmt.Errorf("failed to get filtered instance types: %w", err)
}
if len(filteredTypes) == 0 {
return fmt.Errorf("no instance types returned when filtering by type: %s", firstType.Type)
}
// Compare the first type with the filtered result, ignoring fields that may vary
// between different calls or implementations
expectedType := firstType
expectedType.ID = ""
expectedType.SubLocation = ""
expectedType.AvailableAzs = nil
// Find the matching type in filteredTypes by ID (since order is not guaranteed)
var actualType InstanceType
found := false
for _, t := range filteredTypes {
tmp := t
tmp.ID = ""
tmp.SubLocation = ""
tmp.AvailableAzs = nil
if reflect.DeepEqual(expectedType, tmp) {
actualType = tmp
found = true
break
}
}
if !found {
// If not found by struct equality, just compare the first filtered type for debugging
actualType = filteredTypes[0]
actualType.ID = ""
actualType.SubLocation = ""
actualType.AvailableAzs = nil
diff := cmp.Diff(expectedType, actualType)
fmt.Printf("Filtered instance type does not match expected type. Diff:\n%s\n", diff)
return fmt.Errorf("filtered instance type does not match expected type: expected %+v, got %+v", expectedType, actualType)
}
return nil
}
// ValidateLocationalInstanceTypes validates that locational filtering works correctly
// by comparing locational results with all-location results using CloudLocation capabilities
func ValidateLocationalInstanceTypes(ctx context.Context, client CloudInstanceType) error {
// Get all-location instance types by requesting from all locations
allLocationTypes, err := client.GetInstanceTypes(ctx, GetInstanceTypeArgs{
Locations: All,
})
if err != nil {
// If all-location is not supported, skip this validation
return fmt.Errorf("all-location instance types not supported: %w", err)
}
if len(allLocationTypes) == 0 {
return errors.New("no all-location instance types available for validation")
}
locationToTest := allLocationTypes[0].Location
// Get locational instance types (default behavior - typically current location)
locationalTypes, err := client.GetInstanceTypes(ctx, GetInstanceTypeArgs{
Locations: LocationsFilter{locationToTest},
})
if err != nil {
return fmt.Errorf("failed to get locational instance types: %w", err)
}
if len(locationalTypes) == 0 {
return errors.New("no locational instance types available for validation")
}
// Validate that locational results are a subset of all-location results
if len(locationalTypes) >= len(allLocationTypes) {
return fmt.Errorf("locational instance types (%d) should be fewer than all-location types (%d)",
len(locationalTypes), len(allLocationTypes))
}
// Create a map of all-location types for efficient lookup
allLocationMap := make(map[InstanceTypeID]InstanceType)
for _, instanceType := range allLocationTypes {
allLocationMap[instanceType.ID] = instanceType
}
// Validate that all locational types exist in all-location results
for _, locationalType := range locationalTypes {
if _, exists := allLocationMap[locationalType.ID]; !exists {
return fmt.Errorf("locational instance type %s not found in all-location results", locationalType.ID)
}
}
// Additional validation: ensure locational types have appropriate location information
for _, locationalType := range locationalTypes {
if locationalType.Location == "" {
return fmt.Errorf("locational instance type %s should have location information", locationalType.ID)
}
}
return nil
}
// normalizeInstanceTypes removes volatile fields that may change between calls
func normalizeInstanceTypes(types []InstanceType) []InstanceType {
normalized := make([]InstanceType, len(types))
for i, instanceType := range types {
normalized[i] = instanceType
// Remove fields that may vary between calls
normalized[i].BasePrice = nil
normalized[i].Quota = InstanceTypeQuota{}
normalized[i].EstimatedDeployTime = nil
}
return normalized
}
// ValidateStableInstanceTypeIDs validates that the provided stable instance type IDs are valid and stable
// This function ensures that stable IDs exist in the current instance types and have required properties
func ValidateStableInstanceTypeIDs(ctx context.Context, client CloudInstanceType, stableIDs []InstanceTypeID) error {
// Get all instance types
allTypes, err := client.GetInstanceTypes(ctx, GetInstanceTypeArgs{})
if err != nil {
return fmt.Errorf("failed to get instance types: %w", err)
}
if len(allTypes) == 0 {
return errors.New("no instance types available for validation")
}
// Group types by ID for efficient lookup
typesByID := make(map[InstanceTypeID][]InstanceType)
for _, instanceType := range allTypes {
typesByID[instanceType.ID] = append(typesByID[instanceType.ID], instanceType)
}
// Validate that each ID has exactly one instance type (uniqueness)
for id, types := range typesByID {
if len(types) != 1 {
return fmt.Errorf("instance type id %s should be unique, found %d instances", id, len(types))
}
}
// Validate that stable IDs are not empty
if len(stableIDs) == 0 {
return errors.New("stable IDs list cannot be empty")
}
// Validate that all stable IDs exist in current instance types, collecting all errors
var errs []error
for _, stableID := range stableIDs {
if _, exists := typesByID[stableID]; !exists {
errs = append(errs, fmt.Errorf("instance type id %s should be stable but not found", stableID)) // if this fails, we may need to coordinate a migration of the stable ID
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
// Validate that all instance types have required properties
for _, instanceType := range allTypes {
// Check that instance type has base price
if instanceType.BasePrice == nil {
return fmt.Errorf("instance type %s should have base price", instanceType.ID)
}
// Check that supported storage has price information
for i, storage := range instanceType.SupportedStorage {
if storage.MinSize != nil {
if storage.PricePerGBHr == nil {
return fmt.Errorf("instance type %s should have storage %d price", instanceType.ID, i)
}
}
}
}
return nil
}