Skip to content

Commit c5eca9e

Browse files
committed
Implement retrieval of prices from 3rd party source in a runtime
1 parent f7a6723 commit c5eca9e

File tree

2 files changed

+222
-31
lines changed

2 files changed

+222
-31
lines changed

pkg/providers/pricing/pricing.go

+125-9
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@ package pricing
1616

1717
import (
1818
"context"
19+
"encoding/csv"
1920
"encoding/json"
2021
"fmt"
22+
"io"
2123
"net/http"
2224
"os"
2325
"path/filepath"
2426
"runtime"
27+
"strconv"
2528
"sync"
29+
"time"
2630

2731
"k8s.io/klog/v2"
2832
)
2933

3034
const (
3135
initialOnDemandPricesFile = "initial-on-demand-prices.json"
36+
pricingCSVURL = "https://gcloud-compute.com/machine-types-regions.csv"
3237
)
3338

3439
type Provider interface {
@@ -46,12 +51,16 @@ type DefaultProvider struct {
4651
muSpot sync.RWMutex
4752
region string
4853
prices map[string]map[string]float64
54+
spotPrices map[string]map[string]float64
55+
lastUpdated time.Time
4956
}
5057

5158
func NewDefaultProvider(ctx context.Context, region string) *DefaultProvider {
5259
p := &DefaultProvider{
53-
region: region,
54-
prices: make(map[string]map[string]float64),
60+
region: region,
61+
prices: make(map[string]map[string]float64),
62+
spotPrices: make(map[string]map[string]float64),
63+
lastUpdated: time.Now(),
5564
}
5665

5766
if err := p.loadInitialPrices(); err != nil {
@@ -113,7 +122,6 @@ func (p *DefaultProvider) OnDemandPrice(instanceType string) (float64, bool) {
113122

114123
regionPrices, ok := p.prices[p.region]
115124
if !ok {
116-
fmt.Println(p.prices)
117125
return 0, false
118126
}
119127

@@ -122,16 +130,124 @@ func (p *DefaultProvider) OnDemandPrice(instanceType string) (float64, bool) {
122130
}
123131

124132
func (p *DefaultProvider) SpotPrice(instanceType string, zone string) (float64, bool) {
125-
// Currently, we don't have spot price information
126-
return 0, false
133+
p.muSpot.RLock()
134+
defer p.muSpot.RUnlock()
135+
136+
regionPrices, ok := p.spotPrices[p.region]
137+
if !ok {
138+
return 0, false
139+
}
140+
141+
price, ok := regionPrices[instanceType]
142+
return price, ok
127143
}
128144

129-
func (p *DefaultProvider) UpdateOnDemandPricing(ctx context.Context) error {
130-
// For now, we only use static pricing data
145+
type priceUpdateConfig struct {
146+
priceColumn string
147+
targetMap *map[string]map[string]float64
148+
mu *sync.RWMutex
149+
}
150+
151+
func (p *DefaultProvider) updatePricing(ctx context.Context, config priceUpdateConfig) error {
152+
config.mu.Lock()
153+
defer config.mu.Unlock()
154+
155+
// Download the CSV file
156+
resp, err := http.Get(pricingCSVURL)
157+
if err != nil {
158+
return fmt.Errorf("downloading CSV: %w", err)
159+
}
160+
defer resp.Body.Close()
161+
162+
// Read the CSV data
163+
reader := csv.NewReader(resp.Body)
164+
reader.Comma = ','
165+
166+
// Read header
167+
header, err := reader.Read()
168+
if err != nil {
169+
return fmt.Errorf("reading CSV header: %w", err)
170+
}
171+
172+
// Find the required column indices
173+
priceColIndex := -1
174+
regionColIndex := -1
175+
machineTypeColIndex := -1
176+
177+
for i, col := range header {
178+
switch col {
179+
case config.priceColumn:
180+
priceColIndex = i
181+
case "region":
182+
regionColIndex = i
183+
case "name":
184+
machineTypeColIndex = i
185+
}
186+
}
187+
188+
if priceColIndex == -1 || regionColIndex == -1 || machineTypeColIndex == -1 {
189+
return fmt.Errorf("could not find required columns in CSV")
190+
}
191+
192+
// Process the data
193+
newPrices := make(map[string]map[string]float64)
194+
195+
for {
196+
record, err := reader.Read()
197+
if err == io.EOF {
198+
break
199+
}
200+
if err != nil {
201+
klog.Errorf("Error reading CSV record: %v", err)
202+
continue
203+
}
204+
205+
// Get machine type, region and price
206+
machineType := record[machineTypeColIndex]
207+
region := record[regionColIndex]
208+
priceStr := record[priceColIndex]
209+
210+
// Parse price
211+
price, err := strconv.ParseFloat(priceStr, 64)
212+
if err != nil {
213+
klog.Errorf("Error parsing price for %s in region %s: %v", machineType, region, err)
214+
continue
215+
}
216+
217+
// Initialize region map if it doesn't exist
218+
if _, exists := newPrices[region]; !exists {
219+
newPrices[region] = make(map[string]float64)
220+
}
221+
222+
// Store the price
223+
newPrices[region][machineType] = price
224+
}
225+
226+
// Check if we got any prices
227+
if len(newPrices) == 0 {
228+
return fmt.Errorf("no prices found during price update for %s pricing", config.priceColumn)
229+
}
230+
231+
// Update the prices
232+
*config.targetMap = newPrices
233+
p.lastUpdated = time.Now()
234+
235+
klog.V(2).Infof("Updated prices for %d regions", len(newPrices))
131236
return nil
132237
}
133238

239+
func (p *DefaultProvider) UpdateOnDemandPricing(ctx context.Context) error {
240+
return p.updatePricing(ctx, priceUpdateConfig{
241+
priceColumn: "hour",
242+
targetMap: &p.prices,
243+
mu: &p.muOnDemand,
244+
})
245+
}
246+
134247
func (p *DefaultProvider) UpdateSpotPricing(ctx context.Context) error {
135-
// Currently, we don't have spot price information
136-
return nil
248+
return p.updatePricing(ctx, priceUpdateConfig{
249+
priceColumn: "hourSpot",
250+
targetMap: &p.spotPrices,
251+
mu: &p.muSpot,
252+
})
137253
}

pkg/providers/pricing/pricing_test.go

+97-22
Original file line numberDiff line numberDiff line change
@@ -23,49 +23,124 @@ import (
2323
"github.com/cloudpilot-ai/karpenter-provider-gcp/pkg/providers/pricing"
2424
)
2525

26-
func TestDefaultProvider_OnDemandPrice(t *testing.T) {
26+
var testInstanceTypes = []string{
27+
"e2-standard-32",
28+
"n2-standard-16",
29+
"c2-standard-8",
30+
"m1-ultramem-40",
31+
"a2-highgpu-1g",
32+
}
33+
34+
func TestPricingProvider_InitialPrices(t *testing.T) {
35+
// Initialize the provider with europe-west4 region
36+
provider := pricing.NewDefaultProvider(context.Background(), "europe-west4")
37+
38+
// Test getting prices for various instance types from initial file
39+
for _, instanceType := range testInstanceTypes {
40+
_, found := provider.OnDemandPrice(instanceType)
41+
if !found {
42+
t.Errorf("Failed to find initial on-demand price for %s", instanceType)
43+
continue
44+
}
45+
}
46+
47+
// Get all instance types from initial file
48+
types := provider.InstanceTypes()
49+
if len(types) == 0 {
50+
t.Fatal("No instance types found in initial prices")
51+
}
52+
53+
// Check if all test instance types are in the initial list
54+
for _, instanceType := range testInstanceTypes {
55+
found := false
56+
for _, t := range types {
57+
if t == instanceType {
58+
found = true
59+
break
60+
}
61+
}
62+
if !found {
63+
t.Errorf("%s not found in initial instance types list", instanceType)
64+
}
65+
}
66+
}
67+
68+
func TestPricingProvider_OnDemandPrice(t *testing.T) {
2769
// Initialize the provider with europe-west4 region
2870
provider := pricing.NewDefaultProvider(context.Background(), "europe-west4")
29-
provider.UpdateOnDemandPricing(context.Background())
3071

31-
// Test getting price for e2-standard-32
32-
price, found := provider.OnDemandPrice("e2-standard-32")
33-
if !found {
34-
t.Fatal("Failed to find price for e2-standard-32")
72+
// Test price update
73+
if err := provider.UpdateOnDemandPricing(context.Background()); err != nil {
74+
t.Fatalf("Failed to update on-demand pricing: %v", err)
3575
}
3676

37-
t.Logf("On-demand price for e2-standard-32 in europe-west4: $%.4f per hour", price)
77+
// Test getting prices for various instance types
78+
for _, instanceType := range testInstanceTypes {
79+
_, found := provider.OnDemandPrice(instanceType)
80+
if !found {
81+
t.Errorf("Failed to find on-demand price for %s", instanceType)
82+
continue
83+
}
84+
}
3885

3986
// Test getting price for a non-existent instance type
40-
_, found = provider.OnDemandPrice("non-existent-type")
87+
_, found := provider.OnDemandPrice("non-existent-type")
4188
if found {
4289
t.Error("Expected to not find price for non-existent instance type")
4390
}
4491
}
4592

46-
func TestDefaultProvider_InstanceTypes(t *testing.T) {
93+
func TestPricingProvider_SpotPrice(t *testing.T) {
4794
// Initialize the provider with europe-west4 region
4895
provider := pricing.NewDefaultProvider(context.Background(), "europe-west4")
49-
provider.UpdateOnDemandPricing(context.Background())
96+
97+
// Test price update
98+
if err := provider.UpdateSpotPricing(context.Background()); err != nil {
99+
t.Fatalf("Failed to update spot pricing: %v", err)
100+
}
101+
102+
// Test getting spot prices for various instance types
103+
for _, instanceType := range testInstanceTypes {
104+
_, found := provider.SpotPrice(instanceType, "europe-west4-a")
105+
if !found {
106+
t.Errorf("Failed to find spot price for %s", instanceType)
107+
continue
108+
}
109+
}
110+
111+
// Test getting price for a non-existent instance type
112+
_, found := provider.SpotPrice("non-existent-type", "europe-west4-a")
113+
if found {
114+
t.Error("Expected to not find price for non-existent instance type")
115+
}
116+
}
117+
118+
func TestPricingProvider_InstanceTypes(t *testing.T) {
119+
// Initialize the provider with europe-west4 region
120+
provider := pricing.NewDefaultProvider(context.Background(), "europe-west4")
121+
122+
// Test price update
123+
if err := provider.UpdateOnDemandPricing(context.Background()); err != nil {
124+
t.Fatalf("Failed to update on-demand pricing: %v", err)
125+
}
50126

51127
// Get all instance types
52128
types := provider.InstanceTypes()
53129
if len(types) == 0 {
54130
t.Fatal("No instance types found")
55131
}
56132

57-
t.Logf("Found %d instance types in europe-west4", len(types))
58-
59-
// Check if e2-standard-32 is in the list
60-
found := false
61-
for _, instanceType := range types {
62-
if instanceType == "e2-standard-32" {
63-
found = true
64-
break
133+
// Check if all test instance types are in the list
134+
for _, instanceType := range testInstanceTypes {
135+
found := false
136+
for _, t := range types {
137+
if t == instanceType {
138+
found = true
139+
break
140+
}
141+
}
142+
if !found {
143+
t.Errorf("%s not found in instance types list", instanceType)
65144
}
66-
}
67-
68-
if !found {
69-
t.Error("e2-standard-32 not found in instance types list")
70145
}
71146
}

0 commit comments

Comments
 (0)