cloudcost.go 20 KB

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