Explorar o código

adding the get cluster functionality for a cloud cost item based on providers

Signed-off-by: Alan Rodrigues <alanr5691@yahoo.com>
Signed-off-by: Logan Ballard <loganballard@gmail.com>
Alan Rodrigues %!s(int64=3) %!d(string=hai) anos
pai
achega
b3824e2e6b
Modificáronse 3 ficheiros con 333 adicións e 19 borrados
  1. 88 0
      pkg/kubecost/cloudcostitem.go
  2. 165 1
      pkg/kubecost/cloudcostitem_test.go
  3. 80 18
      pkg/kubecost/mock.go

+ 88 - 0
pkg/kubecost/cloudcostitem.go

@@ -2,12 +2,22 @@ package kubecost
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/opencost/opencost/pkg/filter"
 	"github.com/opencost/opencost/pkg/filter"
 	"github.com/opencost/opencost/pkg/log"
 	"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
 type CloudCostItemLabels map[string]string
 
 
 func (ccil CloudCostItemLabels) Clone() CloudCostItemLabels {
 func (ccil CloudCostItemLabels) Clone() CloudCostItemLabels {
@@ -136,6 +146,84 @@ func (cci *CloudCostItem) MonitoringKey() string {
 	return cci.Properties.MonitoringKey()
 	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 {
 type CloudCostItemSet struct {
 	CloudCostItems map[string]*CloudCostItem `json:"items"`
 	CloudCostItems map[string]*CloudCostItem `json:"items"`
 	Window         Window                    `json:"window"`
 	Window         Window                    `json:"window"`

+ 165 - 1
pkg/kubecost/cloudcostitem_test.go

@@ -1,9 +1,10 @@
 package kubecost
 package kubecost
 
 
 import (
 import (
-	"github.com/opencost/opencost/pkg/util/timeutil"
 	"testing"
 	"testing"
 	"time"
 	"time"
+
+	"github.com/opencost/opencost/pkg/util/timeutil"
 )
 )
 
 
 var cciProperties1 = CloudCostItemProperties{
 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 (
 import (
 	"fmt"
 	"fmt"
+	"strconv"
 	"time"
 	"time"
 )
 )
 
 
@@ -568,30 +569,44 @@ func GenerateMockAssetSets(start, end time.Time) []*AssetSet {
 //
 //
 // | Asset                        | Cost |  Adj |
 // | 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 {
 func GenerateMockAssetSet(start time.Time) *AssetSet {
 	end := start.Add(day)
 	end := start.Add(day)
@@ -682,3 +697,50 @@ func GenerateMockAssetSet(start time.Time) *AssetSet {
 		node5,
 		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,
+		},
+	}, ""
+}