allocation_helpers_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  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/source"
  8. "github.com/opencost/opencost/core/pkg/util"
  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 endFloat = float64(windowEnd.Unix())
  21. var podKey1 = podKey{
  22. namespaceKey: namespaceKey{
  23. Cluster: "cluster1",
  24. Namespace: "namespace1",
  25. },
  26. Pod: "pod1",
  27. }
  28. var podKey2 = podKey{
  29. namespaceKey: namespaceKey{
  30. Cluster: "cluster1",
  31. Namespace: "namespace1",
  32. },
  33. Pod: "pod2",
  34. }
  35. var podKey3 = podKey{
  36. namespaceKey: namespaceKey{
  37. Cluster: "cluster2",
  38. Namespace: "namespace2",
  39. },
  40. Pod: "pod3",
  41. }
  42. var podKey4 = podKey{
  43. namespaceKey: namespaceKey{
  44. Cluster: "cluster2",
  45. Namespace: "namespace2",
  46. },
  47. Pod: "pod4",
  48. }
  49. var podKeyUnmounted = podKey{
  50. namespaceKey: namespaceKey{
  51. Cluster: "cluster2",
  52. Namespace: opencost.UnmountedSuffix,
  53. },
  54. Pod: opencost.UnmountedSuffix,
  55. }
  56. var kcPVKey1 = opencost.PVKey{
  57. Cluster: "cluster1",
  58. Name: "pv1",
  59. }
  60. var kcPVKey2 = opencost.PVKey{
  61. Cluster: "cluster1",
  62. Name: "pv2",
  63. }
  64. var kcPVKey3 = opencost.PVKey{
  65. Cluster: "cluster2",
  66. Name: "pv3",
  67. }
  68. var kcPVKey4 = opencost.PVKey{
  69. Cluster: "cluster2",
  70. Name: "pv4",
  71. }
  72. var podMap1 = map[podKey]*pod{
  73. podKey1: {
  74. Window: window.Clone(),
  75. Start: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC),
  76. End: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC),
  77. Key: podKey1,
  78. Allocations: nil,
  79. },
  80. podKey2: {
  81. Window: window.Clone(),
  82. Start: time.Date(2020, 6, 16, 12, 0, 0, 0, time.UTC),
  83. End: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC),
  84. Key: podKey2,
  85. Allocations: nil,
  86. },
  87. podKey3: {
  88. Window: window.Clone(),
  89. Start: time.Date(2020, 6, 16, 6, 30, 0, 0, time.UTC),
  90. End: time.Date(2020, 6, 17, 18, 12, 33, 0, time.UTC),
  91. Key: podKey3,
  92. Allocations: nil,
  93. },
  94. podKey4: {
  95. Window: window.Clone(),
  96. Start: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC),
  97. End: time.Date(2020, 6, 17, 13, 0, 0, 0, time.UTC),
  98. Key: podKey4,
  99. Allocations: nil,
  100. },
  101. podKeyUnmounted: {
  102. Window: window.Clone(),
  103. Start: *window.Start(),
  104. End: *window.End(),
  105. Key: podKeyUnmounted,
  106. Allocations: map[string]*opencost.Allocation{
  107. opencost.UnmountedSuffix: {
  108. Name: fmt.Sprintf("%s/%s/%s/%s", podKeyUnmounted.Cluster, podKeyUnmounted.Namespace, podKeyUnmounted.Pod, opencost.UnmountedSuffix),
  109. Properties: &opencost.AllocationProperties{
  110. Cluster: podKeyUnmounted.Cluster,
  111. Node: "",
  112. Container: opencost.UnmountedSuffix,
  113. Namespace: podKeyUnmounted.Namespace,
  114. Pod: podKeyUnmounted.Pod,
  115. Services: []string{"LB1"},
  116. },
  117. Window: window,
  118. Start: *window.Start(),
  119. End: *window.End(),
  120. LoadBalancerCost: 0.60,
  121. LoadBalancerCostAdjustment: 0,
  122. PVs: opencost.PVAllocations{
  123. kcPVKey2: &opencost.PVAllocation{
  124. ByteHours: 24 * Gi,
  125. Cost: 2.25,
  126. },
  127. },
  128. },
  129. },
  130. },
  131. }
  132. var pvKey1 = pvKey{
  133. Cluster: "cluster1",
  134. PersistentVolume: "pv1",
  135. }
  136. var pvKey2 = pvKey{
  137. Cluster: "cluster1",
  138. PersistentVolume: "pv2",
  139. }
  140. var pvKey3 = pvKey{
  141. Cluster: "cluster2",
  142. PersistentVolume: "pv3",
  143. }
  144. var pvKey4 = pvKey{
  145. Cluster: "cluster2",
  146. PersistentVolume: "pv4",
  147. }
  148. var pvMap1 = map[pvKey]*pv{
  149. pvKey1: {
  150. Start: windowStart,
  151. End: windowEnd.Add(time.Hour * -6),
  152. Bytes: 20 * Gi,
  153. CostPerGiBHour: 0.05,
  154. Cluster: "cluster1",
  155. Name: "pv1",
  156. StorageClass: "class1",
  157. },
  158. pvKey2: {
  159. Start: windowStart,
  160. End: windowEnd,
  161. Bytes: 100 * Gi,
  162. CostPerGiBHour: 0.05,
  163. Cluster: "cluster1",
  164. Name: "pv2",
  165. StorageClass: "class1",
  166. },
  167. pvKey3: {
  168. Start: windowStart.Add(time.Hour * 6),
  169. End: windowEnd.Add(time.Hour * -6),
  170. Bytes: 50 * Gi,
  171. CostPerGiBHour: 0.03,
  172. Cluster: "cluster2",
  173. Name: "pv3",
  174. StorageClass: "class2",
  175. },
  176. pvKey4: {
  177. Start: windowStart,
  178. End: windowEnd.Add(time.Hour * -6),
  179. Bytes: 30 * Gi,
  180. CostPerGiBHour: 0.05,
  181. Cluster: "cluster2",
  182. Name: "pv4",
  183. StorageClass: "class1",
  184. },
  185. }
  186. func TestBuildPVMap(t *testing.T) {
  187. pvMap1NoBytes := make(map[pvKey]*pv, len(pvMap1))
  188. for thisPVKey, thisPV := range pvMap1 {
  189. clonePV := thisPV.clone()
  190. clonePV.Bytes = 0.0
  191. clonePV.StorageClass = ""
  192. pvMap1NoBytes[thisPVKey] = clonePV
  193. }
  194. testCases := map[string]struct {
  195. resolution time.Duration
  196. resultsPVCostPerGiBHour []*source.QueryResult
  197. resultsActiveMinutes []*source.QueryResult
  198. expected map[pvKey]*pv
  199. }{
  200. "pvMap1": {
  201. resolution: time.Hour * 6,
  202. resultsPVCostPerGiBHour: []*source.QueryResult{
  203. source.NewQueryResult(
  204. map[string]interface{}{
  205. "cluster_id": "cluster1",
  206. "volumename": "pv1",
  207. },
  208. []*util.Vector{
  209. {
  210. Value: 0.05,
  211. },
  212. },
  213. source.DefaultResultKeys(),
  214. ),
  215. source.NewQueryResult(
  216. map[string]interface{}{
  217. "cluster_id": "cluster1",
  218. "volumename": "pv2",
  219. },
  220. []*util.Vector{
  221. {
  222. Value: 0.05,
  223. },
  224. },
  225. source.DefaultResultKeys(),
  226. ),
  227. source.NewQueryResult(
  228. map[string]interface{}{
  229. "cluster_id": "cluster2",
  230. "volumename": "pv3",
  231. },
  232. []*util.Vector{
  233. {
  234. Value: 0.03,
  235. },
  236. },
  237. source.DefaultResultKeys(),
  238. ),
  239. source.NewQueryResult(
  240. map[string]interface{}{
  241. "cluster_id": "cluster2",
  242. "volumename": "pv4",
  243. },
  244. []*util.Vector{
  245. {
  246. Value: 0.05,
  247. },
  248. },
  249. source.DefaultResultKeys(),
  250. ),
  251. },
  252. resultsActiveMinutes: []*source.QueryResult{
  253. source.NewQueryResult(
  254. map[string]interface{}{
  255. "cluster_id": "cluster1",
  256. "persistentvolume": "pv1",
  257. },
  258. []*util.Vector{
  259. {
  260. Timestamp: startFloat,
  261. },
  262. {
  263. Timestamp: startFloat + (hour * 6),
  264. },
  265. {
  266. Timestamp: startFloat + (hour * 12),
  267. },
  268. {
  269. Timestamp: startFloat + (hour * 18),
  270. },
  271. },
  272. source.DefaultResultKeys(),
  273. ),
  274. source.NewQueryResult(
  275. map[string]interface{}{
  276. "cluster_id": "cluster1",
  277. "persistentvolume": "pv2",
  278. },
  279. []*util.Vector{
  280. {
  281. Timestamp: startFloat,
  282. },
  283. {
  284. Timestamp: startFloat + (hour * 6),
  285. },
  286. {
  287. Timestamp: startFloat + (hour * 12),
  288. },
  289. {
  290. Timestamp: startFloat + (hour * 18),
  291. },
  292. {
  293. Timestamp: startFloat + (hour * 24),
  294. },
  295. },
  296. source.DefaultResultKeys(),
  297. ),
  298. source.NewQueryResult(
  299. map[string]interface{}{
  300. "cluster_id": "cluster2",
  301. "persistentvolume": "pv3",
  302. },
  303. []*util.Vector{
  304. {
  305. Timestamp: startFloat + (hour * 6),
  306. },
  307. {
  308. Timestamp: startFloat + (hour * 12),
  309. },
  310. {
  311. Timestamp: startFloat + (hour * 18),
  312. },
  313. },
  314. source.DefaultResultKeys(),
  315. ),
  316. source.NewQueryResult(
  317. map[string]interface{}{
  318. "cluster_id": "cluster2",
  319. "persistentvolume": "pv4",
  320. },
  321. []*util.Vector{
  322. {
  323. Timestamp: startFloat,
  324. },
  325. {
  326. Timestamp: startFloat + (hour * 6),
  327. },
  328. {
  329. Timestamp: startFloat + (hour * 12),
  330. },
  331. {
  332. Timestamp: startFloat + (hour * 18),
  333. },
  334. },
  335. source.DefaultResultKeys(),
  336. ),
  337. },
  338. expected: pvMap1NoBytes,
  339. },
  340. }
  341. for name, testCase := range testCases {
  342. t.Run(name, func(t *testing.T) {
  343. pvMap := make(map[pvKey]*pv)
  344. pvCostResults := source.DecodeAll(testCase.resultsPVCostPerGiBHour, source.DecodePVPricePerGiBHourResult)
  345. pvActiveMinsResults := source.DecodeAll(testCase.resultsActiveMinutes, source.DecodePVActiveMinutesResult)
  346. pvInfoResult := []*source.PVInfoResult{}
  347. buildPVMap(testCase.resolution, pvMap, pvCostResults, pvActiveMinsResults, pvInfoResult, window)
  348. if len(pvMap) != len(testCase.expected) {
  349. t.Errorf("pv map does not have the expected length %d : %d", len(pvMap), len(testCase.expected))
  350. }
  351. for thisPVKey, expectedPV := range testCase.expected {
  352. actualPV, ok := pvMap[thisPVKey]
  353. if !ok {
  354. t.Errorf("pv map is missing key %s", thisPVKey)
  355. }
  356. if !actualPV.equal(expectedPV) {
  357. t.Errorf("pv does not match with key %s: %s != %s", thisPVKey, opencost.NewClosedWindow(actualPV.Start, actualPV.End), opencost.NewClosedWindow(expectedPV.Start, expectedPV.End))
  358. }
  359. }
  360. })
  361. }
  362. }
  363. func TestGetUnmountedPodForCluster(t *testing.T) {
  364. testCases := map[string]struct {
  365. window opencost.Window
  366. podMap map[podKey]*pod
  367. cluster string
  368. expected *pod
  369. }{
  370. "create new": {
  371. window: window.Clone(),
  372. podMap: podMap1,
  373. cluster: "cluster1",
  374. expected: &pod{
  375. Window: window.Clone(),
  376. Start: *window.Start(),
  377. End: *window.End(),
  378. Key: getUnmountedPodKey("cluster1"),
  379. Allocations: map[string]*opencost.Allocation{
  380. opencost.UnmountedSuffix: {
  381. Name: fmt.Sprintf("%s/%s/%s/%s", "cluster1", opencost.UnmountedSuffix, opencost.UnmountedSuffix, opencost.UnmountedSuffix),
  382. Properties: &opencost.AllocationProperties{
  383. Cluster: "cluster1",
  384. Node: "",
  385. Container: opencost.UnmountedSuffix,
  386. Namespace: opencost.UnmountedSuffix,
  387. Pod: opencost.UnmountedSuffix,
  388. },
  389. Window: window,
  390. Start: *window.Start(),
  391. End: *window.End(),
  392. },
  393. },
  394. },
  395. },
  396. "get existing": {
  397. window: window.Clone(),
  398. podMap: podMap1,
  399. cluster: "cluster2",
  400. expected: &pod{
  401. Window: window.Clone(),
  402. Start: *window.Start(),
  403. End: *window.End(),
  404. Key: getUnmountedPodKey("cluster2"),
  405. Allocations: map[string]*opencost.Allocation{
  406. opencost.UnmountedSuffix: {
  407. Name: fmt.Sprintf("%s/%s/%s/%s", "cluster2", opencost.UnmountedSuffix, opencost.UnmountedSuffix, opencost.UnmountedSuffix),
  408. Properties: &opencost.AllocationProperties{
  409. Cluster: "cluster2",
  410. Node: "",
  411. Container: opencost.UnmountedSuffix,
  412. Namespace: opencost.UnmountedSuffix,
  413. Pod: opencost.UnmountedSuffix,
  414. Services: []string{"LB1"},
  415. },
  416. Window: window,
  417. Start: *window.Start(),
  418. End: *window.End(),
  419. LoadBalancerCost: .60,
  420. LoadBalancerCostAdjustment: 0,
  421. PVs: opencost.PVAllocations{
  422. kcPVKey2: &opencost.PVAllocation{
  423. ByteHours: 24 * Gi,
  424. Cost: 2.25,
  425. },
  426. },
  427. },
  428. },
  429. },
  430. },
  431. }
  432. for name, testCase := range testCases {
  433. t.Run(name, func(t *testing.T) {
  434. actual := getUnmountedPodForCluster(testCase.window, testCase.podMap, testCase.cluster)
  435. if !actual.equal(testCase.expected) {
  436. t.Errorf("Unmounted pod does not match expectation")
  437. }
  438. })
  439. }
  440. }
  441. func TestCalculateStartAndEnd(t *testing.T) {
  442. testCases := map[string]struct {
  443. resolution time.Duration // User defined config when querying Prometheus
  444. expectedStart time.Time
  445. expectedEnd time.Time
  446. result *source.QueryResult
  447. }{
  448. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1h:1h]
  449. "1 hour resolution, 1 hour window": {
  450. resolution: time.Hour,
  451. expectedStart: windowStart,
  452. expectedEnd: windowStart.Add(time.Hour),
  453. result: &source.QueryResult{
  454. Values: []*util.Vector{
  455. {
  456. Timestamp: startFloat,
  457. },
  458. {
  459. Timestamp: startFloat + (minute * 60),
  460. },
  461. },
  462. },
  463. },
  464. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1h:30m]
  465. "30 minute resolution, 1 hour window": {
  466. resolution: time.Minute * 30,
  467. expectedStart: windowStart,
  468. expectedEnd: windowStart.Add(time.Hour),
  469. result: &source.QueryResult{
  470. Values: []*util.Vector{
  471. {
  472. Timestamp: startFloat,
  473. },
  474. {
  475. Timestamp: startFloat + (minute * 30),
  476. },
  477. {
  478. Timestamp: startFloat + (minute * 60),
  479. },
  480. },
  481. },
  482. },
  483. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[45m:15m]
  484. "15 minute resolution, 45 minute window": {
  485. resolution: time.Minute * 15,
  486. expectedStart: windowStart,
  487. expectedEnd: windowStart.Add(time.Minute * 45),
  488. result: &source.QueryResult{
  489. Values: []*util.Vector{
  490. {
  491. Timestamp: startFloat + (minute * 0),
  492. },
  493. {
  494. Timestamp: startFloat + (minute * 15),
  495. },
  496. {
  497. Timestamp: startFloat + (minute * 30),
  498. },
  499. {
  500. Timestamp: startFloat + (minute * 45),
  501. },
  502. },
  503. },
  504. },
  505. "1 minute resolution, 5 minute window": {
  506. resolution: time.Minute,
  507. expectedStart: windowStart.Add(time.Minute * 15),
  508. expectedEnd: windowStart.Add(time.Minute * 20),
  509. result: &source.QueryResult{
  510. Values: []*util.Vector{
  511. {
  512. Timestamp: startFloat + (minute * 15),
  513. },
  514. {
  515. Timestamp: startFloat + (minute * 16),
  516. },
  517. {
  518. Timestamp: startFloat + (minute * 17),
  519. },
  520. {
  521. Timestamp: startFloat + (minute * 18),
  522. },
  523. {
  524. Timestamp: startFloat + (minute * 19),
  525. },
  526. {
  527. Timestamp: startFloat + (minute * 20),
  528. },
  529. },
  530. },
  531. },
  532. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
  533. "1 minute resolution, 1 minute window": {
  534. resolution: time.Minute,
  535. expectedStart: windowStart.Add(time.Minute * 15),
  536. expectedEnd: windowStart.Add(time.Minute * 16),
  537. result: &source.QueryResult{
  538. Values: []*util.Vector{
  539. {
  540. Timestamp: startFloat + (minute * 15),
  541. },
  542. },
  543. },
  544. },
  545. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
  546. "1 minute resolution, 1 minute window, at window start": {
  547. resolution: time.Minute,
  548. expectedStart: windowStart,
  549. expectedEnd: windowStart.Add(time.Minute),
  550. result: &source.QueryResult{
  551. Values: []*util.Vector{
  552. {
  553. Timestamp: startFloat,
  554. },
  555. },
  556. },
  557. },
  558. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
  559. "1 minute resolution, 1 minute window, at window end": {
  560. resolution: time.Minute,
  561. expectedStart: windowEnd,
  562. expectedEnd: windowEnd,
  563. result: &source.QueryResult{
  564. Values: []*util.Vector{
  565. {
  566. Timestamp: endFloat,
  567. },
  568. },
  569. },
  570. },
  571. // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
  572. "1 minute resolution, 1 minute window, near window end": {
  573. resolution: time.Minute,
  574. expectedStart: windowEnd.Add(-time.Second * 15),
  575. expectedEnd: windowEnd,
  576. result: &source.QueryResult{
  577. Values: []*util.Vector{
  578. {
  579. Timestamp: endFloat - 15.0,
  580. },
  581. },
  582. },
  583. },
  584. }
  585. for name, testCase := range testCases {
  586. t.Run(name, func(t *testing.T) {
  587. start, end := calculateStartAndEnd(testCase.result.Values, testCase.resolution, window)
  588. if !start.Equal(testCase.expectedStart) {
  589. t.Errorf("start does not match: expected %v; got %v", testCase.expectedStart, start)
  590. }
  591. if !end.Equal(testCase.expectedEnd) {
  592. t.Errorf("end does not match: expected %v; got %v", testCase.expectedEnd, end)
  593. }
  594. })
  595. }
  596. }
  597. // makePodMapWithEmptyAllocations builds a pod map with the given podKey present
  598. // and an empty Allocations map, which mirrors what buildPodMap produces before
  599. // container-scoped apply functions run.
  600. func makePodMapWithEmptyAllocations(pk podKey) map[podKey]*pod {
  601. return map[podKey]*pod{
  602. pk: {
  603. Window: window.Clone(),
  604. Start: windowStart,
  605. End: windowEnd,
  606. Key: pk,
  607. Allocations: map[string]*opencost.Allocation{},
  608. },
  609. }
  610. }
  611. // ramUsageMaxResult builds a RAMUsageMaxResult for a single container with the
  612. // given pod-level identity and sample value.
  613. func ramUsageMaxResult(pk podKey, container string, value float64) *source.RAMUsageMaxResult {
  614. return &source.RAMUsageMaxResult{
  615. Cluster: pk.Cluster,
  616. Namespace: pk.Namespace,
  617. Pod: pk.Pod,
  618. Container: container,
  619. Data: []*util.Vector{
  620. {Value: value},
  621. },
  622. }
  623. }
  624. // cpuUsageMaxResult builds a CPUUsageMaxResult for a single container with the
  625. // given pod-level identity and sample value.
  626. func cpuUsageMaxResult(pk podKey, container string, value float64) *source.CPUUsageMaxResult {
  627. return &source.CPUUsageMaxResult{
  628. Cluster: pk.Cluster,
  629. Namespace: pk.Namespace,
  630. Pod: pk.Pod,
  631. Container: container,
  632. Data: []*util.Vector{
  633. {Value: value},
  634. },
  635. }
  636. }
  637. // gpuUsageMaxResult builds a GPUsUsageMaxResult for a single container with
  638. // the given pod-level identity and sample value.
  639. func gpuUsageMaxResult(pk podKey, container string, value float64) *source.GPUsUsageMaxResult {
  640. return &source.GPUsUsageMaxResult{
  641. Cluster: pk.Cluster,
  642. Namespace: pk.Namespace,
  643. Pod: pk.Pod,
  644. Container: container,
  645. Data: []*util.Vector{
  646. {Value: value},
  647. },
  648. }
  649. }
  650. // TestApplyRAMBytesUsedMax_KeepsLargestAcrossDuplicateRows regression-tests
  651. // the case where the Prometheus RAM max query returns multiple rows for the
  652. // same (cluster, namespace, pod, container) combination (for example when a
  653. // pod restarted mid-window and kube-state-metrics emitted a new uid row, or
  654. // when the pod was scraped from more than one instance). The previous
  655. // implementation overwrote the stored max with whichever row happened to be
  656. // iterated last, producing an arbitrary (and typically tiny) maximum. This
  657. // test asserts that applyRAMBytesUsedMax now takes the max across rows.
  658. func TestApplyRAMBytesUsedMax_KeepsLargestAcrossDuplicateRows(t *testing.T) {
  659. const container = "c1"
  660. const small = 442368.0
  661. const large = 65101824.0
  662. // The order of duplicate-row results must not matter; verify both orderings.
  663. testCases := map[string]struct {
  664. results []*source.RAMUsageMaxResult
  665. }{
  666. "large first, small second": {
  667. results: []*source.RAMUsageMaxResult{
  668. ramUsageMaxResult(podKey1, container, large),
  669. ramUsageMaxResult(podKey1, container, small),
  670. },
  671. },
  672. "small first, large second": {
  673. results: []*source.RAMUsageMaxResult{
  674. ramUsageMaxResult(podKey1, container, small),
  675. ramUsageMaxResult(podKey1, container, large),
  676. },
  677. },
  678. }
  679. for name, tc := range testCases {
  680. t.Run(name, func(t *testing.T) {
  681. podMap := makePodMapWithEmptyAllocations(podKey1)
  682. applyRAMBytesUsedMax(podMap, tc.results, map[podKey][]podKey{})
  683. alloc, ok := podMap[podKey1].Allocations[container]
  684. if !ok {
  685. t.Fatalf("container allocation %q was not created", container)
  686. }
  687. if alloc.RawAllocationOnly == nil {
  688. t.Fatalf("RawAllocationOnly was not populated for container %q", container)
  689. }
  690. if got := alloc.RawAllocationOnly.RAMBytesUsageMax; got != large {
  691. t.Errorf("RAMBytesUsageMax = %v; want %v (max across duplicate rows)", got, large)
  692. }
  693. })
  694. }
  695. }
  696. // TestApplyCPUCoresUsedMax_KeepsLargestAcrossDuplicateRows is the CPU analogue
  697. // of TestApplyRAMBytesUsedMax_KeepsLargestAcrossDuplicateRows.
  698. func TestApplyCPUCoresUsedMax_KeepsLargestAcrossDuplicateRows(t *testing.T) {
  699. const container = "c1"
  700. const small = 0.01
  701. const large = 1.75
  702. testCases := map[string]struct {
  703. results []*source.CPUUsageMaxResult
  704. }{
  705. "large first, small second": {
  706. results: []*source.CPUUsageMaxResult{
  707. cpuUsageMaxResult(podKey1, container, large),
  708. cpuUsageMaxResult(podKey1, container, small),
  709. },
  710. },
  711. "small first, large second": {
  712. results: []*source.CPUUsageMaxResult{
  713. cpuUsageMaxResult(podKey1, container, small),
  714. cpuUsageMaxResult(podKey1, container, large),
  715. },
  716. },
  717. }
  718. for name, tc := range testCases {
  719. t.Run(name, func(t *testing.T) {
  720. podMap := makePodMapWithEmptyAllocations(podKey1)
  721. applyCPUCoresUsedMax(podMap, tc.results, map[podKey][]podKey{})
  722. alloc, ok := podMap[podKey1].Allocations[container]
  723. if !ok {
  724. t.Fatalf("container allocation %q was not created", container)
  725. }
  726. if alloc.RawAllocationOnly == nil {
  727. t.Fatalf("RawAllocationOnly was not populated for container %q", container)
  728. }
  729. if got := alloc.RawAllocationOnly.CPUCoreUsageMax; got != large {
  730. t.Errorf("CPUCoreUsageMax = %v; want %v (max across duplicate rows)", got, large)
  731. }
  732. })
  733. }
  734. }
  735. // TestApplyGPUUsageMax_KeepsLargestAcrossDuplicateRows is the GPU analogue.
  736. // It additionally asserts the behavior for the pointer-valued GPUUsageMax
  737. // field, covering both the fresh-create and update paths.
  738. func TestApplyGPUUsageMax_KeepsLargestAcrossDuplicateRows(t *testing.T) {
  739. const container = "c1"
  740. const small = 0.02
  741. const large = 0.97
  742. testCases := map[string]struct {
  743. results []*source.GPUsUsageMaxResult
  744. }{
  745. "large first, small second": {
  746. results: []*source.GPUsUsageMaxResult{
  747. gpuUsageMaxResult(podKey1, container, large),
  748. gpuUsageMaxResult(podKey1, container, small),
  749. },
  750. },
  751. "small first, large second": {
  752. results: []*source.GPUsUsageMaxResult{
  753. gpuUsageMaxResult(podKey1, container, small),
  754. gpuUsageMaxResult(podKey1, container, large),
  755. },
  756. },
  757. }
  758. for name, tc := range testCases {
  759. t.Run(name, func(t *testing.T) {
  760. podMap := makePodMapWithEmptyAllocations(podKey1)
  761. applyGPUUsageMax(podMap, tc.results, map[podKey][]podKey{})
  762. alloc, ok := podMap[podKey1].Allocations[container]
  763. if !ok {
  764. t.Fatalf("container allocation %q was not created", container)
  765. }
  766. if alloc.RawAllocationOnly == nil {
  767. t.Fatalf("RawAllocationOnly was not populated for container %q", container)
  768. }
  769. ptr := alloc.RawAllocationOnly.GPUUsageMax
  770. if ptr == nil {
  771. t.Fatalf("GPUUsageMax was nil; expected %v", large)
  772. }
  773. if *ptr != large {
  774. t.Errorf("GPUUsageMax = %v; want %v (max across duplicate rows)", *ptr, large)
  775. }
  776. })
  777. }
  778. }