2
0

cloudcost.go 21 KB

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