cloudcost.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  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. // If the Aggregation properties is not set the returned key will be a hash of the properties values
  295. ccKey := cc.Properties.GenerateKey(ccs.AggregationProperties)
  296. // Add the given CloudCost to the existing entry, if there is one;
  297. // otherwise just set directly into allocations
  298. if _, ok := ccs.CloudCosts[ccKey]; !ok {
  299. ccs.CloudCosts[ccKey] = cc.Clone()
  300. } else {
  301. ccs.CloudCosts[ccKey].add(cc)
  302. }
  303. return nil
  304. }
  305. func (ccs *CloudCostSet) Clone() *CloudCostSet {
  306. cloudCosts := make(map[string]*CloudCost, len(ccs.CloudCosts))
  307. for k, v := range ccs.CloudCosts {
  308. cloudCosts[k] = v.Clone()
  309. }
  310. cloneCCS := ccs.cloneSet()
  311. cloneCCS.CloudCosts = cloudCosts
  312. return cloneCCS
  313. }
  314. // cloneSet creates a copy of the receiver without any of its CloudCosts
  315. func (ccs *CloudCostSet) cloneSet() *CloudCostSet {
  316. var aggProps []string
  317. if ccs.AggregationProperties != nil {
  318. aggProps = make([]string, len(ccs.AggregationProperties))
  319. for i, v := range ccs.AggregationProperties {
  320. aggProps[i] = v
  321. }
  322. }
  323. return &CloudCostSet{
  324. CloudCosts: make(map[string]*CloudCost),
  325. Integration: ccs.Integration,
  326. AggregationProperties: aggProps,
  327. Window: ccs.Window.Clone(),
  328. }
  329. }
  330. func (ccs *CloudCostSet) IsEmpty() bool {
  331. if ccs == nil {
  332. return true
  333. }
  334. if len(ccs.CloudCosts) == 0 {
  335. return true
  336. }
  337. return false
  338. }
  339. func (ccs *CloudCostSet) Length() int {
  340. if ccs == nil {
  341. return 0
  342. }
  343. return len(ccs.CloudCosts)
  344. }
  345. func (ccs *CloudCostSet) GetWindow() Window {
  346. return ccs.Window
  347. }
  348. func (ccs *CloudCostSet) Merge(that *CloudCostSet) (*CloudCostSet, error) {
  349. if ccs == nil {
  350. return nil, fmt.Errorf("cannot merge nil CloudCostSets")
  351. }
  352. if that.IsEmpty() {
  353. return ccs.Clone(), nil
  354. }
  355. if !ccs.Window.Equal(that.Window) {
  356. return nil, fmt.Errorf("cannot merge CloudCostSets with different windows")
  357. }
  358. result := ccs.cloneSet()
  359. // clear integration if it is not equal
  360. if ccs.Integration != that.Integration {
  361. result.Integration = ""
  362. }
  363. for _, cc := range ccs.CloudCosts {
  364. result.Insert(cc)
  365. }
  366. for _, cc := range that.CloudCosts {
  367. result.Insert(cc)
  368. }
  369. return result, nil
  370. }
  371. type CloudCostSetRange struct {
  372. CloudCostSets []*CloudCostSet `json:"sets"`
  373. Window Window `json:"window"`
  374. }
  375. // NewCloudCostSetRange create a CloudCostSetRange containing CloudCostSets with windows of equal duration
  376. // the duration between start and end must be divisible by the window duration argument
  377. func NewCloudCostSetRange(start time.Time, end time.Time, accumOpt AccumulateOption, integration string) (*CloudCostSetRange, error) {
  378. windows, err := NewClosedWindow(start.UTC(), end.UTC()).GetAccumulateWindows(accumOpt)
  379. if err != nil {
  380. return nil, err
  381. }
  382. // Build slice of CloudCostSet to cover the range
  383. cloudCostItemSets := make([]*CloudCostSet, len(windows))
  384. for i, w := range windows {
  385. ccs := NewCloudCostSet(*w.Start(), *w.End())
  386. ccs.Integration = integration
  387. cloudCostItemSets[i] = ccs
  388. }
  389. return &CloudCostSetRange{
  390. CloudCostSets: cloudCostItemSets,
  391. }, nil
  392. }
  393. func (ccsr *CloudCostSetRange) Clone() *CloudCostSetRange {
  394. ccsSlice := make([]*CloudCostSet, len(ccsr.CloudCostSets))
  395. for i, ccs := range ccsr.CloudCostSets {
  396. ccsSlice[i] = ccs.Clone()
  397. }
  398. return &CloudCostSetRange{
  399. CloudCostSets: ccsSlice,
  400. }
  401. }
  402. func (ccsr *CloudCostSetRange) IsEmpty() bool {
  403. for _, ccs := range ccsr.CloudCostSets {
  404. if !ccs.IsEmpty() {
  405. return false
  406. }
  407. }
  408. return true
  409. }
  410. // accumulate sums each CloudCostSet in the given range, returning a single cumulative
  411. // CloudCostSet for the entire range.
  412. func (ccsr *CloudCostSetRange) AccumulateAll() (*CloudCostSet, error) {
  413. var cloudCostSet *CloudCostSet
  414. var err error
  415. if ccsr == nil {
  416. return nil, fmt.Errorf("nil CloudCostSetRange in accumulation")
  417. }
  418. if len(ccsr.CloudCostSets) == 0 {
  419. return nil, fmt.Errorf("CloudCostSetRange has empty CloudCostSet in accumulation")
  420. }
  421. for _, ccs := range ccsr.CloudCostSets {
  422. if cloudCostSet == nil {
  423. cloudCostSet = ccs.Clone()
  424. continue
  425. }
  426. err = cloudCostSet.accumulateInto(ccs)
  427. if err != nil {
  428. return nil, err
  429. }
  430. }
  431. return cloudCostSet, nil
  432. }
  433. // Accumulate sums CloudCostSets based on the AccumulateOption (calendar week or calendar month).
  434. // The accumulated set is determined by the start of the window of the allocation set.
  435. func (ccsr *CloudCostSetRange) Accumulate(accumulateBy AccumulateOption) (*CloudCostSetRange, error) {
  436. switch accumulateBy {
  437. case AccumulateOptionNone:
  438. return ccsr.accumulateByNone()
  439. case AccumulateOptionAll:
  440. return ccsr.accumulateByAll()
  441. case AccumulateOptionHour:
  442. return ccsr.accumulateByHour()
  443. case AccumulateOptionDay:
  444. return ccsr.accumulateByDay()
  445. case AccumulateOptionWeek:
  446. return ccsr.accumulateByWeek()
  447. case AccumulateOptionMonth:
  448. return ccsr.accumulateByMonth()
  449. default:
  450. // ideally, this should never happen
  451. return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
  452. }
  453. }
  454. func (ccsr *CloudCostSetRange) accumulateByAll() (*CloudCostSetRange, error) {
  455. ccs, err := ccsr.AccumulateAll()
  456. if err != nil {
  457. return nil, fmt.Errorf("error accumulating all:%s", err)
  458. }
  459. accumulated := &CloudCostSetRange{
  460. CloudCostSets: []*CloudCostSet{ccs},
  461. }
  462. return accumulated, nil
  463. }
  464. func (ccsr *CloudCostSetRange) accumulateByNone() (*CloudCostSetRange, error) {
  465. return ccsr.Clone(), nil
  466. }
  467. func (ccsr *CloudCostSetRange) accumulateByHour() (*CloudCostSetRange, error) {
  468. // ensure that the summary allocation sets have a 1-hour window, if a set exists
  469. if len(ccsr.CloudCostSets) > 0 && ccsr.CloudCostSets[0].Window.Duration() != time.Hour {
  470. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", ccsr.CloudCostSets[0].Window.Duration())
  471. }
  472. return ccsr.Clone(), nil
  473. }
  474. func (ccsr *CloudCostSetRange) accumulateByDay() (*CloudCostSetRange, error) {
  475. // if the allocation set window is 1-day, just return the existing allocation set range
  476. if len(ccsr.CloudCostSets) > 0 && ccsr.CloudCostSets[0].Window.Duration() == time.Hour*24 {
  477. return ccsr, nil
  478. }
  479. var toAccumulate *CloudCostSetRange
  480. result := &CloudCostSetRange{}
  481. for i, ccs := range ccsr.CloudCostSets {
  482. if ccs.Window.Duration() != time.Hour {
  483. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", ccs.Window.Duration())
  484. }
  485. hour := ccs.Window.Start().Hour()
  486. if toAccumulate == nil {
  487. toAccumulate = &CloudCostSetRange{}
  488. ccs = ccs.Clone()
  489. }
  490. toAccumulate.Append(ccs)
  491. accumulated, err := toAccumulate.accumulateByAll()
  492. if err != nil {
  493. return nil, fmt.Errorf("error accumulating result: %s", err)
  494. }
  495. toAccumulate = accumulated
  496. if hour == 23 || i == len(ccsr.CloudCostSets)-1 {
  497. if length := len(toAccumulate.CloudCostSets); length != 1 {
  498. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  499. }
  500. result.Append(toAccumulate.CloudCostSets[0])
  501. toAccumulate = nil
  502. }
  503. }
  504. return result, nil
  505. }
  506. func (ccsr *CloudCostSetRange) accumulateByWeek() (*CloudCostSetRange, error) {
  507. if len(ccsr.CloudCostSets) > 0 && ccsr.CloudCostSets[0].Window.Duration() == timeutil.Week {
  508. return ccsr, nil
  509. }
  510. var toAccumulate *CloudCostSetRange
  511. result := &CloudCostSetRange{}
  512. for i, css := range ccsr.CloudCostSets {
  513. if css.Window.Duration() != time.Hour*24 {
  514. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", css.Window.Duration())
  515. }
  516. dayOfWeek := css.Window.Start().Weekday()
  517. if toAccumulate == nil {
  518. toAccumulate = &CloudCostSetRange{}
  519. css = css.Clone()
  520. }
  521. toAccumulate.Append(css)
  522. accumulated, err := toAccumulate.accumulateByAll()
  523. if err != nil {
  524. return nil, fmt.Errorf("error accumulating result: %s", err)
  525. }
  526. toAccumulate = accumulated
  527. // current assumption is the week always ends on Saturday, or there are no more allocation sets
  528. if dayOfWeek == time.Saturday || i == len(ccsr.CloudCostSets)-1 {
  529. if length := len(toAccumulate.CloudCostSets); length != 1 {
  530. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  531. }
  532. result.Append(toAccumulate.CloudCostSets[0])
  533. toAccumulate = nil
  534. }
  535. }
  536. return result, nil
  537. }
  538. func (ccsr *CloudCostSetRange) accumulateByMonth() (*CloudCostSetRange, error) {
  539. var toAccumulate *CloudCostSetRange
  540. result := &CloudCostSetRange{}
  541. for i, css := range ccsr.CloudCostSets {
  542. if css.Window.Duration() != time.Hour*24 {
  543. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", css.Window.Duration())
  544. }
  545. _, month, _ := css.Window.Start().Date()
  546. _, nextDayMonth, _ := css.Window.Start().Add(time.Hour * 24).Date()
  547. if toAccumulate == nil {
  548. toAccumulate = &CloudCostSetRange{}
  549. css = css.Clone()
  550. }
  551. toAccumulate.Append(css)
  552. accumulated, err := toAccumulate.accumulateByAll()
  553. if err != nil {
  554. return nil, fmt.Errorf("error accumulating result: %s", err)
  555. }
  556. toAccumulate = accumulated
  557. // either the month has ended, or there are no more allocation sets
  558. if month != nextDayMonth || i == len(ccsr.CloudCostSets)-1 {
  559. if length := len(toAccumulate.CloudCostSets); length != 1 {
  560. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  561. }
  562. result.Append(toAccumulate.CloudCostSets[0])
  563. toAccumulate = nil
  564. }
  565. }
  566. return result, nil
  567. }
  568. // Append appends the given CloudCostSet to the end of the range. It does not
  569. // validate whether or not that violates window continuity.
  570. func (ccsr *CloudCostSetRange) Append(that *CloudCostSet) {
  571. ccsr.CloudCostSets = append(ccsr.CloudCostSets, that)
  572. }
  573. // LoadCloudCost loads CloudCosts into existing CloudCostSets of the CloudCostSetRange.
  574. // This function service to aggregate and distribute costs over predefined windows
  575. // are accumulated here so that the resulting CloudCost with the 1d window has the correct price for the entire day.
  576. // If all or a portion of the window of the CloudCost is outside of the windows of the existing CloudCostSets,
  577. // that portion of the CloudCost's cost will not be inserted
  578. func (ccsr *CloudCostSetRange) LoadCloudCost(cloudCost *CloudCost) {
  579. window := cloudCost.Window
  580. if window.IsOpen() {
  581. log.Errorf("CloudCostSetRange: LoadCloudCost: invalid window %s", window.String())
  582. return
  583. }
  584. totalPct := 0.0
  585. // Distribute cost of the current item across one or more CloudCosts in
  586. // across each relevant CloudCostSet. Stop when the end of the current
  587. // block reaches the item's end time or the end of the range.
  588. for _, ccs := range ccsr.CloudCostSets {
  589. setWindow := ccs.Window
  590. // get percent of item window contained in set window
  591. pct := setWindow.GetPercentInWindow(window)
  592. if pct == 0 {
  593. continue
  594. }
  595. cc := cloudCost
  596. // If the current set Window only contains a portion of the CloudCost Window, insert costs relative to that portion
  597. if pct < 1.0 {
  598. cc = &CloudCost{
  599. Properties: cloudCost.Properties,
  600. Window: window.Contract(setWindow),
  601. ListCost: cloudCost.ListCost.percent(pct),
  602. NetCost: cloudCost.NetCost.percent(pct),
  603. AmortizedNetCost: cloudCost.AmortizedNetCost.percent(pct),
  604. InvoicedCost: cloudCost.InvoicedCost.percent(pct),
  605. AmortizedCost: cloudCost.AmortizedCost.percent(pct),
  606. }
  607. }
  608. err := ccs.Insert(cc)
  609. if err != nil {
  610. log.Errorf("CloudCostSetRange: LoadCloudCost: failed to load CloudCost with window %s: %s", setWindow.String(), err.Error())
  611. }
  612. // If all cost has been inserted, then there is no need to check later days in the range
  613. totalPct += pct
  614. if totalPct >= 1.0 {
  615. return
  616. }
  617. }
  618. }