cloudcostitem.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. Account string `json:"account,omitempty"`
  33. Project string `json:"project,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.Account == that.Account &&
  42. ccip.Project == that.Project &&
  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. Account: ccip.Account,
  52. Project: ccip.Project,
  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.Account, ccip.Project, 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
  65. IsKubernetes bool
  66. Window Window
  67. Cost float64
  68. Credit float64
  69. }
  70. func (cci *CloudCostItem) Clone() *CloudCostItem {
  71. return &CloudCostItem{
  72. Properties: cci.Properties.Clone(),
  73. IsKubernetes: cci.IsKubernetes,
  74. Window: cci.Window.Clone(),
  75. Cost: cci.Cost,
  76. Credit: cci.Credit,
  77. }
  78. }
  79. func (cci *CloudCostItem) Equal(that *CloudCostItem) bool {
  80. if that == nil {
  81. return false
  82. }
  83. return cci.Properties.Equal(that.Properties) &&
  84. cci.IsKubernetes == that.IsKubernetes &&
  85. cci.Window.Equal(that.Window) &&
  86. cci.Cost == that.Cost &&
  87. cci.Credit == that.Credit
  88. }
  89. func (cci *CloudCostItem) Key() string {
  90. return cci.Properties.Key()
  91. }
  92. func (cci *CloudCostItem) add(that *CloudCostItem) {
  93. if cci == nil {
  94. log.Warnf("cannot add to nil CloudCostItem")
  95. return
  96. }
  97. cci.Cost += that.Cost
  98. cci.Credit += that.Credit
  99. cci.Window = cci.Window.Expand(that.Window)
  100. }
  101. type CloudCostItemSet struct {
  102. CloudCostItems map[string]*CloudCostItem
  103. Window Window
  104. Integration string
  105. }
  106. // NewAssetSet instantiates a new AssetSet and, optionally, inserts
  107. // the given list of Assets
  108. func NewCloudCostItemSet(start, end time.Time, cloudCostItems ...*CloudCostItem) *CloudCostItemSet {
  109. ccis := &CloudCostItemSet{
  110. CloudCostItems: map[string]*CloudCostItem{},
  111. Window: NewWindow(&start, &end),
  112. }
  113. for _, cci := range cloudCostItems {
  114. ccis.Insert(cci)
  115. }
  116. return ccis
  117. }
  118. func (ccis *CloudCostItemSet) Equal(that *CloudCostItemSet) bool {
  119. if ccis.Integration != that.Integration {
  120. return false
  121. }
  122. if !ccis.Window.Equal(that.Window) {
  123. return false
  124. }
  125. if len(ccis.CloudCostItems) != len(that.CloudCostItems) {
  126. return false
  127. }
  128. for k, cci := range ccis.CloudCostItems {
  129. tcci, ok := that.CloudCostItems[k]
  130. if !ok {
  131. return false
  132. }
  133. if !cci.Equal(tcci) {
  134. return false
  135. }
  136. }
  137. return true
  138. }
  139. func (ccis *CloudCostItemSet) Filter(filters filter.Filter[*CloudCostItem]) *CloudCostItemSet {
  140. if ccis == nil {
  141. return nil
  142. }
  143. if filters == nil {
  144. return ccis.Clone()
  145. }
  146. result := NewCloudCostItemSet(*ccis.Window.start, *ccis.Window.end)
  147. for _, cci := range ccis.CloudCostItems {
  148. if filters.Matches(cci) {
  149. result.Insert(cci.Clone())
  150. }
  151. }
  152. return result
  153. }
  154. func (ccis *CloudCostItemSet) Insert(that *CloudCostItem) error {
  155. if ccis == nil {
  156. return fmt.Errorf("cannot insert into nil CloudCostItemSet")
  157. }
  158. if that == nil {
  159. return fmt.Errorf("cannot insert nil CloudCostItem into CloudCostItemSet")
  160. }
  161. if ccis.CloudCostItems == nil {
  162. ccis.CloudCostItems = map[string]*CloudCostItem{}
  163. }
  164. // Add the given CloudCostItem to the existing entry, if there is one;
  165. // otherwise just set directly into allocations
  166. if _, ok := ccis.CloudCostItems[that.Key()]; !ok {
  167. ccis.CloudCostItems[that.Key()] = that.Clone()
  168. } else {
  169. ccis.CloudCostItems[that.Key()].add(that)
  170. }
  171. return nil
  172. }
  173. func (ccis *CloudCostItemSet) Clone() *CloudCostItemSet {
  174. items := make(map[string]*CloudCostItem, len(ccis.CloudCostItems))
  175. for k, v := range ccis.CloudCostItems {
  176. items[k] = v.Clone()
  177. }
  178. return &CloudCostItemSet{
  179. CloudCostItems: items,
  180. Integration: ccis.Integration,
  181. Window: ccis.Window.Clone(),
  182. }
  183. }
  184. func (ccis *CloudCostItemSet) IsEmpty() bool {
  185. if ccis == nil {
  186. return true
  187. }
  188. if len(ccis.CloudCostItems) == 0 {
  189. return true
  190. }
  191. return false
  192. }
  193. func (ccis *CloudCostItemSet) Length() int {
  194. if ccis == nil {
  195. return 0
  196. }
  197. return len(ccis.CloudCostItems)
  198. }
  199. func (ccis *CloudCostItemSet) GetWindow() Window {
  200. return ccis.Window
  201. }
  202. func (ccis *CloudCostItemSet) Merge(that *CloudCostItemSet) (*CloudCostItemSet, error) {
  203. if ccis == nil {
  204. return nil, fmt.Errorf("cannot merge nil CloudCostItemSets")
  205. }
  206. if that.IsEmpty() {
  207. return ccis.Clone(), nil
  208. }
  209. if !ccis.Window.Equal(that.Window) {
  210. return nil, fmt.Errorf("cannot merge CloudCostItemSets with different windows")
  211. }
  212. start, end := *ccis.Window.Start(), *ccis.Window.End()
  213. result := NewCloudCostItemSet(start, end)
  214. for _, cci := range ccis.CloudCostItems {
  215. result.Insert(cci)
  216. }
  217. for _, cci := range that.CloudCostItems {
  218. result.Insert(cci)
  219. }
  220. return result, nil
  221. }
  222. // GetCloudCostItemSets
  223. func GetCloudCostItemSets(start time.Time, end time.Time, window time.Duration, integration string) ([]*CloudCostItemSet, error) {
  224. windows, err := GetWindows(start, end, window)
  225. if err != nil {
  226. return nil, err
  227. }
  228. // Build slice of CloudCostItemSet to cover the range
  229. CloudCostItemSets := []*CloudCostItemSet{}
  230. for _, w := range windows {
  231. ccis := NewCloudCostItemSet(*w.Start(), *w.End())
  232. ccis.Integration = integration
  233. CloudCostItemSets = append(CloudCostItemSets, ccis)
  234. }
  235. return CloudCostItemSets, nil
  236. }
  237. // LoadCloudCostItemSets creates and loads CloudCostItems into provided CloudCostItemSets. This method makes it so
  238. // that the input windows do not have to match the one day frame of the Athena queries. CloudCostItems being generated from a
  239. // CUR which may be the identical except for the pricing model used (default, RI or savings plan)
  240. // are accumulated here so that the resulting CloudCostItem with the 1d window has the correct price for the entire day.
  241. func LoadCloudCostItemSets(itemStart time.Time, itemEnd time.Time, properties CloudCostItemProperties, isK8s bool, cost, credit float64, CloudCostItemSets []*CloudCostItemSet) {
  242. // Disperse cost of the current item across one or more CloudCostItems in
  243. // across each relevant CloudCostItemSet. Stop when the end of the current
  244. // block reaches the item's end time or the end of the range.
  245. for _, ccis := range CloudCostItemSets {
  246. pct := ccis.GetWindow().GetPercentInWindow(itemStart, itemEnd)
  247. // Insert an CloudCostItem with that cost into the CloudCostItemSet at the given index
  248. cci := &CloudCostItem{
  249. Properties: properties,
  250. IsKubernetes: isK8s,
  251. Window: ccis.GetWindow(),
  252. Cost: cost * pct,
  253. Credit: credit * pct,
  254. }
  255. err := ccis.Insert(cci)
  256. if err != nil {
  257. log.Errorf("LoadCloudCostItemSets: failed to load CloudCostItem with key %s and window %s: %s", cci.Key(), ccis.GetWindow().String(), err.Error())
  258. }
  259. }
  260. }
  261. type CloudCostItemSetRange struct {
  262. CloudCostItemSets []*CloudCostItemSet `json:"sets"`
  263. Window Window `json:"window"`
  264. }