2
0

cloudcost.go 20 KB

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