| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- 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 {
- result := make(map[string]string, len(ccil))
- for k, v := range ccil {
- result[k] = v
- }
- return result
- }
- func (ccil CloudCostItemLabels) Equal(that CloudCostItemLabels) bool {
- if len(ccil) != len(that) {
- return false
- }
- // Maps are of equal length, so if all keys are in both maps, we don't
- // have to check the keys of the other map.
- for k, v := range ccil {
- if tv, ok := that[k]; !ok || v != tv {
- return false
- }
- }
- return true
- }
- type CloudCostItemProperties struct {
- ProviderID string `json:"providerID,omitempty"`
- Provider string `json:"provider,omitempty"`
- WorkGroupID string `json:"workGroupID,omitempty"`
- BillingID string `json:"billingID,omitempty"`
- Service string `json:"service,omitempty"`
- Category string `json:"category,omitempty"`
- Labels CloudCostItemLabels `json:"labels,omitempty"`
- }
- func (ccip CloudCostItemProperties) Equal(that CloudCostItemProperties) bool {
- return ccip.ProviderID == that.ProviderID &&
- ccip.Provider == that.Provider &&
- ccip.WorkGroupID == that.WorkGroupID &&
- ccip.BillingID == that.BillingID &&
- ccip.Service == that.Service &&
- ccip.Category == that.Category &&
- ccip.Labels.Equal(that.Labels)
- }
- func (ccip CloudCostItemProperties) Clone() CloudCostItemProperties {
- return CloudCostItemProperties{
- ProviderID: ccip.ProviderID,
- Provider: ccip.Provider,
- WorkGroupID: ccip.WorkGroupID,
- BillingID: ccip.BillingID,
- Service: ccip.Service,
- Category: ccip.Category,
- Labels: ccip.Labels.Clone(),
- }
- }
- 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", ccip.Provider, ccip.ProviderID)
- }
- // CloudCostItem represents a CUR line item, identifying a cloud resource and
- // its cost over some period of time.
- type CloudCostItem struct {
- Properties CloudCostItemProperties `json:"properties"`
- IsKubernetes bool `json:"isKubernetes"`
- Window Window `json:"window"`
- Cost float64 `json:"cost"`
- NetCost float64 `json:"netCost"`
- }
- // NewCloudCostItem instantiates a new CloudCostItem asset
- func NewCloudCostItem(start, end time.Time, cciProperties CloudCostItemProperties, isKubernetes bool, cost, netcost float64) *CloudCostItem {
- return &CloudCostItem{
- Properties: cciProperties,
- IsKubernetes: isKubernetes,
- Window: NewWindow(&start, &end),
- Cost: cost,
- NetCost: netcost,
- }
- }
- func (cci *CloudCostItem) Clone() *CloudCostItem {
- return &CloudCostItem{
- Properties: cci.Properties.Clone(),
- IsKubernetes: cci.IsKubernetes,
- Window: cci.Window.Clone(),
- Cost: cci.Cost,
- NetCost: cci.NetCost,
- }
- }
- func (cci *CloudCostItem) Equal(that *CloudCostItem) bool {
- if that == nil {
- return false
- }
- return cci.Properties.Equal(that.Properties) &&
- cci.IsKubernetes == that.IsKubernetes &&
- cci.Window.Equal(that.Window) &&
- cci.Cost == that.Cost &&
- cci.NetCost == that.NetCost
- }
- func (cci *CloudCostItem) Key() string {
- return cci.Properties.Key()
- }
- func (cci *CloudCostItem) add(that *CloudCostItem) {
- if cci == nil {
- log.Warnf("cannot add to nil CloudCostItem")
- return
- }
- cci.Cost += that.Cost
- cci.NetCost += that.NetCost
- 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"`
- Integration string `json:"-"`
- }
- // NewAssetSet instantiates a new AssetSet and, optionally, inserts
- // the given list of Assets
- func NewCloudCostItemSet(start, end time.Time, cloudCostItems ...*CloudCostItem) *CloudCostItemSet {
- ccis := &CloudCostItemSet{
- CloudCostItems: map[string]*CloudCostItem{},
- Window: NewWindow(&start, &end),
- }
- for _, cci := range cloudCostItems {
- ccis.Insert(cci)
- }
- 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
- }
- if !ccis.Window.Equal(that.Window) {
- return false
- }
- if len(ccis.CloudCostItems) != len(that.CloudCostItems) {
- return false
- }
- for k, cci := range ccis.CloudCostItems {
- tcci, ok := that.CloudCostItems[k]
- if !ok {
- return false
- }
- if !cci.Equal(tcci) {
- return false
- }
- }
- return true
- }
- func (ccis *CloudCostItemSet) Filter(filters filter.Filter[*CloudCostItem]) *CloudCostItemSet {
- if ccis == nil {
- return nil
- }
- if filters == nil {
- return ccis.Clone()
- }
- result := NewCloudCostItemSet(*ccis.Window.start, *ccis.Window.end)
- for _, cci := range ccis.CloudCostItems {
- if filters.Matches(cci) {
- result.Insert(cci.Clone())
- }
- }
- return result
- }
- func (ccis *CloudCostItemSet) Insert(that *CloudCostItem) error {
- if ccis == nil {
- return fmt.Errorf("cannot insert into nil CloudCostItemSet")
- }
- if that == nil {
- return fmt.Errorf("cannot insert nil CloudCostItem into CloudCostItemSet")
- }
- if ccis.CloudCostItems == nil {
- ccis.CloudCostItems = map[string]*CloudCostItem{}
- }
- // Add the given CloudCostItem to the existing entry, if there is one;
- // otherwise just set directly into allocations
- if _, ok := ccis.CloudCostItems[that.Key()]; !ok {
- ccis.CloudCostItems[that.Key()] = that.Clone()
- } else {
- ccis.CloudCostItems[that.Key()].add(that)
- }
- return nil
- }
- func (ccis *CloudCostItemSet) Clone() *CloudCostItemSet {
- items := make(map[string]*CloudCostItem, len(ccis.CloudCostItems))
- for k, v := range ccis.CloudCostItems {
- items[k] = v.Clone()
- }
- return &CloudCostItemSet{
- CloudCostItems: items,
- Integration: ccis.Integration,
- Window: ccis.Window.Clone(),
- }
- }
- func (ccis *CloudCostItemSet) IsEmpty() bool {
- if ccis == nil {
- return true
- }
- if len(ccis.CloudCostItems) == 0 {
- return true
- }
- return false
- }
- func (ccis *CloudCostItemSet) Length() int {
- if ccis == nil {
- return 0
- }
- return len(ccis.CloudCostItems)
- }
- func (ccis *CloudCostItemSet) GetWindow() Window {
- return ccis.Window
- }
- func (ccis *CloudCostItemSet) Merge(that *CloudCostItemSet) (*CloudCostItemSet, error) {
- if ccis == nil {
- return nil, fmt.Errorf("cannot merge nil CloudCostItemSets")
- }
- if that.IsEmpty() {
- return ccis.Clone(), nil
- }
- if !ccis.Window.Equal(that.Window) {
- return nil, fmt.Errorf("cannot merge CloudCostItemSets with different windows")
- }
- start, end := *ccis.Window.Start(), *ccis.Window.End()
- result := NewCloudCostItemSet(start, end)
- for _, cci := range ccis.CloudCostItems {
- result.Insert(cci)
- }
- for _, cci := range that.CloudCostItems {
- result.Insert(cci)
- }
- return result, nil
- }
- type CloudCostItemSetRange struct {
- CloudCostItemSets []*CloudCostItemSet `json:"sets"`
- Window Window `json:"window"`
- }
- // NewCloudCostItemSetRange create a CloudCostItemSetRange containing CloudCostItemSets with windows of equal duration
- // the duration between start and end must be divisible by the window duration argument
- func NewCloudCostItemSetRange(start time.Time, end time.Time, window time.Duration, integration string) (*CloudCostItemSetRange, error) {
- windows, err := GetWindows(start, end, window)
- if err != nil {
- return nil, err
- }
- // Build slice of CloudCostItemSet to cover the range
- cloudCostItemSets := make([]*CloudCostItemSet, len(windows))
- for i, w := range windows {
- ccis := NewCloudCostItemSet(*w.Start(), *w.End())
- ccis.Integration = integration
- cloudCostItemSets[i] = ccis
- }
- return &CloudCostItemSetRange{
- Window: NewWindow(&start, &end),
- CloudCostItemSets: cloudCostItemSets,
- }, nil
- }
- func (ccisr *CloudCostItemSetRange) Clone() *CloudCostItemSetRange {
- ccisSlice := make([]*CloudCostItemSet, len(ccisr.CloudCostItemSets))
- for i, ccis := range ccisr.CloudCostItemSets {
- ccisSlice[i] = ccis.Clone()
- }
- return &CloudCostItemSetRange{
- Window: ccisr.Window.Clone(),
- CloudCostItemSets: ccisSlice,
- }
- }
- // 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.
- // If all or a portion of the window of the CloudCostItem is outside of the windows of the existing CloudCostItemSets,
- // that portion of the CloudCostItem's cost will not be inserted
- func (ccisr *CloudCostItemSetRange) LoadCloudCostItem(cloudCostItem *CloudCostItem) {
- window := cloudCostItem.Window
- if window.IsOpen() {
- log.Errorf("CloudCostItemSetRange: LoadCloudCostItem: invalid window %s", window.String())
- return
- }
- totalPct := 0.0
- // Distribute cost of the current item across one or more CloudCostItems in
- // across each relevant CloudCostItemSet. Stop when the end of the current
- // block reaches the item's end time or the end of the range.
- for _, ccis := range ccisr.CloudCostItemSets {
- setWindow := ccis.Window
- // get percent of item window contained in set window
- pct := setWindow.GetPercentInWindow(window)
- if pct == 0 {
- continue
- }
- cci := cloudCostItem
- // If the current set Window only contains a portion of the CloudCostItem Window, insert costs relative to that portion
- if pct < 1.0 {
- cci = &CloudCostItem{
- Properties: cloudCostItem.Properties,
- IsKubernetes: cloudCostItem.IsKubernetes,
- Window: window.Contract(setWindow),
- Cost: cloudCostItem.Cost * pct,
- NetCost: cloudCostItem.NetCost * pct,
- }
- }
- err := ccis.Insert(cci)
- if err != nil {
- log.Errorf("CloudCostItemSetRange: LoadCloudCostItem: failed to load CloudCostItem with key %s and window %s: %s", cci.Key(), ccis.GetWindow().String(), err.Error())
- }
- // If all cost has been inserted then finish
- totalPct += pct
- if totalPct >= 1.0 {
- return
- }
- }
- }
|