cloudcostitem.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. package kubecost
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/opencost/opencost/pkg/filter"
  6. "github.com/opencost/opencost/pkg/log"
  7. )
  8. type CloudCostItemLabels map[string]string
  9. func (ccil CloudCostItemLabels) Clone() CloudCostItemLabels {
  10. result := make(map[string]string, len(ccil))
  11. for k, v := range ccil {
  12. result[k] = v
  13. }
  14. return result
  15. }
  16. func (ccil CloudCostItemLabels) Equal(that CloudCostItemLabels) bool {
  17. if len(ccil) != len(that) {
  18. return false
  19. }
  20. // Maps are of equal length, so if all keys are in both maps, we don't
  21. // have to check the keys of the other map.
  22. for k, v := range ccil {
  23. if tv, ok := that[k]; !ok || v != tv {
  24. return false
  25. }
  26. }
  27. return true
  28. }
  29. type CloudCostItemProperties struct {
  30. ProviderID string `json:"providerID,omitempty"`
  31. Provider string `json:"provider,omitempty"`
  32. WorkGroupID string `json:"workGroupID,omitempty"`
  33. BillingID string `json:"billingID,omitempty"`
  34. Service string `json:"service,omitempty"`
  35. Category string `json:"category,omitempty"`
  36. Labels CloudCostItemLabels `json:"labels,omitempty"`
  37. }
  38. func (ccip CloudCostItemProperties) Equal(that CloudCostItemProperties) bool {
  39. return ccip.ProviderID == that.ProviderID &&
  40. ccip.Provider == that.Provider &&
  41. ccip.WorkGroupID == that.WorkGroupID &&
  42. ccip.BillingID == that.BillingID &&
  43. ccip.Service == that.Service &&
  44. ccip.Category == that.Category &&
  45. ccip.Labels.Equal(that.Labels)
  46. }
  47. func (ccip CloudCostItemProperties) Clone() CloudCostItemProperties {
  48. return CloudCostItemProperties{
  49. ProviderID: ccip.ProviderID,
  50. Provider: ccip.Provider,
  51. WorkGroupID: ccip.WorkGroupID,
  52. BillingID: ccip.BillingID,
  53. Service: ccip.Service,
  54. Category: ccip.Category,
  55. Labels: ccip.Labels.Clone(),
  56. }
  57. }
  58. func (ccip CloudCostItemProperties) Key() string {
  59. return fmt.Sprintf("%s/%s/%s/%s/%s/%s", ccip.Provider, ccip.BillingID, ccip.WorkGroupID, ccip.Category, ccip.Service, ccip.ProviderID)
  60. }
  61. // CloudCostItem represents a CUR line item, identifying a cloud resource and
  62. // its cost over some period of time.
  63. type CloudCostItem struct {
  64. Properties CloudCostItemProperties `json:"properties"`
  65. IsKubernetes bool `json:"isKubernetes"`
  66. Window Window `json:"window"`
  67. Cost float64 `json:"cost"`
  68. NetCost float64 `json:"netCost"`
  69. }
  70. // NewCloudCostItem instantiates a new CloudCostItem asset
  71. func NewCloudCostItem(start, end time.Time, cciProperties CloudCostItemProperties, isKubernetes bool, cost, netcost float64) *CloudCostItem {
  72. return &CloudCostItem{
  73. Properties: cciProperties,
  74. IsKubernetes: isKubernetes,
  75. Window: NewWindow(&start, &end),
  76. Cost: cost,
  77. NetCost: netcost,
  78. }
  79. }
  80. func (cci *CloudCostItem) Clone() *CloudCostItem {
  81. return &CloudCostItem{
  82. Properties: cci.Properties.Clone(),
  83. IsKubernetes: cci.IsKubernetes,
  84. Window: cci.Window.Clone(),
  85. Cost: cci.Cost,
  86. NetCost: cci.NetCost,
  87. }
  88. }
  89. func (cci *CloudCostItem) Equal(that *CloudCostItem) bool {
  90. if that == nil {
  91. return false
  92. }
  93. return cci.Properties.Equal(that.Properties) &&
  94. cci.IsKubernetes == that.IsKubernetes &&
  95. cci.Window.Equal(that.Window) &&
  96. cci.Cost == that.Cost &&
  97. cci.NetCost == that.NetCost
  98. }
  99. func (cci *CloudCostItem) Key() string {
  100. return cci.Properties.Key()
  101. }
  102. func (cci *CloudCostItem) add(that *CloudCostItem) {
  103. if cci == nil {
  104. log.Warnf("cannot add to nil CloudCostItem")
  105. return
  106. }
  107. cci.Cost += that.Cost
  108. cci.NetCost += that.NetCost
  109. cci.Window = cci.Window.Expand(that.Window)
  110. }
  111. type CloudCostItemSet struct {
  112. CloudCostItems map[string]*CloudCostItem `json:"items"`
  113. Window Window `json:"window"`
  114. Integration string `json:"-"`
  115. }
  116. // NewAssetSet instantiates a new AssetSet and, optionally, inserts
  117. // the given list of Assets
  118. func NewCloudCostItemSet(start, end time.Time, cloudCostItems ...*CloudCostItem) *CloudCostItemSet {
  119. ccis := &CloudCostItemSet{
  120. CloudCostItems: map[string]*CloudCostItem{},
  121. Window: NewWindow(&start, &end),
  122. }
  123. for _, cci := range cloudCostItems {
  124. ccis.Insert(cci)
  125. }
  126. return ccis
  127. }
  128. func (ccis *CloudCostItemSet) Equal(that *CloudCostItemSet) bool {
  129. if ccis.Integration != that.Integration {
  130. return false
  131. }
  132. if !ccis.Window.Equal(that.Window) {
  133. return false
  134. }
  135. if len(ccis.CloudCostItems) != len(that.CloudCostItems) {
  136. return false
  137. }
  138. for k, cci := range ccis.CloudCostItems {
  139. tcci, ok := that.CloudCostItems[k]
  140. if !ok {
  141. return false
  142. }
  143. if !cci.Equal(tcci) {
  144. return false
  145. }
  146. }
  147. return true
  148. }
  149. func (ccis *CloudCostItemSet) Filter(filters filter.Filter[*CloudCostItem]) *CloudCostItemSet {
  150. if ccis == nil {
  151. return nil
  152. }
  153. if filters == nil {
  154. return ccis.Clone()
  155. }
  156. result := NewCloudCostItemSet(*ccis.Window.start, *ccis.Window.end)
  157. for _, cci := range ccis.CloudCostItems {
  158. if filters.Matches(cci) {
  159. result.Insert(cci.Clone())
  160. }
  161. }
  162. return result
  163. }
  164. func (ccis *CloudCostItemSet) Insert(that *CloudCostItem) error {
  165. if ccis == nil {
  166. return fmt.Errorf("cannot insert into nil CloudCostItemSet")
  167. }
  168. if that == nil {
  169. return fmt.Errorf("cannot insert nil CloudCostItem into CloudCostItemSet")
  170. }
  171. if ccis.CloudCostItems == nil {
  172. ccis.CloudCostItems = map[string]*CloudCostItem{}
  173. }
  174. // Add the given CloudCostItem to the existing entry, if there is one;
  175. // otherwise just set directly into allocations
  176. if _, ok := ccis.CloudCostItems[that.Key()]; !ok {
  177. ccis.CloudCostItems[that.Key()] = that.Clone()
  178. } else {
  179. ccis.CloudCostItems[that.Key()].add(that)
  180. }
  181. return nil
  182. }
  183. func (ccis *CloudCostItemSet) Clone() *CloudCostItemSet {
  184. items := make(map[string]*CloudCostItem, len(ccis.CloudCostItems))
  185. for k, v := range ccis.CloudCostItems {
  186. items[k] = v.Clone()
  187. }
  188. return &CloudCostItemSet{
  189. CloudCostItems: items,
  190. Integration: ccis.Integration,
  191. Window: ccis.Window.Clone(),
  192. }
  193. }
  194. func (ccis *CloudCostItemSet) IsEmpty() bool {
  195. if ccis == nil {
  196. return true
  197. }
  198. if len(ccis.CloudCostItems) == 0 {
  199. return true
  200. }
  201. return false
  202. }
  203. func (ccis *CloudCostItemSet) Length() int {
  204. if ccis == nil {
  205. return 0
  206. }
  207. return len(ccis.CloudCostItems)
  208. }
  209. func (ccis *CloudCostItemSet) GetWindow() Window {
  210. return ccis.Window
  211. }
  212. func (ccis *CloudCostItemSet) Merge(that *CloudCostItemSet) (*CloudCostItemSet, error) {
  213. if ccis == nil {
  214. return nil, fmt.Errorf("cannot merge nil CloudCostItemSets")
  215. }
  216. if that.IsEmpty() {
  217. return ccis.Clone(), nil
  218. }
  219. if !ccis.Window.Equal(that.Window) {
  220. return nil, fmt.Errorf("cannot merge CloudCostItemSets with different windows")
  221. }
  222. start, end := *ccis.Window.Start(), *ccis.Window.End()
  223. result := NewCloudCostItemSet(start, end)
  224. for _, cci := range ccis.CloudCostItems {
  225. result.Insert(cci)
  226. }
  227. for _, cci := range that.CloudCostItems {
  228. result.Insert(cci)
  229. }
  230. return result, nil
  231. }
  232. type CloudCostItemSetRange struct {
  233. CloudCostItemSets []*CloudCostItemSet `json:"sets"`
  234. Window Window `json:"window"`
  235. }
  236. // NewCloudCostItemSetRange create a CloudCostItemSetRange containing CloudCostItemSets with windows of equal duration
  237. // the duration between start and end must be divisible by the window duration argument
  238. func NewCloudCostItemSetRange(start time.Time, end time.Time, window time.Duration, integration string) (*CloudCostItemSetRange, error) {
  239. windows, err := GetWindows(start, end, window)
  240. if err != nil {
  241. return nil, err
  242. }
  243. // Build slice of CloudCostItemSet to cover the range
  244. cloudCostItemSets := make([]*CloudCostItemSet, len(windows))
  245. for i, w := range windows {
  246. ccis := NewCloudCostItemSet(*w.Start(), *w.End())
  247. ccis.Integration = integration
  248. cloudCostItemSets[i] = ccis
  249. }
  250. return &CloudCostItemSetRange{
  251. Window: NewWindow(&start, &end),
  252. CloudCostItemSets: cloudCostItemSets,
  253. }, nil
  254. }
  255. func (ccisr *CloudCostItemSetRange) Clone() *CloudCostItemSetRange {
  256. ccisSlice := make([]*CloudCostItemSet, len(ccisr.CloudCostItemSets))
  257. for i, ccis := range ccisr.CloudCostItemSets {
  258. ccisSlice[i] = ccis.Clone()
  259. }
  260. return &CloudCostItemSetRange{
  261. Window: ccisr.Window.Clone(),
  262. CloudCostItemSets: ccisSlice,
  263. }
  264. }
  265. // LoadCloudCostItem loads CloudCostItems into existing CloudCostItemSets of the CloudCostItemSetRange.
  266. // This function service to aggregate and distribute costs over predefined windows
  267. // are accumulated here so that the resulting CloudCostItem with the 1d window has the correct price for the entire day.
  268. // If all or a portion of the window of the CloudCostItem is outside of the windows of the existing CloudCostItemSets,
  269. // that portion of the CloudCostItem's cost will not be inserted
  270. func (ccisr *CloudCostItemSetRange) LoadCloudCostItem(cloudCostItem *CloudCostItem) {
  271. window := cloudCostItem.Window
  272. if window.IsOpen() {
  273. log.Errorf("CloudCostItemSetRange: LoadCloudCostItem: invalid window %s", window.String())
  274. return
  275. }
  276. totalPct := 0.0
  277. // Distribute cost of the current item across one or more CloudCostItems in
  278. // across each relevant CloudCostItemSet. Stop when the end of the current
  279. // block reaches the item's end time or the end of the range.
  280. for _, ccis := range ccisr.CloudCostItemSets {
  281. setWindow := ccis.Window
  282. // get percent of item window contained in set window
  283. pct := setWindow.GetPercentInWindow(window)
  284. if pct == 0 {
  285. continue
  286. }
  287. cci := cloudCostItem
  288. // If the current set Window only contains a portion of the CloudCostItem Window, insert costs relative to that portion
  289. if pct < 1.0 {
  290. cci = &CloudCostItem{
  291. Properties: cloudCostItem.Properties,
  292. IsKubernetes: cloudCostItem.IsKubernetes,
  293. Window: window.Contract(setWindow),
  294. Cost: cloudCostItem.Cost * pct,
  295. NetCost: cloudCostItem.NetCost * pct,
  296. }
  297. }
  298. err := ccis.Insert(cci)
  299. if err != nil {
  300. log.Errorf("CloudCostItemSetRange: LoadCloudCostItem: failed to load CloudCostItem with key %s and window %s: %s", cci.Key(), ccis.GetWindow().String(), err.Error())
  301. }
  302. // If all cost has been inserted then finish
  303. totalPct += pct
  304. if totalPct >= 1.0 {
  305. return
  306. }
  307. }
  308. }