cloudcost.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. package opencost
  2. import (
  3. "errors"
  4. "fmt"
  5. "time"
  6. "github.com/opencost/opencost/core/pkg/filter"
  7. "github.com/opencost/opencost/core/pkg/filter/ast"
  8. legacyfilter "github.com/opencost/opencost/core/pkg/filter/legacy"
  9. "github.com/opencost/opencost/core/pkg/log"
  10. "github.com/opencost/opencost/core/pkg/util/timeutil"
  11. )
  12. // CloudCost represents a CUR line item, identifying a cloud resource and
  13. // its cost over some period of time.
  14. type CloudCost struct {
  15. Properties *CloudCostProperties `json:"properties"`
  16. Window Window `json:"window"`
  17. ListCost CostMetric `json:"listCost"`
  18. NetCost CostMetric `json:"netCost"`
  19. AmortizedNetCost CostMetric `json:"amortizedNetCost"`
  20. InvoicedCost CostMetric `json:"invoicedCost"`
  21. AmortizedCost CostMetric `json:"amortizedCost"`
  22. }
  23. // NewCloudCost instantiates a new CloudCost
  24. func NewCloudCost(start, end time.Time, ccProperties *CloudCostProperties, kubernetesPercent, listCost, netCost, amortizedNetCost, invoicedCost, amortizedCost float64) *CloudCost {
  25. return &CloudCost{
  26. Properties: ccProperties,
  27. Window: NewWindow(&start, &end),
  28. ListCost: CostMetric{
  29. Cost: listCost,
  30. KubernetesPercent: kubernetesPercent,
  31. },
  32. NetCost: CostMetric{
  33. Cost: netCost,
  34. KubernetesPercent: kubernetesPercent,
  35. },
  36. AmortizedNetCost: CostMetric{
  37. Cost: amortizedNetCost,
  38. KubernetesPercent: kubernetesPercent,
  39. },
  40. InvoicedCost: CostMetric{
  41. Cost: invoicedCost,
  42. KubernetesPercent: kubernetesPercent,
  43. },
  44. AmortizedCost: CostMetric{
  45. Cost: amortizedCost,
  46. KubernetesPercent: kubernetesPercent,
  47. },
  48. }
  49. }
  50. func (cc *CloudCost) Clone() *CloudCost {
  51. return &CloudCost{
  52. Properties: cc.Properties.Clone(),
  53. Window: cc.Window.Clone(),
  54. ListCost: cc.ListCost.Clone(),
  55. NetCost: cc.NetCost.Clone(),
  56. AmortizedNetCost: cc.AmortizedNetCost.Clone(),
  57. InvoicedCost: cc.InvoicedCost.Clone(),
  58. AmortizedCost: cc.AmortizedCost.Clone(),
  59. }
  60. }
  61. func (cc *CloudCost) Equal(that *CloudCost) bool {
  62. if that == nil {
  63. return false
  64. }
  65. return cc.Properties.Equal(that.Properties) &&
  66. cc.Window.Equal(that.Window) &&
  67. cc.ListCost.Equal(that.ListCost) &&
  68. cc.NetCost.Equal(that.NetCost) &&
  69. cc.AmortizedNetCost.Equal(that.AmortizedNetCost) &&
  70. cc.InvoicedCost.Equal(that.InvoicedCost) &&
  71. cc.AmortizedCost.Equal(that.AmortizedCost)
  72. }
  73. func (cc *CloudCost) add(that *CloudCost) {
  74. if cc == nil {
  75. log.Warnf("cannot add to nil CloudCost")
  76. return
  77. }
  78. // Preserve properties of cloud cost that are matching between the two CloudCost
  79. cc.Properties = cc.Properties.Intersection(that.Properties)
  80. cc.ListCost = cc.ListCost.add(that.ListCost)
  81. cc.NetCost = cc.NetCost.add(that.NetCost)
  82. cc.AmortizedNetCost = cc.AmortizedNetCost.add(that.AmortizedNetCost)
  83. cc.InvoicedCost = cc.InvoicedCost.add(that.InvoicedCost)
  84. cc.AmortizedCost = cc.AmortizedCost.add(that.AmortizedCost)
  85. cc.Window = cc.Window.Expand(that.Window)
  86. }
  87. func (cc *CloudCost) StringProperty(prop string) (string, error) {
  88. if cc == nil {
  89. return "", nil
  90. }
  91. switch prop {
  92. case CloudCostInvoiceEntityIDProp:
  93. return cc.Properties.InvoiceEntityID, nil
  94. case CloudCostAccountIDProp:
  95. return cc.Properties.AccountID, nil
  96. case CloudCostProviderProp:
  97. return cc.Properties.Provider, nil
  98. case CloudCostProviderIDProp:
  99. return cc.Properties.ProviderID, nil
  100. case CloudCostServiceProp:
  101. return cc.Properties.Service, nil
  102. case CloudCostCategoryProp:
  103. return cc.Properties.Category, nil
  104. default:
  105. return "", fmt.Errorf("invalid property name: %s", prop)
  106. }
  107. }
  108. func (cc *CloudCost) StringMapProperty(property string) (map[string]string, error) {
  109. switch property {
  110. case CloudCostLabelProp:
  111. if cc.Properties == nil {
  112. return nil, nil
  113. }
  114. return cc.Properties.Labels, nil
  115. default:
  116. return nil, fmt.Errorf("CloudCost: StringMapProperty: invalid property name: %s", property)
  117. }
  118. }
  119. func (cc *CloudCost) GetCostMetric(costMetricName CostMetricName) (CostMetric, error) {
  120. switch costMetricName {
  121. case CostMetricListCost:
  122. return cc.ListCost, nil
  123. case CostMetricNetCost:
  124. return cc.NetCost, nil
  125. case CostMetricAmortizedNetCost:
  126. return cc.AmortizedNetCost, nil
  127. case CostMetricInvoicedCost:
  128. return cc.InvoicedCost, nil
  129. case CostMetricAmortizedCost:
  130. return cc.AmortizedCost, nil
  131. }
  132. return CostMetric{}, fmt.Errorf("invalid Cost Metric: %s", costMetricName)
  133. }
  134. // WeightCostMetrics weights all the cost metrics with the given weightedAverage
  135. func (cc *CloudCost) WeightCostMetrics(weightedAverge float64) {
  136. cc.ListCost.Cost *= weightedAverge
  137. cc.NetCost.Cost *= weightedAverge
  138. cc.AmortizedNetCost.Cost *= weightedAverge
  139. cc.InvoicedCost.Cost *= weightedAverge
  140. cc.AmortizedCost.Cost *= weightedAverge
  141. }
  142. // CloudCostSet follows the established set pattern of windowed data types. It has addition metadata types that can be
  143. // used to preserve data consistency and be used for validation.
  144. // - Integration is the ID for the integration that a CloudCostSet was sourced from, this value is cleared if when a
  145. // set is joined with another with a different key
  146. // - AggregationProperties is set by the Aggregate function and ensures that any additional inserts are keyed correctly
  147. type CloudCostSet struct {
  148. CloudCosts map[string]*CloudCost `json:"cloudCosts"`
  149. Window Window `json:"window"`
  150. Integration string `json:"-"`
  151. AggregationProperties []string `json:"aggregationProperties"`
  152. }
  153. // NewCloudCostSet instantiates a new CloudCostSet and, optionally, inserts
  154. // the given list of CloudCosts
  155. func NewCloudCostSet(start, end time.Time, cloudCosts ...*CloudCost) *CloudCostSet {
  156. ccs := &CloudCostSet{
  157. CloudCosts: map[string]*CloudCost{},
  158. Window: NewWindow(&start, &end),
  159. }
  160. for _, cc := range cloudCosts {
  161. ccs.Insert(cc)
  162. }
  163. return ccs
  164. }
  165. func (ccs *CloudCostSet) Aggregate(props []string) (*CloudCostSet, error) {
  166. if ccs == nil {
  167. return nil, errors.New("cannot aggregate a nil CloudCostSet")
  168. }
  169. if ccs.Window.IsOpen() {
  170. return nil, fmt.Errorf("cannot aggregate a CloudCostSet with an open window: %s", ccs.Window)
  171. }
  172. // Create a new result set, with the given aggregation property
  173. result := ccs.cloneSet()
  174. result.AggregationProperties = props
  175. // Insert clones of each item in the set, keyed by the given property.
  176. // The underlying insert logic will add binned items together.
  177. for name, cc := range ccs.CloudCosts {
  178. ccClone := cc.Clone()
  179. err := result.Insert(ccClone)
  180. if err != nil {
  181. return nil, fmt.Errorf("error aggregating %s by %v: %s", name, props, err)
  182. }
  183. }
  184. return result, nil
  185. }
  186. func (ccs *CloudCostSet) Accumulate(that *CloudCostSet) (*CloudCostSet, error) {
  187. if ccs.IsEmpty() {
  188. return that.Clone(), nil
  189. }
  190. acc := ccs.Clone()
  191. err := acc.accumulateInto(that)
  192. if err == nil {
  193. return nil, err
  194. }
  195. return acc, nil
  196. }
  197. // accumulateInto accumulates a the arg CloudCostSet Into the receiver
  198. func (ccs *CloudCostSet) accumulateInto(that *CloudCostSet) error {
  199. if ccs == nil {
  200. return fmt.Errorf("CloudCost: cannot accumulate into nil set")
  201. }
  202. if that.IsEmpty() {
  203. return nil
  204. }
  205. if ccs.Integration != that.Integration {
  206. ccs.Integration = ""
  207. }
  208. ccs.Window.Expand(that.Window)
  209. for _, cc := range that.CloudCosts {
  210. err := ccs.Insert(cc)
  211. if err != nil {
  212. return err
  213. }
  214. }
  215. return nil
  216. }
  217. func (ccs *CloudCostSet) Equal(that *CloudCostSet) bool {
  218. if ccs.Integration != that.Integration {
  219. return false
  220. }
  221. if !ccs.Window.Equal(that.Window) {
  222. return false
  223. }
  224. // Check Aggregation Properties, slice order is grounds for inequality
  225. if len(ccs.AggregationProperties) != len(that.AggregationProperties) {
  226. return false
  227. }
  228. for i, prop := range ccs.AggregationProperties {
  229. if that.AggregationProperties[i] != prop {
  230. return false
  231. }
  232. }
  233. if len(ccs.CloudCosts) != len(that.CloudCosts) {
  234. return false
  235. }
  236. for k, cc := range ccs.CloudCosts {
  237. if tcc, ok := that.CloudCosts[k]; !ok || !cc.Equal(tcc) {
  238. return false
  239. }
  240. }
  241. return true
  242. }
  243. func (ccs *CloudCostSet) Filter(filters legacyfilter.Filter[*CloudCost]) *CloudCostSet {
  244. if ccs == nil {
  245. return nil
  246. }
  247. if filters == nil {
  248. return ccs.Clone()
  249. }
  250. result := ccs.cloneSet()
  251. for _, cc := range ccs.CloudCosts {
  252. if filters.Matches(cc) {
  253. result.Insert(cc.Clone())
  254. }
  255. }
  256. return result
  257. }
  258. func (ccs *CloudCostSet) Filter21(filters filter.Filter) (*CloudCostSet, error) {
  259. if ccs == nil {
  260. return nil, nil
  261. }
  262. if filters == nil {
  263. return ccs.Clone(), nil
  264. }
  265. compiler := NewCloudCostMatchCompiler()
  266. var err error
  267. matcher, err := compiler.Compile(filters)
  268. if err != nil {
  269. return ccs.Clone(), fmt.Errorf("compiling filter '%s': %w", ast.ToPreOrderShortString(filters), err)
  270. }
  271. if matcher == nil {
  272. return ccs.Clone(), fmt.Errorf("unexpected nil filter")
  273. }
  274. result := ccs.cloneSet()
  275. for _, cc := range ccs.CloudCosts {
  276. if matcher.Matches(cc) {
  277. result.Insert(cc.Clone())
  278. }
  279. }
  280. return result, nil
  281. }
  282. // Insert adds a CloudCost to a CloudCostSet using its AggregationProperties and LabelConfig
  283. // to determine the key where it will be inserted
  284. func (ccs *CloudCostSet) Insert(cc *CloudCost) error {
  285. if ccs == nil {
  286. return fmt.Errorf("cannot insert into nil CloudCostSet")
  287. }
  288. if cc == nil {
  289. return fmt.Errorf("cannot insert nil CloudCost into CloudCostSet")
  290. }
  291. if ccs.CloudCosts == nil {
  292. ccs.CloudCosts = map[string]*CloudCost{}
  293. }
  294. ccKey := cc.Properties.GenerateKey(ccs.AggregationProperties)
  295. // Add the given CloudCost to the existing entry, if there is one;
  296. // otherwise just set directly into allocations
  297. if _, ok := ccs.CloudCosts[ccKey]; !ok {
  298. ccs.CloudCosts[ccKey] = cc.Clone()
  299. } else {
  300. ccs.CloudCosts[ccKey].add(cc)
  301. }
  302. return nil
  303. }
  304. func (ccs *CloudCostSet) Clone() *CloudCostSet {
  305. cloudCosts := make(map[string]*CloudCost, len(ccs.CloudCosts))
  306. for k, v := range ccs.CloudCosts {
  307. cloudCosts[k] = v.Clone()
  308. }
  309. cloneCCS := ccs.cloneSet()
  310. cloneCCS.CloudCosts = cloudCosts
  311. return cloneCCS
  312. }
  313. // cloneSet creates a copy of the receiver without any of its CloudCosts
  314. func (ccs *CloudCostSet) cloneSet() *CloudCostSet {
  315. var aggProps []string
  316. if ccs.AggregationProperties != nil {
  317. aggProps = make([]string, len(ccs.AggregationProperties))
  318. for i, v := range ccs.AggregationProperties {
  319. aggProps[i] = v
  320. }
  321. }
  322. return &CloudCostSet{
  323. CloudCosts: make(map[string]*CloudCost),
  324. Integration: ccs.Integration,
  325. AggregationProperties: aggProps,
  326. Window: ccs.Window.Clone(),
  327. }
  328. }
  329. func (ccs *CloudCostSet) IsEmpty() bool {
  330. if ccs == nil {
  331. return true
  332. }
  333. if len(ccs.CloudCosts) == 0 {
  334. return true
  335. }
  336. return false
  337. }
  338. func (ccs *CloudCostSet) Length() int {
  339. if ccs == nil {
  340. return 0
  341. }
  342. return len(ccs.CloudCosts)
  343. }
  344. func (ccs *CloudCostSet) GetWindow() Window {
  345. return ccs.Window
  346. }
  347. func (ccs *CloudCostSet) Merge(that *CloudCostSet) (*CloudCostSet, error) {
  348. if ccs == nil {
  349. return nil, fmt.Errorf("cannot merge nil CloudCostSets")
  350. }
  351. if that.IsEmpty() {
  352. return ccs.Clone(), nil
  353. }
  354. if !ccs.Window.Equal(that.Window) {
  355. return nil, fmt.Errorf("cannot merge CloudCostSets with different windows")
  356. }
  357. result := ccs.cloneSet()
  358. // clear integration if it is not equal
  359. if ccs.Integration != that.Integration {
  360. result.Integration = ""
  361. }
  362. for _, cc := range ccs.CloudCosts {
  363. result.Insert(cc)
  364. }
  365. for _, cc := range that.CloudCosts {
  366. result.Insert(cc)
  367. }
  368. return result, nil
  369. }
  370. type CloudCostSetRange struct {
  371. CloudCostSets []*CloudCostSet `json:"sets"`
  372. Window Window `json:"window"`
  373. }
  374. // NewCloudCostSetRange create a CloudCostSetRange containing CloudCostSets with windows of equal duration
  375. // the duration between start and end must be divisible by the window duration argument
  376. func NewCloudCostSetRange(start time.Time, end time.Time, accumOpt AccumulateOption, integration string) (*CloudCostSetRange, error) {
  377. windows, err := NewClosedWindow(start.UTC(), end.UTC()).GetAccumulateWindows(accumOpt)
  378. if err != nil {
  379. return nil, err
  380. }
  381. // Build slice of CloudCostSet to cover the range
  382. cloudCostItemSets := make([]*CloudCostSet, len(windows))
  383. for i, w := range windows {
  384. ccs := NewCloudCostSet(*w.Start(), *w.End())
  385. ccs.Integration = integration
  386. cloudCostItemSets[i] = ccs
  387. }
  388. return &CloudCostSetRange{
  389. CloudCostSets: cloudCostItemSets,
  390. }, nil
  391. }
  392. func (ccsr *CloudCostSetRange) Clone() *CloudCostSetRange {
  393. ccsSlice := make([]*CloudCostSet, len(ccsr.CloudCostSets))
  394. for i, ccs := range ccsr.CloudCostSets {
  395. ccsSlice[i] = ccs.Clone()
  396. }
  397. return &CloudCostSetRange{
  398. CloudCostSets: ccsSlice,
  399. }
  400. }
  401. func (ccsr *CloudCostSetRange) IsEmpty() bool {
  402. for _, ccs := range ccsr.CloudCostSets {
  403. if !ccs.IsEmpty() {
  404. return false
  405. }
  406. }
  407. return true
  408. }
  409. // accumulate sums each CloudCostSet in the given range, returning a single cumulative
  410. // CloudCostSet for the entire range.
  411. func (ccsr *CloudCostSetRange) AccumulateAll() (*CloudCostSet, error) {
  412. var cloudCostSet *CloudCostSet
  413. var err error
  414. if ccsr == nil {
  415. return nil, fmt.Errorf("nil CloudCostSetRange in accumulation")
  416. }
  417. if len(ccsr.CloudCostSets) == 0 {
  418. return nil, fmt.Errorf("CloudCostSetRange has empty CloudCostSet in accumulation")
  419. }
  420. for _, ccs := range ccsr.CloudCostSets {
  421. if cloudCostSet == nil {
  422. cloudCostSet = ccs.Clone()
  423. continue
  424. }
  425. err = cloudCostSet.accumulateInto(ccs)
  426. if err != nil {
  427. return nil, err
  428. }
  429. }
  430. return cloudCostSet, nil
  431. }
  432. // Accumulate sums CloudCostSets based on the AccumulateOption (calendar week or calendar month).
  433. // The accumulated set is determined by the start of the window of the allocation set.
  434. func (ccsr *CloudCostSetRange) Accumulate(accumulateBy AccumulateOption) (*CloudCostSetRange, error) {
  435. switch accumulateBy {
  436. case AccumulateOptionNone:
  437. return ccsr.accumulateByNone()
  438. case AccumulateOptionAll:
  439. return ccsr.accumulateByAll()
  440. case AccumulateOptionHour:
  441. return ccsr.accumulateByHour()
  442. case AccumulateOptionDay:
  443. return ccsr.accumulateByDay()
  444. case AccumulateOptionWeek:
  445. return ccsr.accumulateByWeek()
  446. case AccumulateOptionMonth:
  447. return ccsr.accumulateByMonth()
  448. default:
  449. // ideally, this should never happen
  450. return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
  451. }
  452. }
  453. func (ccsr *CloudCostSetRange) accumulateByAll() (*CloudCostSetRange, error) {
  454. ccs, err := ccsr.AccumulateAll()
  455. if err != nil {
  456. return nil, fmt.Errorf("error accumulating all:%s", err)
  457. }
  458. accumulated := &CloudCostSetRange{
  459. CloudCostSets: []*CloudCostSet{ccs},
  460. }
  461. return accumulated, nil
  462. }
  463. func (ccsr *CloudCostSetRange) accumulateByNone() (*CloudCostSetRange, error) {
  464. return ccsr.Clone(), nil
  465. }
  466. func (ccsr *CloudCostSetRange) accumulateByHour() (*CloudCostSetRange, error) {
  467. // ensure that the summary allocation sets have a 1-hour window, if a set exists
  468. if len(ccsr.CloudCostSets) > 0 && ccsr.CloudCostSets[0].Window.Duration() != time.Hour {
  469. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", ccsr.CloudCostSets[0].Window.Duration())
  470. }
  471. return ccsr.Clone(), nil
  472. }
  473. func (ccsr *CloudCostSetRange) accumulateByDay() (*CloudCostSetRange, error) {
  474. // if the allocation set window is 1-day, just return the existing allocation set range
  475. if len(ccsr.CloudCostSets) > 0 && ccsr.CloudCostSets[0].Window.Duration() == time.Hour*24 {
  476. return ccsr, nil
  477. }
  478. var toAccumulate *CloudCostSetRange
  479. result := &CloudCostSetRange{}
  480. for i, ccs := range ccsr.CloudCostSets {
  481. if ccs.Window.Duration() != time.Hour {
  482. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", ccs.Window.Duration())
  483. }
  484. hour := ccs.Window.Start().Hour()
  485. if toAccumulate == nil {
  486. toAccumulate = &CloudCostSetRange{}
  487. ccs = ccs.Clone()
  488. }
  489. toAccumulate.Append(ccs)
  490. accumulated, err := toAccumulate.accumulateByAll()
  491. if err != nil {
  492. return nil, fmt.Errorf("error accumulating result: %s", err)
  493. }
  494. toAccumulate = accumulated
  495. if hour == 23 || i == len(ccsr.CloudCostSets)-1 {
  496. if length := len(toAccumulate.CloudCostSets); length != 1 {
  497. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  498. }
  499. result.Append(toAccumulate.CloudCostSets[0])
  500. toAccumulate = nil
  501. }
  502. }
  503. return result, nil
  504. }
  505. func (ccsr *CloudCostSetRange) accumulateByWeek() (*CloudCostSetRange, error) {
  506. if len(ccsr.CloudCostSets) > 0 && ccsr.CloudCostSets[0].Window.Duration() == timeutil.Week {
  507. return ccsr, nil
  508. }
  509. var toAccumulate *CloudCostSetRange
  510. result := &CloudCostSetRange{}
  511. for i, css := range ccsr.CloudCostSets {
  512. if css.Window.Duration() != time.Hour*24 {
  513. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", css.Window.Duration())
  514. }
  515. dayOfWeek := css.Window.Start().Weekday()
  516. if toAccumulate == nil {
  517. toAccumulate = &CloudCostSetRange{}
  518. css = css.Clone()
  519. }
  520. toAccumulate.Append(css)
  521. accumulated, err := toAccumulate.accumulateByAll()
  522. if err != nil {
  523. return nil, fmt.Errorf("error accumulating result: %s", err)
  524. }
  525. toAccumulate = accumulated
  526. // current assumption is the week always ends on Saturday, or there are no more allocation sets
  527. if dayOfWeek == time.Saturday || i == len(ccsr.CloudCostSets)-1 {
  528. if length := len(toAccumulate.CloudCostSets); length != 1 {
  529. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  530. }
  531. result.Append(toAccumulate.CloudCostSets[0])
  532. toAccumulate = nil
  533. }
  534. }
  535. return result, nil
  536. }
  537. func (ccsr *CloudCostSetRange) accumulateByMonth() (*CloudCostSetRange, error) {
  538. var toAccumulate *CloudCostSetRange
  539. result := &CloudCostSetRange{}
  540. for i, css := range ccsr.CloudCostSets {
  541. if css.Window.Duration() != time.Hour*24 {
  542. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", css.Window.Duration())
  543. }
  544. _, month, _ := css.Window.Start().Date()
  545. _, nextDayMonth, _ := css.Window.Start().Add(time.Hour * 24).Date()
  546. if toAccumulate == nil {
  547. toAccumulate = &CloudCostSetRange{}
  548. css = css.Clone()
  549. }
  550. toAccumulate.Append(css)
  551. accumulated, err := toAccumulate.accumulateByAll()
  552. if err != nil {
  553. return nil, fmt.Errorf("error accumulating result: %s", err)
  554. }
  555. toAccumulate = accumulated
  556. // either the month has ended, or there are no more allocation sets
  557. if month != nextDayMonth || i == len(ccsr.CloudCostSets)-1 {
  558. if length := len(toAccumulate.CloudCostSets); length != 1 {
  559. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  560. }
  561. result.Append(toAccumulate.CloudCostSets[0])
  562. toAccumulate = nil
  563. }
  564. }
  565. return result, nil
  566. }
  567. // Append appends the given CloudCostSet to the end of the range. It does not
  568. // validate whether or not that violates window continuity.
  569. func (ccsr *CloudCostSetRange) Append(that *CloudCostSet) {
  570. ccsr.CloudCostSets = append(ccsr.CloudCostSets, that)
  571. }
  572. // LoadCloudCost loads CloudCosts into existing CloudCostSets of the CloudCostSetRange.
  573. // This function service to aggregate and distribute costs over predefined windows
  574. // are accumulated here so that the resulting CloudCost with the 1d window has the correct price for the entire day.
  575. // If all or a portion of the window of the CloudCost is outside of the windows of the existing CloudCostSets,
  576. // that portion of the CloudCost's cost will not be inserted
  577. func (ccsr *CloudCostSetRange) LoadCloudCost(cloudCost *CloudCost) {
  578. window := cloudCost.Window
  579. if window.IsOpen() {
  580. log.Errorf("CloudCostSetRange: LoadCloudCost: invalid window %s", window.String())
  581. return
  582. }
  583. totalPct := 0.0
  584. // Distribute cost of the current item across one or more CloudCosts in
  585. // across each relevant CloudCostSet. Stop when the end of the current
  586. // block reaches the item's end time or the end of the range.
  587. for _, ccs := range ccsr.CloudCostSets {
  588. setWindow := ccs.Window
  589. // get percent of item window contained in set window
  590. pct := setWindow.GetPercentInWindow(window)
  591. if pct == 0 {
  592. continue
  593. }
  594. cc := cloudCost
  595. // If the current set Window only contains a portion of the CloudCost Window, insert costs relative to that portion
  596. if pct < 1.0 {
  597. cc = &CloudCost{
  598. Properties: cloudCost.Properties,
  599. Window: window.Contract(setWindow),
  600. ListCost: cloudCost.ListCost.percent(pct),
  601. NetCost: cloudCost.NetCost.percent(pct),
  602. AmortizedNetCost: cloudCost.AmortizedNetCost.percent(pct),
  603. InvoicedCost: cloudCost.InvoicedCost.percent(pct),
  604. AmortizedCost: cloudCost.AmortizedCost.percent(pct),
  605. }
  606. }
  607. err := ccs.Insert(cc)
  608. if err != nil {
  609. log.Errorf("CloudCostSetRange: LoadCloudCost: failed to load CloudCost with window %s: %s", setWindow.String(), err.Error())
  610. }
  611. // If all cost has been inserted, then there is no need to check later days in the range
  612. totalPct += pct
  613. if totalPct >= 1.0 {
  614. return
  615. }
  616. }
  617. }