Просмотр исходного кода

fix(oracle): normalize Karpenter OCI flex labels for node pricing (#3814)

Signed-off-by: Arindam Bandyopadhyay <arindam.bandyopadhyay@oracle.com>
Arindam Bandyopadhyay 8 часов назад
Родитель
Сommit
e825018baa

+ 41 - 0
pkg/cloud/oracle/product.go

@@ -64,6 +64,47 @@ type instanceProduct map[string]Product
 // instanceProducts maps instance types to associated part numbers.
 var instanceProducts instanceProduct
 
+// normalizeOCIInstanceShape parses synthetic flex shape labels in the form
+// <base-shape>.<ocpus>o.<memory>g.<baseline>b. Supported baselines are 1_1,
+// 1_2, and 1_8, which map to the burstable CPU price multipliers below.
+func normalizeOCIInstanceShape(shape string) (string, float64, bool) {
+	const defaultCPUPriceMultiplier = 1.0
+
+	parts := strings.Split(shape, ".")
+	if len(parts) < 4 {
+		return "", defaultCPUPriceMultiplier, false
+	}
+
+	ocpuPart := parts[len(parts)-3]
+	memoryPart := parts[len(parts)-2]
+	baselinePart := parts[len(parts)-1]
+
+	if !strings.HasSuffix(ocpuPart, "o") || !strings.HasSuffix(memoryPart, "g") || !strings.HasSuffix(baselinePart, "b") {
+		return "", defaultCPUPriceMultiplier, false
+	}
+	if _, err := strconv.ParseFloat(strings.TrimSuffix(ocpuPart, "o"), 64); err != nil {
+		return "", defaultCPUPriceMultiplier, false
+	}
+	if _, err := strconv.ParseFloat(strings.TrimSuffix(memoryPart, "g"), 64); err != nil {
+		return "", defaultCPUPriceMultiplier, false
+	}
+
+	var cpuPriceMultiplier float64
+	switch strings.TrimSuffix(baselinePart, "b") {
+	case "1_1":
+		cpuPriceMultiplier = 1.0
+	case "1_2":
+		cpuPriceMultiplier = 0.5
+	case "1_8":
+		cpuPriceMultiplier = 0.125
+	default:
+		return "", defaultCPUPriceMultiplier, false
+	}
+
+	baseShape := strings.Join(parts[:len(parts)-3], ".")
+	return baseShape, cpuPriceMultiplier, true
+}
+
 func (i instanceProduct) get(shape string) Product {
 	if product, ok := i[shape]; ok {
 		return product

+ 4 - 0
pkg/cloud/oracle/provider.go

@@ -21,6 +21,7 @@ const nodePoolIdAnnotation = "oci.oraclecloud.com/node-pool-id"
 const virtualPoolIdAnnotation = "oci.oraclecloud.com/virtual-node-pool-id"
 const virtualNodeLabel = "node-role.kubernetes.io/virtual-node"
 const preemptibleLabel = "oci.oraclecloud.com/oke-is-preemptible"
+const ociInstanceShapeLabel = "oci.oraclecloud.com/instance-shape"
 const managementPlatformOKE = "oke"
 const currencyCodeUSD = "USD"
 
@@ -147,6 +148,9 @@ func (o *Oracle) GetKey(labels map[string]string, n *clustercache.Node) models.K
 		gpuType = "nvidia.com/gpu"
 	}
 	instanceType, _ := util.GetInstanceType(labels)
+	if instanceType == "" {
+		instanceType = labels[ociInstanceShapeLabel]
+	}
 	return &oracleKey{
 		providerID:   n.SpecProviderID,
 		instanceType: instanceType,

+ 25 - 0
pkg/cloud/oracle/provider_test.go

@@ -54,6 +54,31 @@ func TestGetKey(t *testing.T) {
 	}
 }
 
+func TestGetKeyFallsBackToOCIInstanceShapeLabel(t *testing.T) {
+	labels := map[string]string{
+		ociInstanceShapeLabel: "VM.Standard.E3.Flex",
+	}
+
+	key := (&Oracle{}).GetKey(labels, testNode(0))
+	features := strings.Split(key.Features(), ",")
+
+	assert.Len(t, features, 3)
+	assert.Equal(t, "VM.Standard.E3.Flex", features[0])
+}
+
+func TestGetKeyPrefersKubernetesInstanceTypeLabel(t *testing.T) {
+	labels := map[string]string{
+		v1.LabelInstanceTypeStable: "VM.Standard.E3.Flex.2o.32g.1_1b",
+		ociInstanceShapeLabel:      "VM.Standard.E3.Flex",
+	}
+
+	key := (&Oracle{}).GetKey(labels, testNode(0))
+	features := strings.Split(key.Features(), ",")
+
+	assert.Len(t, features, 3)
+	assert.Equal(t, "VM.Standard.E3.Flex.2o.32g.1_1b", features[0])
+}
+
 func TestGetPVKey(t *testing.T) {
 	storageClass := "xyz"
 	providerID := "ocid.abc"

+ 12 - 2
pkg/cloud/oracle/ratecard.go

@@ -126,7 +126,17 @@ func (rcs *RateCardStore) ForPVK(pvk models.PVKey, defaultPricing DefaultPricing
 // ForKey retrieves costing metadata for a key.
 func (rcs *RateCardStore) ForKey(key models.Key, defaultPricing DefaultPricing) (*models.Node, models.PricingMetadata, error) {
 	features := strings.Split(key.Features(), ",")
-	product := instanceProducts.get(features[0])
+	shape := features[0]
+	cpuPriceMultiplier := 1.0
+	product := instanceProducts.get(shape)
+	if baseShape, multiplier, ok := normalizeOCIInstanceShape(shape); ok {
+		baseProduct := instanceProducts.get(baseShape)
+		if !baseProduct.isEmpty() {
+			shape = baseShape
+			cpuPriceMultiplier = multiplier
+			product = baseProduct
+		}
+	}
 	var node *models.Node
 	// Use the default pricing if the instance product is unknown
 	if product.isEmpty() {
@@ -151,7 +161,7 @@ func (rcs *RateCardStore) ForKey(key models.Key, defaultPricing DefaultPricing)
 			GPU:      defaultPricing.GPU,
 		}
 	} else {
-		ocpuPrice := rcs.prices[product.OCPU].UnitPrice
+		ocpuPrice := rcs.prices[product.OCPU].UnitPrice * cpuPriceMultiplier
 		if !isARMArch(features) {
 			// Non-ARM architectures have 2 VCPU per OCPU
 			ocpuPrice /= 2

+ 71 - 7
pkg/cloud/oracle/ratecard_test.go

@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestRCSForKey(t *testing.T) {
@@ -38,6 +39,10 @@ func TestRCSForKey(t *testing.T) {
 			"0.014000",
 			false,
 		},
+		"unknown-shape.2o.32g.1_2b": {
+			"0.600000",
+			false,
+		},
 		"unknown-shape": {
 			"0.600000",
 			false,
@@ -58,12 +63,63 @@ func TestRCSForKey(t *testing.T) {
 				Memory: "0.1",
 				GPU:    "0.3",
 			})
-			assert.NoError(t, err)
+			require.NoError(t, err)
 			assertFloatStrings(t, testCase.cost, node.Cost, 0.001)
 		})
 	}
 }
 
+func TestRCSForKey_KarpenterFlexShape(t *testing.T) {
+	rcs, server := testSetupRateCardStore(t)
+	defer server.Close()
+
+	defaultPricing := DefaultPricing{
+		OCPU:   "0.2",
+		Memory: "0.1",
+		GPU:    "0.3",
+	}
+
+	testCases := map[string]struct {
+		baseShape     string
+		flexShape     string
+		cpuMultiplier float64
+		assertCost    bool
+	}{
+		"baseline-1_1": {
+			baseShape:     "VM.Standard.E3.Flex",
+			flexShape:     "VM.Standard.E3.Flex.2o.32g.1_1b",
+			cpuMultiplier: 1,
+			assertCost:    true,
+		},
+		"baseline-1_2": {
+			baseShape:     "VM.Standard.E4.Flex",
+			flexShape:     "VM.Standard.E4.Flex.8o.32g.1_2b",
+			cpuMultiplier: 0.5,
+		},
+		"baseline-1_8": {
+			baseShape:     "VM.Standard.E4.Flex",
+			flexShape:     "VM.Standard.E4.Flex.8o.32g.1_8b",
+			cpuMultiplier: 0.125,
+		},
+	}
+
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			baseNode, _, err := rcs.ForKey(&oracleKey{instanceType: testCase.baseShape, labels: make(map[string]string)}, defaultPricing)
+			require.NoError(t, err)
+
+			flexNode, _, err := rcs.ForKey(&oracleKey{instanceType: testCase.flexShape, labels: make(map[string]string)}, defaultPricing)
+			require.NoError(t, err)
+
+			assertFloatStrings(t, baseNode.RAMCost, flexNode.RAMCost, 0.000001)
+			assert.InDelta(t, mustParseFloat(t, baseNode.VCPUCost)*testCase.cpuMultiplier, mustParseFloat(t, flexNode.VCPUCost), 0.000001)
+			if testCase.assertCost {
+				assertFloatStrings(t, baseNode.Cost, flexNode.Cost, 0.000001)
+			}
+		})
+	}
+}
+
 func TestRCSForPVK(t *testing.T) {
 	rcs, server := testSetupRateCardStore(t)
 	defer server.Close()
@@ -90,7 +146,7 @@ func TestRCSForPVK(t *testing.T) {
 			pv, err := rcs.ForPVK(pvk, DefaultPricing{
 				Storage: "0.25",
 			})
-			assert.NoError(t, err)
+			require.NoError(t, err)
 			assertFloatStrings(t, testCase.cost, pv.Cost, 0.00001)
 		})
 	}
