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

Merge branch 'develop' into kaelan-fix-node-vgpu-count

Kaelan Patel 3 лет назад
Родитель
Сommit
81e47194e9
4 измененных файлов с 422 добавлено и 19 удалено
  1. 29 0
      pkg/kubecost/asset.go
  2. 148 0
      pkg/kubecost/cloudcostitem.go
  3. 165 1
      pkg/kubecost/cloudcostitem_test.go
  4. 80 18
      pkg/kubecost/mock.go

+ 29 - 0
pkg/kubecost/asset.go

@@ -2157,6 +2157,22 @@ func (n *Node) GPUs() float64 {
 	return n.GPUHours * (60.0 / n.Minutes())
 }
 
+func (n *Node) MonitoringKey() string {
+	nodeProps := n.GetProperties()
+	if nodeProps == nil {
+		return ""
+	}
+	//TO-DO: For Alibaba investigate why cloudCost ProviderID doesnt match Kubecost ProviderID via Kubernetes API
+	if nodeProps.Provider == AlibabaProvider {
+		aliProviderID := strings.Split(nodeProps.ProviderID, ".")
+		if len(aliProviderID) != 2 {
+			return ""
+		}
+		return nodeProps.Provider + "/" + nodeProps.Account + "/" + aliProviderID[1]
+	}
+	return nodeProps.Provider + "/" + nodeProps.Account + "/" + nodeProps.ProviderID
+}
+
 // LoadBalancer is an Asset representing a single load balancer in a cluster
 // TODO: add GB of ingress processed, numForwardingRules once we start recording those to prometheus metric
 type LoadBalancer struct {
@@ -3163,6 +3179,19 @@ func (as *AssetSet) accumulate(that *AssetSet) (*AssetSet, error) {
 	return acc, nil
 }
 
+func (as *AssetSet) MonitoredNodeForCloudCostItem(cci *CloudCostItem) *Node {
+	for _, node := range as.Nodes {
+		if node.MonitoringKey() == cci.MonitoringKey() {
+			props := node.GetProperties()
+			if props == nil {
+				continue
+			}
+			return node
+		}
+	}
+	return nil
+}
+
 type DiffKind string
 
 const (

+ 148 - 0
pkg/kubecost/cloudcostitem.go

@@ -2,12 +2,22 @@ package kubecost
 
 import (
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/opencost/opencost/pkg/filter"
 	"github.com/opencost/opencost/pkg/log"
 )
 
+// These contain some labels that can be used on Cloud cost
+// item to get the corresponding cluster its associated.
+const (
+	AWSMatchLabel1     = "eks_cluster_name"
+	AWSMatchLabel2     = "alpha_eksctl_io_cluster_name"
+	AlibabaMatchLabel1 = "ack.aliyun.com"
+	GCPMatchLabel1     = "goog-k8s-cluster-name"
+)
+
 type CloudCostItemLabels map[string]string
 
 func (ccil CloudCostItemLabels) Clone() CloudCostItemLabels {
@@ -70,6 +80,10 @@ func (ccip CloudCostItemProperties) Key() string {
 	return fmt.Sprintf("%s/%s/%s/%s/%s/%s", ccip.Provider, ccip.BillingID, ccip.WorkGroupID, ccip.Category, ccip.Service, ccip.ProviderID)
 }
 
+func (ccip CloudCostItemProperties) MonitoringKey() string {
+	return fmt.Sprintf("%s/%s/%s", ccip.Provider, ccip.WorkGroupID, ccip.ProviderID)
+}
+
 // CloudCostItem represents a CUR line item, identifying a cloud resource and
 // its cost over some period of time.
 type CloudCostItem struct {
@@ -128,6 +142,88 @@ func (cci *CloudCostItem) add(that *CloudCostItem) {
 	cci.Window = cci.Window.Expand(that.Window)
 }
 
+func (cci *CloudCostItem) MonitoringKey() string {
+	return cci.Properties.MonitoringKey()
+}
+
+// Ony use compute resources to get Cluster names
+func (cci *CloudCostItem) GetCluster() string {
+	switch provider := cci.Properties.Provider; provider {
+	case AWSProvider:
+		return cci.GetAWSCluster()
+	case AzureProvider:
+		return cci.GetAzureCluster()
+	case GCPProvider:
+		return cci.GetGCPCluster()
+	case AlibabaProvider:
+		return cci.GetAlibabaCluster()
+	default:
+		log.Warnf("unsupported CloudCostItem found for a provider: %s", provider)
+		return ""
+	}
+}
+
+// Add any new ways of finding GCP cluster from Cloud cost Item
+func (cci *CloudCostItem) GetGCPCluster() string {
+	// currently from Cloud cost compute unable to get cluster name so returning empty
+	return ""
+}
+
+// Add any new ways of finding AWS cluster from Cloud cost Item
+func (cci *CloudCostItem) GetAWSCluster() string {
+	if cci == nil {
+		return ""
+	}
+
+	// This flag should be removed with filters in the compute query
+	if cci.Properties.Provider != AWSProvider || cci.Properties.Category != ComputeCategory {
+		return ""
+	}
+	// cn be either of these two labels to distinguish cluster name for a given providerID
+	if val, ok := cci.Properties.Labels[AWSMatchLabel1]; ok {
+		return val
+	}
+	if val, ok := cci.Properties.Labels[AWSMatchLabel2]; ok {
+		return val
+	}
+	return ""
+}
+
+// Add any new ways of finding Azure cluster from Cloud cost Item
+func (cci *CloudCostItem) GetAzureCluster() string {
+	if cci == nil {
+		return ""
+	}
+
+	// This flag should be removed with filters in the compute query
+	if cci.Properties.Provider != AzureProvider || cci.Properties.Category != ComputeCategory {
+		return ""
+	}
+
+	providerIDSplit := strings.Split(cci.Properties.ProviderID, "/")
+	// ensure this is actually returnable before return
+	if len(providerIDSplit) < 6 {
+		return ""
+	}
+	return strings.Split(cci.Properties.ProviderID, "/")[6]
+}
+
+// Add any new ways of finding Alibaba cluster from Cloud cost Item
+func (cci *CloudCostItem) GetAlibabaCluster() string {
+	if cci == nil {
+		return ""
+	}
+
+	// This flag should be removed with filters in the compute query
+	if cci.Properties.Provider != AlibabaProvider || cci.Properties.Category != ComputeCategory {
+		return ""
+	}
+	if val, ok := cci.Properties.Labels[AlibabaMatchLabel1]; ok {
+		return val
+	}
+	return ""
+}
+
 type CloudCostItemSet struct {
 	CloudCostItems map[string]*CloudCostItem `json:"items"`
 	Window         Window                    `json:"window"`
@@ -149,6 +245,42 @@ func NewCloudCostItemSet(start, end time.Time, cloudCostItems ...*CloudCostItem)
 	return ccis
 }
 
+func (ccis *CloudCostItemSet) Accumulate(that *CloudCostItemSet) (*CloudCostItemSet, error) {
+	if ccis.IsEmpty() {
+		return that.Clone(), nil
+	}
+
+	if that.IsEmpty() {
+		return ccis.Clone(), nil
+	}
+	// Set start, end to min(start), max(end)
+	start := ccis.Window.Start()
+	end := ccis.Window.End()
+	if that.Window.Start().Before(*start) {
+		start = that.Window.Start()
+	}
+	if that.Window.End().After(*end) {
+		end = that.Window.End()
+	}
+
+	acc := NewCloudCostItemSet(*start, *end)
+
+	for _, cci := range ccis.CloudCostItems {
+		err := acc.Insert(cci)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for _, cci := range that.CloudCostItems {
+		err := acc.Insert(cci)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return acc, nil
+}
+
 func (ccis *CloudCostItemSet) Equal(that *CloudCostItemSet) bool {
 	if ccis.Integration != that.Integration {
 		return false
@@ -319,6 +451,22 @@ func (ccisr *CloudCostItemSetRange) Clone() *CloudCostItemSetRange {
 	}
 }
 
+// Accumulate sums each CloudCostItemSet in the given range, returning a single cumulative
+// CloudCostItemSet for the entire range.
+func (ccisr *CloudCostItemSetRange) Accumulate() (*CloudCostItemSet, error) {
+	var cloudCostItemSet *CloudCostItemSet
+	var err error
+
+	for _, ccis := range ccisr.CloudCostItemSets {
+		cloudCostItemSet, err = cloudCostItemSet.Accumulate(ccis)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return cloudCostItemSet, nil
+}
+
 // LoadCloudCostItem loads CloudCostItems into existing CloudCostItemSets of the CloudCostItemSetRange.
 // This function service to aggregate and distribute costs over predefined windows
 // are accumulated here so that the resulting CloudCostItem with the 1d window has the correct price for the entire day.

+ 165 - 1
pkg/kubecost/cloudcostitem_test.go

@@ -1,9 +1,10 @@
 package kubecost
 
 import (
-	"github.com/opencost/opencost/pkg/util/timeutil"
 	"testing"
 	"time"
+
+	"github.com/opencost/opencost/pkg/util/timeutil"
 )
 
 var cciProperties1 = CloudCostItemProperties{
@@ -254,3 +255,166 @@ func TestCloudCostItem_LoadCloudCostItem(t *testing.T) {
 	}
 
 }
+
+func TestGetAWSClusterFromCCI(t *testing.T) {
+	awsCCIWithLabeleksClusterName, eksClusterName := GenerateAWSMockCCIAndPID(1, 1, AWSMatchLabel1, ComputeCategory)
+	awsCCIWithLabeleksCtlClusterName, eksCtlClusterName := GenerateAWSMockCCIAndPID(2, 2, AWSMatchLabel2, ComputeCategory)
+	awsCCIWithLabelWithRandomLabel, _ := GenerateAWSMockCCIAndPID(1, 1, "randomLabel", ComputeCategory)
+	awsCCINetworkCategory, _ := GenerateAWSMockCCIAndPID(1, 1, AWSMatchLabel1, NetworkCategory)
+	alibabaCCI, _ := GenerateAlibabaMockCCIAndPID(4, 4, AlibabaMatchLabel1, ComputeCategory)
+	testCases := map[string]struct {
+		testcci  *CloudCostItem
+		expected string
+	}{
+		"cluster in label eks_cluster_name": {
+			testcci:  awsCCIWithLabeleksClusterName,
+			expected: eksClusterName,
+		},
+		"cluster in label alpha_eksctl_io_cluster_name": {
+			testcci:  awsCCIWithLabeleksCtlClusterName,
+			expected: eksCtlClusterName,
+		},
+		"cluster name in random label either not eks_cluster_name or eks_cluster_name": {
+			testcci:  awsCCIWithLabelWithRandomLabel,
+			expected: "",
+		},
+		"Not a AWS provider": {
+			testcci:  alibabaCCI,
+			expected: "",
+		},
+		"Not a compute resource": {
+			testcci:  awsCCINetworkCategory,
+			expected: "",
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			actual := testCase.testcci.GetAWSCluster()
+			if actual != testCase.expected {
+				t.Errorf("incorrect result: Actual: '%s', Expected: '%s", actual, testCase.expected)
+			}
+		})
+	}
+}
+
+func TestGetAzureClusterFromCCI(t *testing.T) {
+	testCases := map[string]struct {
+		testcci  *CloudCostItem
+		expected string
+	}{
+		"cluster in ProviderID complete": {
+			testcci: &CloudCostItem{
+				IsKubernetes: true,
+				Window:       Window{},
+				Properties: CloudCostItemProperties{
+					Labels: map[string]string{
+						"randomLabel": "value1",
+					},
+					Provider:   AzureProvider,
+					Category:   ComputeCategory,
+					ProviderID: "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/mc_dev_dev-1_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-devsysz1-24570986-vmss/virtualMachines/0",
+				},
+			},
+			expected: "mc_dev_dev-1_eastus",
+		},
+		"cluster in ProviderID complete but missing some values": {
+			testcci: &CloudCostItem{
+				IsKubernetes: true,
+				Window:       Window{},
+				Properties: CloudCostItemProperties{
+					Labels: map[string]string{
+						"randomLabel": "value1",
+					},
+					Provider:   AzureProvider,
+					Category:   ComputeCategory,
+					ProviderID: "azure:///subscriptions//resourceGroups/mc_dev_dev-1_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-devsysz1-XXXXX-vmss/virtualMachines/0",
+				},
+			},
+			expected: "mc_dev_dev-1_eastus",
+		},
+		"Not having enough split content in providerID": {
+			testcci: &CloudCostItem{
+				IsKubernetes: true,
+				Window:       Window{},
+				Properties: CloudCostItemProperties{
+					Labels: map[string]string{
+						"randomLabel": "value1",
+					},
+					Provider:   AzureProvider,
+					Category:   ComputeCategory,
+					ProviderID: "test1",
+				},
+			},
+			expected: "",
+		},
+		"Not a Azure provider": {
+			testcci: &CloudCostItem{
+				IsKubernetes: true,
+				Window:       Window{},
+				Properties: CloudCostItemProperties{
+					Labels: map[string]string{
+						"randomLabel": "value1",
+					},
+					Provider:   AWSProvider,
+					Category:   ComputeCategory,
+					ProviderID: "test1",
+				},
+			},
+			expected: "",
+		},
+		"Not a compute resource": {
+			testcci: &CloudCostItem{
+				IsKubernetes: true,
+				Window:       Window{},
+				Properties: CloudCostItemProperties{
+					Labels: map[string]string{
+						"randomLabel": "value1",
+					},
+					Provider:   AzureProvider,
+					Category:   StorageCategory,
+					ProviderID: "pvc-xyz",
+				},
+			},
+			expected: "",
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			actual := testCase.testcci.GetAzureCluster()
+			if actual != testCase.expected {
+				t.Errorf("incorrect result: Actual: '%s', Expected: '%s", actual, testCase.expected)
+			}
+		})
+	}
+}
+
+func TestGetAlibabaClusterFromCCI(t *testing.T) {
+	alibabaCCIWithACKAliyunCom, clusterName1 := GenerateAlibabaMockCCIAndPID(4, 4, AlibabaMatchLabel1, ComputeCategory)
+	awsCCI, _ := GenerateAWSMockCCIAndPID(1, 1, AWSMatchLabel1, ComputeCategory)
+	alibabaCCINetworkCategory, clusterName1 := GenerateAlibabaMockCCIAndPID(4, 4, AlibabaMatchLabel1, NetworkCategory)
+	testCases := map[string]struct {
+		testcci  *CloudCostItem
+		expected string
+	}{
+		"cluster in label ack.aliyun.com": {
+			testcci:  alibabaCCIWithACKAliyunCom,
+			expected: clusterName1,
+		},
+		"Not a Alibaba provider": {
+			testcci:  awsCCI,
+			expected: "",
+		},
+		"Not a compute resource": {
+			testcci:  alibabaCCINetworkCategory,
+			expected: "",
+		},
+	}
+	for name, testCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			actual := testCase.testcci.GetAlibabaCluster()
+			if actual != testCase.expected {
+				t.Errorf("incorrect result: Actual: '%s', Expected: '%s", actual, testCase.expected)
+			}
+		})
+	}
+}

+ 80 - 18
pkg/kubecost/mock.go

@@ -2,6 +2,7 @@ package kubecost
 
 import (
 	"fmt"
+	"strconv"
 	"time"
 )
 
@@ -568,30 +569,44 @@ func GenerateMockAssetSets(start, end time.Time) []*AssetSet {
 //
 // | Asset                        | Cost |  Adj |
 // +------------------------------+------+------+
-//   cluster1:
-//     node1:                        6.00   1.00
-//     node2:                        4.00   1.50
-//     node3:                        7.00  -0.50
-//     disk1:                        2.50   0.00
-//     disk2:                        1.50   0.00
-//     clusterManagement1:           3.00   0.00
+//
+//	cluster1:
+//	  node1:                        6.00   1.00
+//	  node2:                        4.00   1.50
+//	  node3:                        7.00  -0.50
+//	  disk1:                        2.50   0.00
+//	  disk2:                        1.50   0.00
+//	  clusterManagement1:           3.00   0.00
+//
 // +------------------------------+------+------+
-//   cluster1 subtotal              24.00   2.00
+//
+//	cluster1 subtotal              24.00   2.00
+//
 // +------------------------------+------+------+
-//   cluster2:
-//     node4:                       12.00  -1.00
-//     disk3:                        2.50   0.00
-//     disk4:                        1.50   0.00
-//     clusterManagement2:           0.00   0.00
+//
+//	cluster2:
+//	  node4:                       12.00  -1.00
+//	  disk3:                        2.50   0.00
+//	  disk4:                        1.50   0.00
+//	  clusterManagement2:           0.00   0.00
+//
 // +------------------------------+------+------+
-//   cluster2 subtotal              16.00  -1.00
+//
+//	cluster2 subtotal              16.00  -1.00
+//
 // +------------------------------+------+------+
-//   cluster3:
-//     node5:                       17.00   2.00
+//
+//	cluster3:
+//	  node5:                       17.00   2.00
+//
 // +------------------------------+------+------+
-//   cluster3 subtotal              17.00   2.00
+//
+//	cluster3 subtotal              17.00   2.00
+//
 // +------------------------------+------+------+
-//   total                          57.00   3.00
+//
+//	total                          57.00   3.00
+//
 // +------------------------------+------+------+
 func GenerateMockAssetSet(start time.Time) *AssetSet {
 	end := start.Add(day)
@@ -682,3 +697,50 @@ func GenerateMockAssetSet(start time.Time) *AssetSet {
 		node5,
 	)
 }
+
+func GenerateKubecostNodeAndPID(mockProviderIDInt int, provider string, mockClusterID int, setEndTime time.Time) (*Node, string) {
+	providerID := "PID" + strconv.FormatInt(int64(mockProviderIDInt), 10)
+	return &Node{
+		Properties: &AssetProperties{
+			Provider:   provider,
+			ProviderID: providerID,
+			Cluster:    "cluster" + strconv.FormatInt(int64(mockClusterID), 10),
+		},
+		End: setEndTime,
+	}, providerID
+}
+func GenerateAWSMockCCIAndPID(mockProviderIDInt int, mockCloudIDInt int, labelKey string, resourceCategory string) (*CloudCostItem, string) {
+	return &CloudCostItem{
+		Properties: CloudCostItemProperties{
+			ProviderID: "PID" + strconv.FormatInt(int64(mockProviderIDInt), 10),
+			Provider:   AWSProvider,
+			Category:   resourceCategory,
+			Labels: map[string]string{
+				labelKey: "cluster" + strconv.FormatInt(int64(mockCloudIDInt), 10),
+			},
+		},
+	}, "cluster" + strconv.FormatInt(int64(mockCloudIDInt), 10)
+}
+
+func GenerateAlibabaMockCCIAndPID(mockProviderIDInt int, mockCloudIDInt int, labelKey string, resourceCategory string) (*CloudCostItem, string) {
+	return &CloudCostItem{
+		Properties: CloudCostItemProperties{
+			ProviderID: "PID" + strconv.FormatInt(int64(mockProviderIDInt), 10),
+			Provider:   AlibabaProvider,
+			Category:   resourceCategory,
+			Labels: map[string]string{
+				labelKey: "cluster" + strconv.FormatInt(int64(mockCloudIDInt), 10),
+			},
+		},
+	}, "cluster" + strconv.FormatInt(int64(mockCloudIDInt), 10)
+}
+
+func GenerateGCPMockCCIAndPID(mockProviderIDInt int, mockCloudIDInt int, labelKey string, resourceCategory string) (*CloudCostItem, string) {
+	return &CloudCostItem{
+		Properties: CloudCostItemProperties{
+			ProviderID: "PID" + strconv.FormatInt(int64(mockProviderIDInt), 10),
+			Provider:   GCPProvider,
+			Category:   resourceCategory,
+		},
+	}, ""
+}