intervals_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. package costmodel
  2. import (
  3. "reflect"
  4. "testing"
  5. "time"
  6. "github.com/opencost/opencost/pkg/kubecost"
  7. )
  8. func TestGetIntervalPointsFromWindows(t *testing.T) {
  9. cases := []struct {
  10. name string
  11. pvcIntervalMap map[podKey]kubecost.Window
  12. expected []IntervalPoint
  13. }{
  14. {
  15. name: "four pods w/ various overlaps",
  16. pvcIntervalMap: map[podKey]kubecost.Window{
  17. // Pod running from 8 am to 9 am
  18. {
  19. Pod: "Pod1",
  20. }: kubecost.Window(kubecost.NewClosedWindow(
  21. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  22. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  23. )),
  24. // Pod running from 8:30 am to 9 am
  25. {
  26. Pod: "Pod2",
  27. }: kubecost.Window(kubecost.NewClosedWindow(
  28. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  29. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  30. )),
  31. // Pod running from 8:45 am to 9 am
  32. {
  33. Pod: "Pod3",
  34. }: kubecost.Window(kubecost.NewClosedWindow(
  35. time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC),
  36. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  37. )),
  38. // Pod running from 8 am to 8:15 am
  39. {
  40. Pod: "Pod4",
  41. }: kubecost.Window(kubecost.NewClosedWindow(
  42. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  43. time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC),
  44. )),
  45. },
  46. expected: []IntervalPoint{
  47. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  48. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod4"}),
  49. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "end", podKey{Pod: "Pod4"}),
  50. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod2"}),
  51. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "start", podKey{Pod: "Pod3"}),
  52. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod2"}),
  53. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod3"}),
  54. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  55. },
  56. },
  57. {
  58. name: "two pods no overlap",
  59. pvcIntervalMap: map[podKey]kubecost.Window{
  60. // Pod running from 8 am to 8:30 am
  61. {
  62. Pod: "Pod1",
  63. }: kubecost.Window(kubecost.NewClosedWindow(
  64. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  65. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  66. )),
  67. // Pod running from 8:30 am to 9 am
  68. {
  69. Pod: "Pod2",
  70. }: kubecost.Window(kubecost.NewClosedWindow(
  71. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  72. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  73. )),
  74. },
  75. expected: []IntervalPoint{
  76. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  77. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod2"}),
  78. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  79. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod2"}),
  80. },
  81. },
  82. {
  83. name: "two pods total overlap",
  84. pvcIntervalMap: map[podKey]kubecost.Window{
  85. // Pod running from 8:30 am to 9 am
  86. {
  87. Pod: "Pod1",
  88. }: kubecost.Window(kubecost.NewClosedWindow(
  89. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  90. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  91. )),
  92. // Pod running from 8:30 am to 9 am
  93. {
  94. Pod: "Pod2",
  95. }: kubecost.Window(kubecost.NewClosedWindow(
  96. time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC),
  97. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  98. )),
  99. },
  100. expected: []IntervalPoint{
  101. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  102. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", podKey{Pod: "Pod2"}),
  103. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  104. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod2"}),
  105. },
  106. },
  107. {
  108. name: "one pod",
  109. pvcIntervalMap: map[podKey]kubecost.Window{
  110. // Pod running from 8 am to 9 am
  111. {
  112. Pod: "Pod1",
  113. }: kubecost.Window(kubecost.NewClosedWindow(
  114. time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  115. time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  116. )),
  117. },
  118. expected: []IntervalPoint{
  119. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", podKey{Pod: "Pod1"}),
  120. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", podKey{Pod: "Pod1"}),
  121. },
  122. },
  123. }
  124. for _, testCase := range cases {
  125. t.Run(testCase.name, func(t *testing.T) {
  126. result := getIntervalPointsFromWindows(testCase.pvcIntervalMap)
  127. if len(result) != len(testCase.expected) {
  128. t.Errorf("getIntervalPointsFromWindows test failed: %s: Got %+v but expected %+v", testCase.name, result, testCase.expected)
  129. }
  130. for i := range testCase.expected {
  131. // For correctness in terms of individual position of IntervalPoints, we only need to check the time/type.
  132. // Key is used in other associated calculations, so it must exist, but order does not matter if other sorting
  133. // logic is obeyed.
  134. if !testCase.expected[i].Time.Equal(result[i].Time) || testCase.expected[i].PointType != result[i].PointType {
  135. t.Errorf("getIntervalPointsFromWindows test failed: %s: Got point %s:%s but expected %s:%s", testCase.name, testCase.expected[i].PointType, testCase.expected[i].Time, result[i].PointType, result[i].Time)
  136. }
  137. }
  138. })
  139. }
  140. }
  141. func TestGetPVCCostCoefficients(t *testing.T) {
  142. pvc1 := &pvc{
  143. Bytes: 0,
  144. Name: "pvc1",
  145. Cluster: "cluster1",
  146. Namespace: "namespace1",
  147. Start: time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC),
  148. End: time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  149. }
  150. pod1Key := newPodKey("cluster1", "namespace1", "pod1")
  151. pod2Key := newPodKey("cluster1", "namespace1", "pod2")
  152. pod3Key := newPodKey("cluster1", "namespace1", "pod3")
  153. pod4Key := newPodKey("cluster1", "namespace1", "pod4")
  154. ummountedPodKey := newPodKey("cluster1", kubecost.UnmountedSuffix, kubecost.UnmountedSuffix)
  155. zeroDuration, _ := time.ParseDuration("0m0s")
  156. fiveMinOffset, _ := time.ParseDuration("5m")
  157. // Reflects the pvc that is offset by duration, the actual case that happens in allocation workflow.
  158. // before core-370, the offset was causing a unmounted shared pvc coefficient map for the duration of the offset.
  159. pvcWithDurationOffset := &pvc{
  160. Bytes: 0,
  161. Name: "pvc1",
  162. Cluster: "cluster1",
  163. Namespace: "namespace1",
  164. Start: time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC).Add(-fiveMinOffset),
  165. End: time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC),
  166. }
  167. cases := []struct {
  168. name string
  169. pvc *pvc
  170. pvcIntervalMap map[podKey]kubecost.Window
  171. intervals []IntervalPoint
  172. resolution time.Duration
  173. expected map[podKey][]CoefficientComponent
  174. }{
  175. {
  176. name: "four pods w/ various overlaps",
  177. pvc: pvc1,
  178. intervals: []IntervalPoint{
  179. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  180. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod4Key),
  181. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "end", pod4Key),
  182. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  183. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "start", pod3Key),
  184. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  185. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod3Key),
  186. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod1Key),
  187. },
  188. resolution: zeroDuration,
  189. expected: map[podKey][]CoefficientComponent{
  190. pod1Key: {
  191. {0.5, 0.25},
  192. {1, 0.25},
  193. {0.5, 0.25},
  194. {1.0 / 3.0, 0.25},
  195. },
  196. pod2Key: {
  197. {0.5, 0.25},
  198. {1.0 / 3.0, 0.25},
  199. },
  200. pod3Key: {
  201. {1.0 / 3.0, 0.25},
  202. },
  203. pod4Key: {
  204. {0.5, 0.25},
  205. },
  206. },
  207. },
  208. {
  209. name: "two pods no overlap",
  210. pvc: pvc1,
  211. intervals: []IntervalPoint{
  212. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  213. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  214. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "end", pod1Key),
  215. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  216. },
  217. resolution: zeroDuration,
  218. expected: map[podKey][]CoefficientComponent{
  219. pod1Key: {
  220. {1.0, 0.5},
  221. },
  222. pod2Key: {
  223. {1.0, 0.5},
  224. },
  225. },
  226. },
  227. {
  228. name: "two pods total overlap",
  229. pvc: pvc1,
  230. intervals: []IntervalPoint{
  231. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod1Key),
  232. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  233. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod1Key),
  234. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  235. },
  236. resolution: zeroDuration,
  237. expected: map[podKey][]CoefficientComponent{
  238. pod1Key: {
  239. {0.5, 0.5},
  240. },
  241. pod2Key: {
  242. {0.5, 0.5},
  243. },
  244. ummountedPodKey: {
  245. {1.0, 0.5},
  246. },
  247. },
  248. },
  249. {
  250. name: "one pod",
  251. pvc: pvc1,
  252. intervals: []IntervalPoint{
  253. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  254. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod1Key),
  255. },
  256. resolution: zeroDuration,
  257. expected: map[podKey][]CoefficientComponent{
  258. pod1Key: {
  259. {1.0, 1.0},
  260. },
  261. },
  262. },
  263. {
  264. name: "two pods with gap",
  265. pvc: pvc1,
  266. intervals: []IntervalPoint{
  267. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  268. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "end", pod1Key),
  269. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "start", pod2Key),
  270. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  271. },
  272. resolution: zeroDuration,
  273. expected: map[podKey][]CoefficientComponent{
  274. pod1Key: {
  275. {1.0, 0.25},
  276. },
  277. pod2Key: {
  278. {1.0, 0.25},
  279. },
  280. ummountedPodKey: {
  281. {1.0, 0.5},
  282. },
  283. },
  284. },
  285. {
  286. name: "one pods start and end in window",
  287. pvc: pvc1,
  288. intervals: []IntervalPoint{
  289. NewIntervalPoint(time.Date(2021, 2, 19, 8, 15, 0, 0, time.UTC), "start", pod1Key),
  290. NewIntervalPoint(time.Date(2021, 2, 19, 8, 45, 0, 0, time.UTC), "end", pod1Key),
  291. },
  292. resolution: zeroDuration,
  293. expected: map[podKey][]CoefficientComponent{
  294. pod1Key: {
  295. {1.0, 0.5},
  296. },
  297. ummountedPodKey: {
  298. {1.0, 0.25},
  299. {1.0, 0.25},
  300. },
  301. },
  302. },
  303. // Test case to ensure the offset doesnt cause any unmounted
  304. {
  305. name: "pvcMap with duration offset not causing any unmounted entry in sharedPV Coefficient",
  306. pvc: pvcWithDurationOffset,
  307. intervals: []IntervalPoint{
  308. NewIntervalPoint(time.Date(2021, 2, 19, 8, 0, 0, 0, time.UTC), "start", pod1Key),
  309. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "start", pod2Key),
  310. NewIntervalPoint(time.Date(2021, 2, 19, 8, 30, 0, 0, time.UTC), "end", pod1Key),
  311. NewIntervalPoint(time.Date(2021, 2, 19, 9, 0, 0, 0, time.UTC), "end", pod2Key),
  312. },
  313. resolution: fiveMinOffset,
  314. expected: map[podKey][]CoefficientComponent{
  315. pod1Key: {
  316. {1.0, 0.5},
  317. },
  318. pod2Key: {
  319. {1.0, 0.5},
  320. },
  321. },
  322. },
  323. }
  324. for _, testCase := range cases {
  325. t.Run(testCase.name, func(t *testing.T) {
  326. result := getPVCCostCoefficients(testCase.intervals, testCase.pvc, testCase.resolution)
  327. if !reflect.DeepEqual(result, testCase.expected) {
  328. t.Errorf("getPVCCostCoefficients test failed: %s: Got %+v but expected %+v", testCase.name, result, testCase.expected)
  329. }
  330. // check that coefficients sum to 1, to ensure that 100% of PVC cost is being distributed
  331. sum := 0.0
  332. for _, coefs := range result {
  333. sum += getCoefficientFromComponents(coefs)
  334. }
  335. if sum != 1.0 {
  336. t.Errorf("getPVCCostCoefficients test failed: coefficient totals did not sum to 1.0: %f", sum)
  337. }
  338. })
  339. }
  340. }