@@ -150,7 +206,7 @@ func TestRCSEgressForRegion(t *testing.T) {
 
 func testSetupRateCardStore(t *testing.T) (*RateCardStore, *httptest.Server) {
 	pricesUSDBytes, err := os.ReadFile("test/prices_usd.json")
-	assert.NoError(t, err)
+	require.NoError(t, err)
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusOK)
 		w.Write(pricesUSDBytes)
@@ -158,15 +214,23 @@ func testSetupRateCardStore(t *testing.T) (*RateCardStore, *httptest.Server) {
 
 	rcs := NewRateCardStore(server.URL, currencyCodeUSD)
 	store, err := rcs.Refresh()
-	assert.NoError(t, err)
-	assert.True(t, len(store) > 0)
+	require.NoError(t, err)
+	require.NotEmpty(t, store)
 	return rcs, server
 }
 
 func assertFloatStrings(t *testing.T, s1, s2 string, delta float64) {
+	t.Helper()
 	f1, err := strconv.ParseFloat(s1, 64)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 	f2, err := strconv.ParseFloat(s2, 64)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 	assert.InDelta(t, f1, f2, delta)
 }
+
+func mustParseFloat(t *testing.T, s string) float64 {
+	t.Helper()
+	f, err := strconv.ParseFloat(s, 64)
+	require.NoError(t, err)
+	return f
+}