allocation_helpers_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. package costmodel
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/opencost/opencost/core/pkg/opencost"
  7. "github.com/opencost/opencost/core/pkg/util"
  8. "github.com/opencost/opencost/pkg/prom"
  9. )
  10. const Ki = 1024
  11. const Mi = Ki * 1024
  12. const Gi = Mi * 1024
  13. const second = 1.0
  14. const minute = second * 60.0
  15. const hour = minute * 60.0
  16. var windowStart = time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC)
  17. var windowEnd = time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC)
  18. var window = opencost.NewWindow(&windowStart, &windowEnd)
  19. var startFloat = float64(windowStart.Unix())
  20. var podKey1 = podKey{
  21. namespaceKey: namespaceKey{
  22. Cluster: "cluster1",
  23. Namespace: "namespace1",
  24. },
  25. Pod: "pod1",
  26. }
  27. var podKey2 = podKey{
  28. namespaceKey: namespaceKey{
  29. Cluster: "cluster1",
  30. Namespace: "namespace1",
  31. },
  32. Pod: "pod2",
  33. }
  34. var podKey3 = podKey{
  35. namespaceKey: namespaceKey{
  36. Cluster: "cluster2",
  37. Namespace: "namespace2",
  38. },
  39. Pod: "pod3",
  40. }
  41. var podKey4 = podKey{
  42. namespaceKey: namespaceKey{
  43. Cluster: "cluster2",
  44. Namespace: "namespace2",
  45. },
  46. Pod: "pod4",
  47. }
  48. var podKeyUnmounted = podKey{
  49. namespaceKey: namespaceKey{
  50. Cluster: "cluster2",
  51. Namespace: opencost.UnmountedSuffix,
  52. },
  53. Pod: opencost.UnmountedSuffix,
  54. }
  55. var kcPVKey1 = opencost.PVKey{
  56. Cluster: "cluster1",
  57. Name: "pv1",
  58. }
  59. var kcPVKey2 = opencost.PVKey{
  60. Cluster: "cluster1",
  61. Name: "pv2",
  62. }
  63. var kcPVKey3 = opencost.PVKey{
  64. Cluster: "cluster2",
  65. Name: "pv3",
  66. }
  67. var kcPVKey4 = opencost.PVKey{
  68. Cluster: "cluster2",
  69. Name: "pv4",
  70. }
  71. var podMap1 = map[podKey]*pod{
  72. podKey1: {
  73. Window: window.Clone(),
  74. Start: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC),
  75. End: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC),
  76. Key: podKey1,
  77. Allocations: nil,
  78. },
  79. podKey2: {
  80. Window: window.Clone(),
  81. Start: time.Date(2020, 6, 16, 12, 0, 0, 0, time.UTC),
  82. End: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC),
  83. Key: podKey2,
  84. Allocations: nil,
  85. },
  86. podKey3: {
  87. Window: window.Clone(),
  88. Start: time.Date(2020, 6, 16, 6, 30, 0, 0, time.UTC),
  89. End: time.Date(2020, 6, 17, 18, 12, 33, 0, time.UTC),
  90. Key: podKey3,
  91. Allocations: nil,
  92. },
  93. podKey4: {
  94. Window: window.Clone(),
  95. Start: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC),
  96. End: time.Date(2020, 6, 17, 13, 0, 0, 0, time.UTC),
  97. Key: podKey4,
  98. Allocations: nil,
  99. },
  100. podKeyUnmounted: {
  101. Window: window.Clone(),
  102. Start: *window.Start(),
  103. End: *window.End(),
  104. Key: podKeyUnmounted,
  105. Allocations: map[string]*opencost.Allocation{
  106. opencost.UnmountedSuffix: {
  107. Name: fmt.Sprintf("%s/%s/%s/%s", podKeyUnmounted.Cluster, podKeyUnmounted.Namespace, podKeyUnmounted.Pod, opencost.UnmountedSuffix),
  108. Properties: &opencost.AllocationProperties{
  109. Cluster: podKeyUnmounted.Cluster,
  110. Node: "",
  111. Container: opencost.UnmountedSuffix,
  112. Namespace: podKeyUnmounted.Namespace,
  113. Pod: podKeyUnmounted.Pod,
  114. Services: []string{"LB1"},
  115. },
  116. Window: window,
  117. Start: *window.Start(),
  118. End: *window.End(),
  119. LoadBalancerCost: 0.60,
  120. LoadBalancerCostAdjustment: 0,
  121. PVs: opencost.PVAllocations{
  122. kcPVKey2: &opencost.PVAllocation{
  123. ByteHours: 24 * Gi,
  124. Cost: 2.25,
  125. },
  126. },
  127. },
  128. },
  129. },
  130. }
  131. var pvKey1 = pvKey{
  132. Cluster: "cluster1",
  133. PersistentVolume: "pv1",
  134. }
  135. var pvKey2 = pvKey{
  136. Cluster: "cluster1",
  137. PersistentVolume: "pv2",
  138. }
  139. var pvKey3 = pvKey{
  140. Cluster: "cluster2",
  141. PersistentVolume: "pv3",
  142. }
  143. var pvKey4 = pvKey{
  144. Cluster: "cluster2",
  145. PersistentVolume: "pv4",
  146. }
  147. var pvMap1 = map[pvKey]*pv{
  148. pvKey1: {
  149. Start: windowStart,
  150. End: windowEnd.Add(time.Hour * -6),
  151. Bytes: 20 * Gi,
  152. CostPerGiBHour: 0.05,
  153. Cluster: "cluster1",
  154. Name: "pv1",
  155. StorageClass: "class1",
  156. },
  157. pvKey2: {
  158. Start: windowStart,
  159. End: windowEnd,
  160. Bytes: 100 * Gi,
  161. CostPerGiBHour: 0.05,
  162. Cluster: "cluster1",
  163. Name: "pv2",
  164. StorageClass: "class1",
  165. },
  166. pvKey3: {
  167. Start: windowStart.Add(time.Hour * 6),
  168. End: windowEnd.Add(time.Hour * -6),
  169. Bytes: 50 * Gi,
  170. CostPerGiBHour: 0.03,
  171. Cluster: "cluster2",
  172. Name: "pv3",
  173. StorageClass: "class2",
  174. },
  175. pvKey4: {
  176. Start: windowStart,
  177. End: windowEnd.Add(time.Hour * -6),
  178. Bytes: 30 * Gi,
  179. CostPerGiBHour: 0.05,
  180. Cluster: "cluster2",
  181. Name: "pv4",
  182. StorageClass: "class1",
  183. },
  184. }
  185. func TestBuildPVMap(t *testing.T) {
  186. pvMap1NoBytes := make(map[pvKey]*pv, len(pvMap1))
  187. for thisPVKey, thisPV := range pvMap1 {
  188. clonePV := thisPV.clone()
  189. clonePV.Bytes = 0.0
  190. clonePV.StorageClass = ""
  191. pvMap1NoBytes[thisPVKey] = clonePV
  192. }
  193. // These test cases are mocking behavior from Prometheus v3+
  194. prometheusVersion = "3.0.0"
  195. testCases := map[string]struct {
  196. resolution time.Duration
  197. resultsPVCostPerGiBHour []*prom.QueryResult
  198. resultsActiveMinutes []*prom.QueryResult
  199. expected map[pvKey]*pv
  200. }{
  201. "pvMap1": {
  202. resolution: time.Hour * 6,
  203. resultsPVCostPerGiBHour: []*prom.QueryResult{
  204. {
  205. Metric: map[string]interface{}{
  206. "cluster_id": "cluster1",
  207. "volumename": "pv1",
  208. },
  209. Values: []*util.Vector{
  210. {
  211. Value: 0.05,
  212. },
  213. },
  214. },
  215. {
  216. Metric: map[string]interface{}{
  217. "cluster_id": "cluster1",
  218. "volumename": "pv2",
  219. },
  220. Values: []*util.Vector{
  221. {
  222. Value: 0.05,
  223. },
  224. },
  225. },
  226. {
  227. Metric: map[string]interface{}{
  228. "cluster_id": "cluster2",
  229. "volumename": "pv3",
  230. },
  231. Values: []*util.Vector{
  232. {
  233. Value: 0.03,
  234. },
  235. },
  236. },
  237. {
  238. Metric: map[string]interface{}{
  239. "cluster_id": "cluster2",
  240. "volumename": "pv4",
  241. },
  242. Values: []*util.Vector{
  243. {
  244. Value: 0.05,
  245. },
  246. },
  247. },
  248. },
  249. resultsActiveMinutes: []*prom.QueryResult{
  250. {
  251. Metric: map[string]interface{}{
  252. "cluster_id": "cluster1",
  253. "persistentvolume": "pv1",
  254. },
  255. Values: []*util.Vector{
  256. {
  257. Timestamp: startFloat + (hour * 6),
  258. },
  259. {
  260. Timestamp: startFloat + (hour * 12),
  261. },
  262. {
  263. Timestamp: startFloat + (hour * 18),
  264. },
  265. },
  266. },
  267. {
  268. Metric: map[string]interface{}{
  269. "cluster_id": "cluster1",
  270. "persistentvolume": "pv2",
  271. },
  272. Values: []*util.Vector{
  273. {
  274. Timestamp: startFloat + (hour * 6),
  275. },
  276. {
  277. Timestamp: startFloat + (hour * 12),
  278. },
  279. {
  280. Timestamp: startFloat + (hour * 18),
  281. },
  282. {
  283. Timestamp: startFloat + (hour * 24),
  284. },
  285. },
  286. },
  287. {
  288. Metric: map[string]interface{}{
  289. "cluster_id": "cluster2",
  290. "persistentvolume": "pv3",
  291. },
  292. Values: []*util.Vector{
  293. {
  294. Timestamp: startFloat + (hour * 12),
  295. },
  296. {
  297. Timestamp: startFloat + (hour * 18),
  298. },
  299. },
  300. },
  301. {
  302. Metric: map[string]interface{}{
  303. "cluster_id": "cluster2",
  304. "persistentvolume": "pv4",
  305. },
  306. Values: []*util.Vector{
  307. {
  308. Timestamp: startFloat + (hour * 6),
  309. },
  310. {
  311. Timestamp: startFloat + (hour * 12),
  312. },
  313. {
  314. Timestamp: startFloat + (hour * 18),
  315. },
  316. },
  317. },
  318. },
  319. expected: pvMap1NoBytes,
  320. },
  321. }
  322. for name, testCase := range testCases {
  323. t.Run(name, func(t *testing.T) {
  324. pvMap := make(map[pvKey]*pv)
  325. buildPVMap(testCase.resolution, pvMap, testCase.resultsPVCostPerGiBHour, testCase.resultsActiveMinutes, []*prom.QueryResult{}, window)
  326. if len(pvMap) != len(testCase.expected) {
  327. t.Errorf("pv map does not have the expected length %d : %d", len(pvMap), len(testCase.expected))
  328. }
  329. for thisPVKey, expectedPV := range testCase.expected {
  330. actualPV, ok := pvMap[thisPVKey]
  331. if !ok {
  332. t.Errorf("pv map is missing key %s", thisPVKey)
  333. }
  334. if !actualPV.equal(expectedPV) {
  335. t.Errorf("pv does not match with key %s: %s != %s", thisPVKey, opencost.NewClosedWindow(actualPV.Start, actualPV.End), opencost.NewClosedWindow(expectedPV.Start, expectedPV.End))
  336. }
  337. }
  338. })
  339. }
  340. }
  341. func TestGetUnmountedPodForCluster(t *testing.T) {
  342. testCases := map[string]struct {
  343. window opencost.Window
  344. podMap map[podKey]*pod
  345. cluster string
  346. expected *pod
  347. }{
  348. "create new": {
  349. window: window.Clone(),
  350. podMap: podMap1,
  351. cluster: "cluster1",
  352. expected: &pod{
  353. Window: window.Clone(),
  354. Start: *window.Start(),
  355. End: *window.End(),
  356. Key: getUnmountedPodKey("cluster1"),
  357. Allocations: map[string]*opencost.Allocation{
  358. opencost.UnmountedSuffix: {
  359. Name: fmt.Sprintf("%s/%s/%s/%s", "cluster1", opencost.UnmountedSuffix, opencost.UnmountedSuffix, opencost.UnmountedSuffix),
  360. Properties: &opencost.AllocationProperties{
  361. Cluster: "cluster1",
  362. Node: "",
  363. Container: opencost.UnmountedSuffix,
  364. Namespace: opencost.UnmountedSuffix,
  365. Pod: opencost.UnmountedSuffix,
  366. },
  367. Window: window,
  368. Start: *window.Start(),
  369. End: *window.End(),
  370. },
  371. },
  372. },
  373. },
  374. "get existing": {
  375. window: window.Clone(),
  376. podMap: podMap1,
  377. cluster: "cluster2",
  378. expected: &pod{
  379. Window: window.Clone(),
  380. Start: *window.Start(),
  381. End: *window.End(),
  382. Key: getUnmountedPodKey("cluster2"),
  383. Allocations: map[string]*opencost.Allocation{
  384. opencost.UnmountedSuffix: {
  385. Name: fmt.Sprintf("%s/%s/%s/%s", "cluster2", opencost.UnmountedSuffix, opencost.UnmountedSuffix, opencost.UnmountedSuffix),
  386. Properties: &opencost.AllocationProperties{
  387. Cluster: "cluster2",
  388. Node: "",
  389. Container: opencost.UnmountedSuffix,
  390. Namespace: opencost.UnmountedSuffix,
  391. Pod: opencost.UnmountedSuffix,
  392. Services: []string{"LB1"},
  393. },
  394. Window: window,
  395. Start: *window.Start(),
  396. End: *window.End(),
  397. LoadBalancerCost: .60,
  398. LoadBalancerCostAdjustment: 0,
  399. PVs: opencost.PVAllocations{
  400. kcPVKey2: &opencost.PVAllocation{
  401. ByteHours: 24 * Gi,
  402. Cost: 2.25,
  403. },
  404. },
  405. },
  406. },
  407. },
  408. },
  409. }
  410. for name, testCase := range testCases {
  411. t.Run(name, func(t *testing.T) {
  412. actual := getUnmountedPodForCluster(testCase.window, testCase.podMap, testCase.cluster)
  413. if !actual.equal(testCase.expected) {
  414. t.Errorf("Unmounted pod does not match expectation")
  415. }
  416. })
  417. }
  418. }
  419. func TestCalculateStartAndEnd(t *testing.T) {
  420. // These test cases are mocking behavior from Prometheus v3+
  421. prometheusVersion = "3.0.0"
  422. testCases := map[string]struct {
  423. resolution time.Duration // User defined config when querying Prometheus
  424. window opencost.Window // User defined config when querying Allocations/Assets
  425. expectedStart time.Time
  426. expectedEnd time.Time
  427. result *prom.QueryResult
  428. }{
  429. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1h:1h]
  430. "1 hour resolution, 1 hour window": {
  431. resolution: time.Hour,
  432. window: opencost.NewClosedWindow(windowStart, windowStart.Add(time.Hour)),
  433. expectedStart: windowStart,
  434. expectedEnd: windowStart.Add(time.Hour),
  435. result: &prom.QueryResult{
  436. Values: []*util.Vector{
  437. {
  438. Timestamp: startFloat + (minute * 60),
  439. },
  440. },
  441. },
  442. },
  443. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1h:30m]
  444. "30 minute resolution, 1 hour window": {
  445. resolution: time.Minute * 30,
  446. window: opencost.NewClosedWindow(windowStart, windowStart.Add(time.Hour)),
  447. expectedStart: windowStart,
  448. expectedEnd: windowStart.Add(time.Hour),
  449. result: &prom.QueryResult{
  450. Values: []*util.Vector{
  451. {
  452. Timestamp: startFloat + (minute * 30),
  453. },
  454. {
  455. Timestamp: startFloat + (minute * 60),
  456. },
  457. },
  458. },
  459. },
  460. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[45m:15m]
  461. "15 minute resolution, 45 minute window": {
  462. resolution: time.Minute * 15,
  463. window: opencost.NewClosedWindow(windowStart, windowStart.Add(time.Minute*45)),
  464. expectedStart: windowStart,
  465. expectedEnd: windowStart.Add(time.Minute * 45),
  466. result: &prom.QueryResult{
  467. Values: []*util.Vector{
  468. {
  469. Timestamp: startFloat + (minute * 15),
  470. },
  471. {
  472. Timestamp: startFloat + (minute * 30),
  473. },
  474. {
  475. Timestamp: startFloat + (minute * 45),
  476. },
  477. },
  478. },
  479. },
  480. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[30m:5m]
  481. "5 minute resolution, 30 minute window": {
  482. resolution: time.Minute * 5,
  483. window: opencost.NewClosedWindow(windowStart, windowStart.Add(time.Minute*30)),
  484. expectedStart: windowStart,
  485. expectedEnd: windowStart.Add(time.Minute * 30),
  486. result: &prom.QueryResult{
  487. Values: []*util.Vector{
  488. {
  489. Timestamp: startFloat + (minute * 5),
  490. },
  491. {
  492. Timestamp: startFloat + (minute * 10),
  493. },
  494. {
  495. Timestamp: startFloat + (minute * 15),
  496. },
  497. {
  498. Timestamp: startFloat + (minute * 20),
  499. },
  500. {
  501. Timestamp: startFloat + (minute * 25),
  502. },
  503. {
  504. Timestamp: startFloat + (minute * 30),
  505. },
  506. },
  507. },
  508. },
  509. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[30m:5m]
  510. "5 minute resolution, 30 minute window, partial data": {
  511. resolution: time.Minute * 5,
  512. window: opencost.NewClosedWindow(windowStart, windowStart.Add(time.Minute*30)),
  513. expectedStart: windowStart.Add(time.Minute * 5),
  514. expectedEnd: windowStart.Add(time.Minute * 20),
  515. result: &prom.QueryResult{
  516. Values: []*util.Vector{
  517. {
  518. Timestamp: startFloat + (minute * 10),
  519. },
  520. {
  521. Timestamp: startFloat + (minute * 15),
  522. },
  523. {
  524. Timestamp: startFloat + (minute * 20),
  525. },
  526. },
  527. },
  528. },
  529. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[5m:1m]
  530. "1 minute resolution, 5 minute window": {
  531. resolution: time.Minute,
  532. window: opencost.NewClosedWindow(windowStart.Add(time.Minute*15), windowStart.Add(time.Minute*20)),
  533. expectedStart: windowStart.Add(time.Minute * 15),
  534. expectedEnd: windowStart.Add(time.Minute * 20),
  535. result: &prom.QueryResult{
  536. Values: []*util.Vector{
  537. {
  538. Timestamp: startFloat + (minute * 16),
  539. },
  540. {
  541. Timestamp: startFloat + (minute * 17),
  542. },
  543. {
  544. Timestamp: startFloat + (minute * 18),
  545. },
  546. {
  547. Timestamp: startFloat + (minute * 19),
  548. },
  549. {
  550. Timestamp: startFloat + (minute * 20),
  551. },
  552. },
  553. },
  554. },
  555. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[5m:1m]
  556. "1 minute resolution, 5 minute window, partial data": {
  557. resolution: time.Minute,
  558. window: opencost.NewClosedWindow(windowStart.Add(time.Minute*15), windowStart.Add(time.Minute*20)),
  559. expectedStart: windowStart.Add(time.Minute * 18),
  560. expectedEnd: windowStart.Add(time.Minute * 20),
  561. result: &prom.QueryResult{
  562. Values: []*util.Vector{
  563. {
  564. Timestamp: startFloat + (minute * 19),
  565. },
  566. {
  567. Timestamp: startFloat + (minute * 20),
  568. },
  569. },
  570. },
  571. },
  572. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
  573. "1 minute resolution, 1 minute window": {
  574. resolution: time.Minute,
  575. window: opencost.NewClosedWindow(windowStart.Add(time.Minute*14).Add(time.Second*30), windowStart.Add(time.Minute*15).Add(time.Second*30)),
  576. expectedStart: windowStart.Add(time.Minute * 14).Add(time.Second * 30),
  577. expectedEnd: windowStart.Add(time.Minute * 15).Add(time.Second * 30),
  578. result: &prom.QueryResult{
  579. Values: []*util.Vector{
  580. {
  581. Timestamp: startFloat + (minute * 15) + (second * 30),
  582. },
  583. },
  584. },
  585. },
  586. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
  587. "1 minute resolution, 1 minute window, at window start": {
  588. resolution: time.Minute,
  589. window: opencost.NewClosedWindow(windowStart, windowStart.Add(time.Second*30)),
  590. expectedStart: windowStart,
  591. expectedEnd: windowStart.Add(time.Second * 30),
  592. result: &prom.QueryResult{
  593. Values: []*util.Vector{
  594. {
  595. Timestamp: startFloat + (second * 30),
  596. },
  597. },
  598. },
  599. },
  600. }
  601. for name, testCase := range testCases {
  602. t.Run(name, func(t *testing.T) {
  603. start, end := calculateStartAndEnd(testCase.result, testCase.resolution, testCase.window)
  604. if !start.Equal(testCase.expectedStart) {
  605. t.Errorf("start does not match: expected %v; got %v", testCase.expectedStart, start)
  606. }
  607. if !end.Equal(testCase.expectedEnd) {
  608. t.Errorf("end does not match: expected %v; got %v", testCase.expectedEnd, end)
  609. }
  610. })
  611. }
  612. }