|
@@ -46,9 +46,10 @@ func (g *GCPPricingSource) GetPricing() (*pricing.PricingSet, error) {
|
|
|
PersistentVolumePricing: []*pricing.PersistentVolumePricing{},
|
|
PersistentVolumePricing: []*pricing.PersistentVolumePricing{},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Maps to accumulate CPU and RAM costs per node key
|
|
|
|
|
|
|
+ // Maps to accumulate CPU, RAM, and per-GPU costs.
|
|
|
nodeCPUCosts := make(map[nodeKey]float64)
|
|
nodeCPUCosts := make(map[nodeKey]float64)
|
|
|
nodeRAMCosts := make(map[nodeKey]float64)
|
|
nodeRAMCosts := make(map[nodeKey]float64)
|
|
|
|
|
+ nodeGPUCosts := make(map[gpuKey]float64)
|
|
|
|
|
|
|
|
// Track volume pricing
|
|
// Track volume pricing
|
|
|
volumeCosts := make(map[volumeKey]float64)
|
|
volumeCosts := make(map[volumeKey]float64)
|
|
@@ -73,7 +74,7 @@ func (g *GCPPricingSource) GetPricing() (*pricing.PricingSet, error) {
|
|
|
return nil, fmt.Errorf("PricingSource (GCP): unexpected status %d on page %d: %s", resp.StatusCode, pageCount, string(body))
|
|
return nil, fmt.Errorf("PricingSource (GCP): unexpected status %d on page %d: %s", resp.StatusCode, pageCount, string(body))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- nextToken, err := g.parsePage(resp.Body, nodeCPUCosts, nodeRAMCosts, volumeCosts)
|
|
|
|
|
|
|
+ nextToken, err := g.parsePage(resp.Body, nodeCPUCosts, nodeRAMCosts, nodeGPUCosts, volumeCosts)
|
|
|
closeErr := resp.Body.Close()
|
|
closeErr := resp.Body.Close()
|
|
|
if closeErr != nil {
|
|
if closeErr != nil {
|
|
|
log.Warnf("failed to close response body: %v", closeErr)
|
|
log.Warnf("failed to close response body: %v", closeErr)
|
|
@@ -91,8 +92,8 @@ func (g *GCPPricingSource) GetPricing() (*pricing.PricingSet, error) {
|
|
|
nextPageToken = nextToken
|
|
nextPageToken = nextToken
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Build node pricing from accumulated CPU and RAM costs
|
|
|
|
|
- g.buildNodePricing(ps, nodeCPUCosts, nodeRAMCosts)
|
|
|
|
|
|
|
+ // Build node pricing from accumulated CPU and RAM costs and GPU-qualified copies
|
|
|
|
|
+ g.buildNodePricing(ps, nodeCPUCosts, nodeRAMCosts, nodeGPUCosts)
|
|
|
|
|
|
|
|
// Build volume pricing
|
|
// Build volume pricing
|
|
|
g.buildVolumePricing(ps, volumeCosts)
|
|
g.buildVolumePricing(ps, volumeCosts)
|
|
@@ -114,7 +115,7 @@ func (g *GCPPricingSource) buildURL(pageToken string) string {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (g *GCPPricingSource) parsePage(body io.Reader, nodeCPUCosts map[nodeKey]float64, nodeRAMCosts map[nodeKey]float64,
|
|
func (g *GCPPricingSource) parsePage(body io.Reader, nodeCPUCosts map[nodeKey]float64, nodeRAMCosts map[nodeKey]float64,
|
|
|
- volumeCosts map[volumeKey]float64,
|
|
|
|
|
|
|
+ nodeGPUCosts map[gpuKey]float64, volumeCosts map[volumeKey]float64,
|
|
|
) (nextPageToken string, err error) {
|
|
) (nextPageToken string, err error) {
|
|
|
|
|
|
|
|
data, err := io.ReadAll(body)
|
|
data, err := io.ReadAll(body)
|
|
@@ -132,6 +133,10 @@ func (g *GCPPricingSource) parsePage(body io.Reader, nodeCPUCosts map[nodeKey]fl
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if isCommitmentOrReservedSKU(sku.Description) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
category := sku.Category
|
|
category := sku.Category
|
|
|
resourceGroup := category.ResourceGroup
|
|
resourceGroup := category.ResourceGroup
|
|
|
usageType := strings.ToLower(category.UsageType)
|
|
usageType := strings.ToLower(category.UsageType)
|
|
@@ -146,7 +151,9 @@ func (g *GCPPricingSource) parsePage(body io.Reader, nodeCPUCosts map[nodeKey]fl
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // TODO: Add GPU pricing support
|
|
|
|
|
|
|
+ if isGPUResource(resourceGroup) {
|
|
|
|
|
+ g.parseGPUSKU(sku, usageType, nodeGPUCosts)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return page.NextPageToken, nil
|
|
return page.NextPageToken, nil
|
|
@@ -216,6 +223,34 @@ func (g *GCPPricingSource) parseComputeSKU(sku *GCPPricing, usageType string, no
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// parseGPUSKU accumulates an hourly price for one GPU. The product label is
|
|
|
|
|
+// part of the key because GPU SKUs are not tied to a single machine type.
|
|
|
|
|
+func (g *GCPPricingSource) parseGPUSKU(sku *GCPPricing, usageType string, nodeGPUCosts map[gpuKey]float64) {
|
|
|
|
|
+ if nodeGPUCosts == nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ product := normalizeGPUProduct(sku.Description)
|
|
|
|
|
+ if product == "" {
|
|
|
|
|
+ log.Debugf("PricingSource (GCP): skipping GPU SKU with unrecognized product label: %q", sku.Description)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hourlyPrice, err := g.extractHourlyPrice(sku)
|
|
|
|
|
+ if err != nil || hourlyPrice == 0 {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for _, region := range sku.ServiceRegions {
|
|
|
|
|
+ key := gpuKey{
|
|
|
|
|
+ Region: region,
|
|
|
|
|
+ Product: product,
|
|
|
|
|
+ UsageType: usageType,
|
|
|
|
|
+ }
|
|
|
|
|
+ nodeGPUCosts[key] = hourlyPrice
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// expandInstanceTypes handles special cases like E2 and A2 families that map to multiple instance types
|
|
// expandInstanceTypes handles special cases like E2 and A2 families that map to multiple instance types
|
|
|
func (g *GCPPricingSource) expandInstanceTypes(instanceType, resourceGroup string) []string {
|
|
func (g *GCPPricingSource) expandInstanceTypes(instanceType, resourceGroup string) []string {
|
|
|
resourceGroupLower := strings.ToLower(resourceGroup)
|
|
resourceGroupLower := strings.ToLower(resourceGroup)
|
|
@@ -262,8 +297,9 @@ func (g *GCPPricingSource) extractHourlyPrice(sku *GCPPricing) (float64, error)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (g *GCPPricingSource) buildNodePricing(ps *pricing.PricingSet, nodeCPUCosts map[nodeKey]float64,
|
|
func (g *GCPPricingSource) buildNodePricing(ps *pricing.PricingSet, nodeCPUCosts map[nodeKey]float64,
|
|
|
- nodeRAMCosts map[nodeKey]float64,
|
|
|
|
|
|
|
+ nodeRAMCosts map[nodeKey]float64, nodeGPUCosts map[gpuKey]float64,
|
|
|
) {
|
|
) {
|
|
|
|
|
+
|
|
|
// Combine CPU and RAM costs into complete node pricing
|
|
// Combine CPU and RAM costs into complete node pricing
|
|
|
processedKeys := make(map[nodeKey]bool)
|
|
processedKeys := make(map[nodeKey]bool)
|
|
|
|
|
|
|
@@ -318,6 +354,30 @@ func (g *GCPPricingSource) buildNodePricing(ps *pricing.PricingSet, nodeCPUCosts
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ps.NodePricing = append(ps.NodePricing, nodePricing)
|
|
ps.NodePricing = append(ps.NodePricing, nodePricing)
|
|
|
|
|
+
|
|
|
|
|
+ // A GPU-qualified record must repeat CPU/RAM pricing
|
|
|
|
|
+ for gpuKey, gpuCost := range nodeGPUCosts {
|
|
|
|
|
+ if gpuKey.Region != key.Region || gpuKey.UsageType != key.UsageType {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ gpuNodePricing := &pricing.NodePricing{
|
|
|
|
|
+ Properties: nodePricing.Properties,
|
|
|
|
|
+ Prices: pricing.Prices{
|
|
|
|
|
+ pricing.ResourceCPU: nodePricing.Prices[pricing.ResourceCPU],
|
|
|
|
|
+ pricing.ResourceRAM: nodePricing.Prices[pricing.ResourceRAM],
|
|
|
|
|
+ pricing.ResourceGPU: {
|
|
|
|
|
+ Unit: unit.GPUHour,
|
|
|
|
|
+ Price: gpuCost,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ gpuNodePricing.Properties.Labels = map[string]string{
|
|
|
|
|
+ gpuProductLabel: gpuKey.Product,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ps.NodePricing = append(ps.NodePricing, gpuNodePricing)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|