allocation_test.go 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783
  1. package kubecost
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "math"
  6. "testing"
  7. "time"
  8. "github.com/kubecost/cost-model/pkg/util"
  9. )
  10. const day = 24 * time.Hour
  11. func NewUnitAllocation(name string, start time.Time, resolution time.Duration, props *Properties) *Allocation {
  12. if name == "" {
  13. name = "cluster1/namespace1/pod1/container1"
  14. }
  15. properties := &Properties{}
  16. if props == nil {
  17. properties.SetCluster("cluster1")
  18. properties.SetNode("node1")
  19. properties.SetNamespace("namespace1")
  20. properties.SetControllerKind("deployment")
  21. properties.SetController("deployment1")
  22. properties.SetPod("pod1")
  23. properties.SetContainer("container1")
  24. } else {
  25. properties = props
  26. }
  27. end := start.Add(resolution)
  28. alloc := &Allocation{
  29. Name: name,
  30. Properties: *properties,
  31. Window: NewWindow(&start, &end).Clone(),
  32. Start: start,
  33. End: end,
  34. CPUCoreHours: 1,
  35. CPUCost: 1,
  36. CPUCoreRequestAverage: 1,
  37. CPUCoreUsageAverage: 1,
  38. GPUHours: 1,
  39. GPUCost: 1,
  40. NetworkCost: 1,
  41. PVByteHours: 1,
  42. PVCost: 1,
  43. RAMByteHours: 1,
  44. RAMCost: 1,
  45. RAMBytesRequestAverage: 1,
  46. RAMBytesUsageAverage: 1,
  47. }
  48. // If idle allocation, remove non-idle costs, but maintain total cost
  49. if alloc.IsIdle() {
  50. alloc.PVByteHours = 0.0
  51. alloc.PVCost = 0.0
  52. alloc.NetworkCost = 0.0
  53. alloc.CPUCoreHours += 1.0
  54. alloc.CPUCost += 1.0
  55. alloc.RAMByteHours += 1.0
  56. alloc.RAMCost += 1.0
  57. }
  58. return alloc
  59. }
  60. func TestAllocation_Add(t *testing.T) {
  61. var nilAlloc *Allocation
  62. zeroAlloc := &Allocation{}
  63. // nil + nil == nil
  64. nilNilSum, err := nilAlloc.Add(nilAlloc)
  65. if err != nil {
  66. t.Fatalf("Allocation.Add unexpected error: %s", err)
  67. }
  68. if nilNilSum != nil {
  69. t.Fatalf("Allocation.Add failed; exp: nil; act: %s", nilNilSum)
  70. }
  71. // nil + zero == zero
  72. nilZeroSum, err := nilAlloc.Add(zeroAlloc)
  73. if err != nil {
  74. t.Fatalf("Allocation.Add unexpected error: %s", err)
  75. }
  76. if nilZeroSum == nil || nilZeroSum.TotalCost() != 0.0 {
  77. t.Fatalf("Allocation.Add failed; exp: 0.0; act: %s", nilZeroSum)
  78. }
  79. cpuPrice := 0.02
  80. gpuPrice := 2.00
  81. ramPrice := 0.01
  82. pvPrice := 0.00005
  83. gib := 1024.0 * 1024.0 * 1024.0
  84. s1 := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
  85. e1 := time.Date(2021, time.January, 1, 12, 0, 0, 0, time.UTC)
  86. hrs1 := e1.Sub(s1).Hours()
  87. a1 := &Allocation{
  88. Start: s1,
  89. End: e1,
  90. CPUCoreHours: 2.0 * hrs1,
  91. CPUCoreRequestAverage: 2.0,
  92. CPUCoreUsageAverage: 1.0,
  93. CPUCost: 2.0 * hrs1 * cpuPrice,
  94. GPUHours: 1.0 * hrs1,
  95. GPUCost: 1.0 * hrs1 * gpuPrice,
  96. PVByteHours: 100.0 * gib * hrs1,
  97. PVCost: 100.0 * hrs1 * pvPrice,
  98. RAMByteHours: 8.0 * gib * hrs1,
  99. RAMBytesRequestAverage: 8.0 * gib,
  100. RAMBytesUsageAverage: 4.0 * gib,
  101. RAMCost: 8.0 * hrs1 * ramPrice,
  102. SharedCost: 2.00,
  103. ExternalCost: 1.00,
  104. }
  105. a1b := a1.Clone()
  106. s2 := time.Date(2021, time.January, 1, 6, 0, 0, 0, time.UTC)
  107. e2 := time.Date(2021, time.January, 1, 24, 0, 0, 0, time.UTC)
  108. hrs2 := e1.Sub(s1).Hours()
  109. a2 := &Allocation{
  110. Start: s2,
  111. End: e2,
  112. CPUCoreHours: 1.0 * hrs2,
  113. CPUCoreRequestAverage: 1.0,
  114. CPUCoreUsageAverage: 1.0,
  115. CPUCost: 1.0 * hrs2 * cpuPrice,
  116. GPUHours: 0.0,
  117. GPUCost: 0.0,
  118. PVByteHours: 0,
  119. PVCost: 0,
  120. RAMByteHours: 8.0 * gib * hrs2,
  121. RAMBytesRequestAverage: 0.0,
  122. RAMBytesUsageAverage: 8.0 * gib,
  123. RAMCost: 8.0 * hrs2 * ramPrice,
  124. NetworkCost: 0.01,
  125. SharedCost: 0.00,
  126. ExternalCost: 1.00,
  127. }
  128. a2b := a2.Clone()
  129. act, err := a1.Add(a2)
  130. if err != nil {
  131. t.Fatalf("Allocation.Add: unexpected error: %s", err)
  132. }
  133. // Neither Allocation should be mutated
  134. if !a1.Equal(a1b) {
  135. t.Fatalf("Allocation.Add: a1 illegally mutated")
  136. }
  137. if !a2.Equal(a2b) {
  138. t.Fatalf("Allocation.Add: a1 illegally mutated")
  139. }
  140. // Costs should be cumulative
  141. if !util.IsApproximately(a1.TotalCost()+a2.TotalCost(), act.TotalCost()) {
  142. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.TotalCost()+a2.TotalCost(), act.TotalCost())
  143. }
  144. if !util.IsApproximately(a1.CPUCost+a2.CPUCost, act.CPUCost) {
  145. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.CPUCost+a2.CPUCost, act.CPUCost)
  146. }
  147. if !util.IsApproximately(a1.GPUCost+a2.GPUCost, act.GPUCost) {
  148. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.GPUCost+a2.GPUCost, act.GPUCost)
  149. }
  150. if !util.IsApproximately(a1.RAMCost+a2.RAMCost, act.RAMCost) {
  151. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.RAMCost+a2.RAMCost, act.RAMCost)
  152. }
  153. if !util.IsApproximately(a1.PVCost+a2.PVCost, act.PVCost) {
  154. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.PVCost+a2.PVCost, act.PVCost)
  155. }
  156. if !util.IsApproximately(a1.NetworkCost+a2.NetworkCost, act.NetworkCost) {
  157. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.NetworkCost+a2.NetworkCost, act.NetworkCost)
  158. }
  159. if !util.IsApproximately(a1.SharedCost+a2.SharedCost, act.SharedCost) {
  160. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.SharedCost+a2.SharedCost, act.SharedCost)
  161. }
  162. if !util.IsApproximately(a1.ExternalCost+a2.ExternalCost, act.ExternalCost) {
  163. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.ExternalCost+a2.ExternalCost, act.ExternalCost)
  164. }
  165. // ResourceHours should be cumulative
  166. if !util.IsApproximately(a1.CPUCoreHours+a2.CPUCoreHours, act.CPUCoreHours) {
  167. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.CPUCoreHours+a2.CPUCoreHours, act.CPUCoreHours)
  168. }
  169. if !util.IsApproximately(a1.RAMByteHours+a2.RAMByteHours, act.RAMByteHours) {
  170. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.RAMByteHours+a2.RAMByteHours, act.RAMByteHours)
  171. }
  172. if !util.IsApproximately(a1.PVByteHours+a2.PVByteHours, act.PVByteHours) {
  173. t.Fatalf("Allocation.Add: expected %f; actual %f", a1.PVByteHours+a2.PVByteHours, act.PVByteHours)
  174. }
  175. // Minutes should be the duration between min(starts) and max(ends)
  176. if !act.Start.Equal(a1.Start) || !act.End.Equal(a2.End) {
  177. t.Fatalf("Allocation.Add: expected %s; actual %s", NewWindow(&a1.Start, &a2.End), NewWindow(&act.Start, &act.End))
  178. }
  179. if act.Minutes() != 1440.0 {
  180. t.Fatalf("Allocation.Add: expected %f; actual %f", 1440.0, act.Minutes())
  181. }
  182. // Requests and Usage should be averaged correctly
  183. // CPU requests = (2.0*12.0 + 1.0*18.0)/(24.0) = 1.75
  184. // CPU usage = (1.0*12.0 + 1.0*18.0)/(24.0) = 1.25
  185. // RAM requests = (8.0*12.0 + 0.0*18.0)/(24.0) = 4.00
  186. // RAM usage = (4.0*12.0 + 8.0*18.0)/(24.0) = 8.00
  187. if !util.IsApproximately(1.75, act.CPUCoreRequestAverage) {
  188. t.Fatalf("Allocation.Add: expected %f; actual %f", 1.75, act.CPUCoreRequestAverage)
  189. }
  190. if !util.IsApproximately(1.25, act.CPUCoreUsageAverage) {
  191. t.Fatalf("Allocation.Add: expected %f; actual %f", 1.25, act.CPUCoreUsageAverage)
  192. }
  193. if !util.IsApproximately(4.00*gib, act.RAMBytesRequestAverage) {
  194. t.Fatalf("Allocation.Add: expected %f; actual %f", 4.00*gib, act.RAMBytesRequestAverage)
  195. }
  196. if !util.IsApproximately(8.00*gib, act.RAMBytesUsageAverage) {
  197. t.Fatalf("Allocation.Add: expected %f; actual %f", 8.00*gib, act.RAMBytesUsageAverage)
  198. }
  199. // Efficiency should be computed accurately from new request/usage
  200. // CPU efficiency = 1.25/1.75 = 0.7142857
  201. // RAM efficiency = 8.00/4.00 = 2.0000000
  202. // Total efficiency = (0.7142857*0.72 + 2.0*1.92)/(2.64) = 1.6493506
  203. if !util.IsApproximately(0.7142857, act.CPUEfficiency()) {
  204. t.Fatalf("Allocation.Add: expected %f; actual %f", 0.7142857, act.CPUEfficiency())
  205. }
  206. if !util.IsApproximately(2.0000000, act.RAMEfficiency()) {
  207. t.Fatalf("Allocation.Add: expected %f; actual %f", 2.0000000, act.RAMEfficiency())
  208. }
  209. if !util.IsApproximately(1.6493506, act.TotalEfficiency()) {
  210. t.Fatalf("Allocation.Add: expected %f; actual %f", 1.6493506, act.TotalEfficiency())
  211. }
  212. }
  213. func TestAllocation_Share(t *testing.T) {
  214. cpuPrice := 0.02
  215. gpuPrice := 2.00
  216. ramPrice := 0.01
  217. pvPrice := 0.00005
  218. gib := 1024.0 * 1024.0 * 1024.0
  219. s1 := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
  220. e1 := time.Date(2021, time.January, 1, 12, 0, 0, 0, time.UTC)
  221. hrs1 := e1.Sub(s1).Hours()
  222. a1 := &Allocation{
  223. Start: s1,
  224. End: e1,
  225. CPUCoreHours: 2.0 * hrs1,
  226. CPUCoreRequestAverage: 2.0,
  227. CPUCoreUsageAverage: 1.0,
  228. CPUCost: 2.0 * hrs1 * cpuPrice,
  229. GPUHours: 1.0 * hrs1,
  230. GPUCost: 1.0 * hrs1 * gpuPrice,
  231. PVByteHours: 100.0 * gib * hrs1,
  232. PVCost: 100.0 * hrs1 * pvPrice,
  233. RAMByteHours: 8.0 * gib * hrs1,
  234. RAMBytesRequestAverage: 8.0 * gib,
  235. RAMBytesUsageAverage: 4.0 * gib,
  236. RAMCost: 8.0 * hrs1 * ramPrice,
  237. SharedCost: 2.00,
  238. ExternalCost: 1.00,
  239. }
  240. a1b := a1.Clone()
  241. s2 := time.Date(2021, time.January, 1, 6, 0, 0, 0, time.UTC)
  242. e2 := time.Date(2021, time.January, 1, 24, 0, 0, 0, time.UTC)
  243. hrs2 := e1.Sub(s1).Hours()
  244. a2 := &Allocation{
  245. Start: s2,
  246. End: e2,
  247. CPUCoreHours: 1.0 * hrs2,
  248. CPUCoreRequestAverage: 1.0,
  249. CPUCoreUsageAverage: 1.0,
  250. CPUCost: 1.0 * hrs2 * cpuPrice,
  251. GPUHours: 0.0,
  252. GPUCost: 0.0,
  253. PVByteHours: 0,
  254. PVCost: 0,
  255. RAMByteHours: 8.0 * gib * hrs2,
  256. RAMBytesRequestAverage: 0.0,
  257. RAMBytesUsageAverage: 8.0 * gib,
  258. RAMCost: 8.0 * hrs2 * ramPrice,
  259. NetworkCost: 0.01,
  260. SharedCost: 0.00,
  261. ExternalCost: 1.00,
  262. }
  263. a2b := a2.Clone()
  264. act, err := a1.Share(a2)
  265. if err != nil {
  266. t.Fatalf("Allocation.Share: unexpected error: %s", err)
  267. }
  268. // Neither Allocation should be mutated
  269. if !a1.Equal(a1b) {
  270. t.Fatalf("Allocation.Share: a1 illegally mutated")
  271. }
  272. if !a2.Equal(a2b) {
  273. t.Fatalf("Allocation.Share: a1 illegally mutated")
  274. }
  275. // SharedCost and TotalCost should reflect increase by a2.TotalCost
  276. if !util.IsApproximately(a1.TotalCost()+a2.TotalCost(), act.TotalCost()) {
  277. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.TotalCost()+a2.TotalCost(), act.TotalCost())
  278. }
  279. if !util.IsApproximately(a1.SharedCost+a2.TotalCost(), act.SharedCost) {
  280. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.SharedCost+a2.TotalCost(), act.SharedCost)
  281. }
  282. // Costs should match before (expect TotalCost and SharedCost)
  283. if !util.IsApproximately(a1.CPUCost, act.CPUCost) {
  284. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.CPUCost, act.CPUCost)
  285. }
  286. if !util.IsApproximately(a1.GPUCost, act.GPUCost) {
  287. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.GPUCost, act.GPUCost)
  288. }
  289. if !util.IsApproximately(a1.RAMCost, act.RAMCost) {
  290. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.RAMCost, act.RAMCost)
  291. }
  292. if !util.IsApproximately(a1.PVCost, act.PVCost) {
  293. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.PVCost, act.PVCost)
  294. }
  295. if !util.IsApproximately(a1.NetworkCost, act.NetworkCost) {
  296. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.NetworkCost, act.NetworkCost)
  297. }
  298. if !util.IsApproximately(a1.ExternalCost, act.ExternalCost) {
  299. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.ExternalCost, act.ExternalCost)
  300. }
  301. // ResourceHours should match before
  302. if !util.IsApproximately(a1.CPUCoreHours, act.CPUCoreHours) {
  303. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.CPUCoreHours, act.CPUCoreHours)
  304. }
  305. if !util.IsApproximately(a1.RAMByteHours, act.RAMByteHours) {
  306. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.RAMByteHours, act.RAMByteHours)
  307. }
  308. if !util.IsApproximately(a1.PVByteHours, act.PVByteHours) {
  309. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.PVByteHours, act.PVByteHours)
  310. }
  311. // Minutes should match before
  312. if !act.Start.Equal(a1.Start) || !act.End.Equal(a1.End) {
  313. t.Fatalf("Allocation.Share: expected %s; actual %s", NewWindow(&a1.Start, &a1.End), NewWindow(&act.Start, &act.End))
  314. }
  315. if act.Minutes() != a1.Minutes() {
  316. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.Minutes(), act.Minutes())
  317. }
  318. // Requests and Usage should match before
  319. if !util.IsApproximately(a1.CPUCoreRequestAverage, act.CPUCoreRequestAverage) {
  320. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.CPUCoreRequestAverage, act.CPUCoreRequestAverage)
  321. }
  322. if !util.IsApproximately(a1.CPUCoreUsageAverage, act.CPUCoreUsageAverage) {
  323. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.CPUCoreUsageAverage, act.CPUCoreUsageAverage)
  324. }
  325. if !util.IsApproximately(a1.RAMBytesRequestAverage, act.RAMBytesRequestAverage) {
  326. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.RAMBytesRequestAverage, act.RAMBytesRequestAverage)
  327. }
  328. if !util.IsApproximately(a1.RAMBytesUsageAverage, act.RAMBytesUsageAverage) {
  329. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.RAMBytesUsageAverage, act.RAMBytesUsageAverage)
  330. }
  331. // Efficiency should match before
  332. if !util.IsApproximately(a1.CPUEfficiency(), act.CPUEfficiency()) {
  333. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.CPUEfficiency(), act.CPUEfficiency())
  334. }
  335. if !util.IsApproximately(a1.RAMEfficiency(), act.RAMEfficiency()) {
  336. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.RAMEfficiency(), act.RAMEfficiency())
  337. }
  338. if !util.IsApproximately(a1.TotalEfficiency(), act.TotalEfficiency()) {
  339. t.Fatalf("Allocation.Share: expected %f; actual %f", a1.TotalEfficiency(), act.TotalEfficiency())
  340. }
  341. }
  342. func TestAllocation_MarshalJSON(t *testing.T) {
  343. start := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
  344. end := time.Date(2021, time.January, 2, 0, 0, 0, 0, time.UTC)
  345. hrs := 24.0
  346. gib := 1024.0 * 1024.0 * 1024.0
  347. cpuPrice := 0.02
  348. gpuPrice := 2.00
  349. ramPrice := 0.01
  350. pvPrice := 0.00005
  351. before := &Allocation{
  352. Name: "cluster1/namespace1/node1/pod1/container1",
  353. Properties: Properties{
  354. ClusterProp: "cluster1",
  355. NodeProp: "node1",
  356. NamespaceProp: "namespace1",
  357. PodProp: "pod1",
  358. ContainerProp: "container1",
  359. },
  360. Window: NewWindow(&start, &end),
  361. Start: start,
  362. End: end,
  363. CPUCoreHours: 2.0 * hrs,
  364. CPUCoreRequestAverage: 2.0,
  365. CPUCoreUsageAverage: 1.0,
  366. CPUCost: 2.0 * hrs * cpuPrice,
  367. GPUHours: 1.0 * hrs,
  368. GPUCost: 1.0 * hrs * gpuPrice,
  369. NetworkCost: 0.05,
  370. PVByteHours: 100.0 * gib * hrs,
  371. PVCost: 100.0 * hrs * pvPrice,
  372. RAMByteHours: 8.0 * gib * hrs,
  373. RAMBytesRequestAverage: 8.0 * gib,
  374. RAMBytesUsageAverage: 4.0 * gib,
  375. RAMCost: 8.0 * hrs * ramPrice,
  376. SharedCost: 2.00,
  377. ExternalCost: 1.00,
  378. }
  379. data, err := json.Marshal(before)
  380. if err != nil {
  381. t.Fatalf("Allocation.MarshalJSON: unexpected error: %s", err)
  382. }
  383. after := &Allocation{}
  384. err = json.Unmarshal(data, after)
  385. if err != nil {
  386. t.Fatalf("Allocation.UnmarshalJSON: unexpected error: %s", err)
  387. }
  388. // TODO:CLEANUP fix json marshaling of Window so that all of this works.
  389. // In the meantime, just set the Window so that we can test the rest.
  390. after.Window = before.Window.Clone()
  391. fmt.Println(*before)
  392. fmt.Println(*after)
  393. if !after.Equal(before) {
  394. t.Fatalf("Allocation.MarshalJSON: before and after are not equal")
  395. }
  396. }
  397. func TestNewAllocationSet(t *testing.T) {
  398. // TODO niko/etl
  399. }
  400. func generateAllocationSet(start time.Time) *AllocationSet {
  401. // Idle allocations
  402. a1i := NewUnitAllocation(fmt.Sprintf("cluster1/%s", IdleSuffix), start, day, &Properties{
  403. ClusterProp: "cluster1",
  404. NodeProp: "node1",
  405. })
  406. a1i.CPUCost = 5.0
  407. a1i.RAMCost = 15.0
  408. a1i.GPUCost = 0.0
  409. a2i := NewUnitAllocation(fmt.Sprintf("cluster2/%s", IdleSuffix), start, day, &Properties{
  410. ClusterProp: "cluster2",
  411. })
  412. a2i.CPUCost = 5.0
  413. a2i.RAMCost = 5.0
  414. a2i.GPUCost = 0.0
  415. // Active allocations
  416. a1111 := NewUnitAllocation("cluster1/namespace1/pod1/container1", start, day, &Properties{
  417. ClusterProp: "cluster1",
  418. NamespaceProp: "namespace1",
  419. PodProp: "pod1",
  420. ContainerProp: "container1",
  421. })
  422. a1111.RAMCost = 11.00
  423. a11abc2 := NewUnitAllocation("cluster1/namespace1/pod-abc/container2", start, day, &Properties{
  424. ClusterProp: "cluster1",
  425. NamespaceProp: "namespace1",
  426. PodProp: "pod-abc",
  427. ContainerProp: "container2",
  428. })
  429. a11def3 := NewUnitAllocation("cluster1/namespace1/pod-def/container3", start, day, &Properties{
  430. ClusterProp: "cluster1",
  431. NamespaceProp: "namespace1",
  432. PodProp: "pod-def",
  433. ContainerProp: "container3",
  434. })
  435. a12ghi4 := NewUnitAllocation("cluster1/namespace2/pod-ghi/container4", start, day, &Properties{
  436. ClusterProp: "cluster1",
  437. NamespaceProp: "namespace2",
  438. PodProp: "pod-ghi",
  439. ContainerProp: "container4",
  440. })
  441. a12ghi5 := NewUnitAllocation("cluster1/namespace2/pod-ghi/container5", start, day, &Properties{
  442. ClusterProp: "cluster1",
  443. NamespaceProp: "namespace2",
  444. PodProp: "pod-ghi",
  445. ContainerProp: "container5",
  446. })
  447. a12jkl6 := NewUnitAllocation("cluster1/namespace2/pod-jkl/container6", start, day, &Properties{
  448. ClusterProp: "cluster1",
  449. NamespaceProp: "namespace2",
  450. PodProp: "pod-jkl",
  451. ContainerProp: "container6",
  452. })
  453. a22mno4 := NewUnitAllocation("cluster2/namespace2/pod-mno/container4", start, day, &Properties{
  454. ClusterProp: "cluster2",
  455. NamespaceProp: "namespace2",
  456. PodProp: "pod-mno",
  457. ContainerProp: "container4",
  458. })
  459. a22mno5 := NewUnitAllocation("cluster2/namespace2/pod-mno/container5", start, day, &Properties{
  460. ClusterProp: "cluster2",
  461. NamespaceProp: "namespace2",
  462. PodProp: "pod-mno",
  463. ContainerProp: "container5",
  464. })
  465. a22pqr6 := NewUnitAllocation("cluster2/namespace2/pod-pqr/container6", start, day, &Properties{
  466. ClusterProp: "cluster2",
  467. NamespaceProp: "namespace2",
  468. PodProp: "pod-pqr",
  469. ContainerProp: "container6",
  470. })
  471. a23stu7 := NewUnitAllocation("cluster2/namespace3/pod-stu/container7", start, day, &Properties{
  472. ClusterProp: "cluster2",
  473. NamespaceProp: "namespace3",
  474. PodProp: "pod-stu",
  475. ContainerProp: "container7",
  476. })
  477. a23vwx8 := NewUnitAllocation("cluster2/namespace3/pod-vwx/container8", start, day, &Properties{
  478. ClusterProp: "cluster2",
  479. NamespaceProp: "namespace3",
  480. PodProp: "pod-vwx",
  481. ContainerProp: "container8",
  482. })
  483. a23vwx9 := NewUnitAllocation("cluster2/namespace3/pod-vwx/container9", start, day, &Properties{
  484. ClusterProp: "cluster2",
  485. NamespaceProp: "namespace3",
  486. PodProp: "pod-vwx",
  487. ContainerProp: "container9",
  488. })
  489. // Controllers
  490. a11abc2.Properties.SetControllerKind("deployment")
  491. a11abc2.Properties.SetController("deployment1")
  492. a11def3.Properties.SetControllerKind("deployment")
  493. a11def3.Properties.SetController("deployment1")
  494. a12ghi4.Properties.SetControllerKind("deployment")
  495. a12ghi4.Properties.SetController("deployment2")
  496. a12ghi5.Properties.SetControllerKind("deployment")
  497. a12ghi5.Properties.SetController("deployment2")
  498. a22mno4.Properties.SetControllerKind("deployment")
  499. a22mno4.Properties.SetController("deployment2")
  500. a22mno5.Properties.SetControllerKind("deployment")
  501. a22mno5.Properties.SetController("deployment2")
  502. a23stu7.Properties.SetControllerKind("deployment")
  503. a23stu7.Properties.SetController("deployment3")
  504. a12jkl6.Properties.SetControllerKind("daemonset")
  505. a12jkl6.Properties.SetController("daemonset1")
  506. a22pqr6.Properties.SetControllerKind("daemonset")
  507. a22pqr6.Properties.SetController("daemonset1")
  508. a23vwx8.Properties.SetControllerKind("statefulset")
  509. a23vwx8.Properties.SetController("statefulset1")
  510. a23vwx9.Properties.SetControllerKind("statefulset")
  511. a23vwx9.Properties.SetController("statefulset1")
  512. // Labels
  513. a1111.Properties.SetLabels(map[string]string{"app": "app1", "env": "env1"})
  514. a12ghi4.Properties.SetLabels(map[string]string{"app": "app2", "env": "env2"})
  515. a12ghi5.Properties.SetLabels(map[string]string{"app": "app2", "env": "env2"})
  516. a22mno4.Properties.SetLabels(map[string]string{"app": "app2"})
  517. a22mno5.Properties.SetLabels(map[string]string{"app": "app2"})
  518. //Annotations
  519. a23stu7.Properties.SetAnnotations(map[string]string{"team": "team1"})
  520. a23vwx8.Properties.SetAnnotations(map[string]string{"team": "team2"})
  521. a23vwx9.Properties.SetAnnotations(map[string]string{"team": "team1"})
  522. // Services
  523. a12jkl6.Properties.SetServices([]string{"service1"})
  524. a22pqr6.Properties.SetServices([]string{"service1"})
  525. return NewAllocationSet(start, start.Add(day),
  526. // idle
  527. a1i, a2i,
  528. // cluster 1, namespace1
  529. a1111, a11abc2, a11def3,
  530. // cluster 1, namespace 2
  531. a12ghi4, a12ghi5, a12jkl6,
  532. // cluster 2, namespace 2
  533. a22mno4, a22mno5, a22pqr6,
  534. // cluster 2, namespace 3
  535. a23stu7, a23vwx8, a23vwx9,
  536. )
  537. }
  538. func assertAllocationSetTotals(t *testing.T, as *AllocationSet, msg string, err error, length int, totalCost float64) {
  539. if err != nil {
  540. t.Fatalf("AllocationSet.AggregateBy[%s]: unexpected error: %s", msg, err)
  541. }
  542. if as.Length() != length {
  543. t.Fatalf("AllocationSet.AggregateBy[%s]: expected set of length %d, actual %d", msg, length, as.Length())
  544. }
  545. if math.Round(as.TotalCost()*100) != math.Round(totalCost*100) {
  546. t.Fatalf("AllocationSet.AggregateBy[%s]: expected total cost %.2f, actual %.2f", msg, totalCost, as.TotalCost())
  547. }
  548. }
  549. func assertAllocationTotals(t *testing.T, as *AllocationSet, msg string, exps map[string]float64) {
  550. as.Each(func(k string, a *Allocation) {
  551. if exp, ok := exps[a.Name]; ok {
  552. if math.Round(a.TotalCost()*100) != math.Round(exp*100) {
  553. t.Fatalf("AllocationSet.AggregateBy[%s]: expected total cost %.2f, actual %.2f", msg, exp, a.TotalCost())
  554. }
  555. } else {
  556. t.Fatalf("AllocationSet.AggregateBy[%s]: unexpected allocation: %s", msg, a.Name)
  557. }
  558. })
  559. }
  560. func assertAllocationWindow(t *testing.T, as *AllocationSet, msg string, expStart, expEnd time.Time, expMinutes float64) {
  561. as.Each(func(k string, a *Allocation) {
  562. if !a.Start.Equal(expStart) {
  563. t.Fatalf("AllocationSet.AggregateBy[%s]: expected start %s, actual %s", msg, expStart, a.Start)
  564. }
  565. if !a.End.Equal(expEnd) {
  566. t.Fatalf("AllocationSet.AggregateBy[%s]: expected end %s, actual %s", msg, expEnd, a.End)
  567. }
  568. if a.Minutes() != expMinutes {
  569. t.Fatalf("AllocationSet.AggregateBy[%s]: expected minutes %f, actual %f", msg, expMinutes, a.Minutes())
  570. }
  571. })
  572. }
  573. func printAllocationSet(msg string, as *AllocationSet) {
  574. fmt.Printf("--- %s ---\n", msg)
  575. as.Each(func(k string, a *Allocation) {
  576. fmt.Printf(" > %s\n", a)
  577. })
  578. }
  579. func TestAllocationSet_AggregateBy(t *testing.T) {
  580. // Test AggregateBy against the following workload topology, which is
  581. // generated by generateAllocationSet:
  582. // | Hierarchy | Cost | CPU | RAM | GPU | PV | Net |
  583. // +----------------------------------------+------+------+------+------+------+------+
  584. // cluster1:
  585. // idle: 20.00 5.00 15.00 0.00 0.00 0.00
  586. // namespace1:
  587. // pod1:
  588. // container1: [app=app1, env=env1] 15.00 1.00 11.00 1.00 1.00 1.00
  589. // pod-abc: (deployment1)
  590. // container2: 5.00 1.00 1.00 1.00 1.00 1.00
  591. // pod-def: (deployment1)
  592. // container3: 5.00 1.00 1.00 1.00 1.00 1.00
  593. // namespace2:
  594. // pod-ghi: (deployment2)
  595. // container4: [app=app2, env=env2] 5.00 1.00 1.00 1.00 1.00 1.00
  596. // container5: [app=app2, env=env2] 5.00 1.00 1.00 1.00 1.00 1.00
  597. // pod-jkl: (daemonset1)
  598. // container6: {service1} 5.00 1.00 1.00 1.00 1.00 1.00
  599. // +-----------------------------------------+------+------+------+------+------+------+
  600. // cluster1 subtotal 60.00 11.00 31.00 6.00 6.00 6.00
  601. // +-----------------------------------------+------+------+------+------+------+------+
  602. // cluster2:
  603. // idle: 10.00 5.00 5.00 0.00 0.00 0.00
  604. // namespace2:
  605. // pod-mno: (deployment2)
  606. // container4: [app=app2] 5.00 1.00 1.00 1.00 1.00 1.00
  607. // container5: [app=app2] 5.00 1.00 1.00 1.00 1.00 1.00
  608. // pod-pqr: (daemonset1)
  609. // container6: {service1} 5.00 1.00 1.00 1.00 1.00 1.00
  610. // namespace3:
  611. // pod-stu: (deployment3)
  612. // container7: an[team=team1] 5.00 1.00 1.00 1.00 1.00 1.00
  613. // pod-vwx: (statefulset1)
  614. // container8: an[team=team2] 5.00 1.00 1.00 1.00 1.00 1.00
  615. // container9: an[team=team1] 5.00 1.00 1.00 1.00 1.00 1.00
  616. // +----------------------------------------+------+------+------+------+------+------+
  617. // cluster2 subtotal 40.00 11.00 11.00 6.00 6.00 6.00
  618. // +----------------------------------------+------+------+------+------+------+------+
  619. // total 100.00 22.00 42.00 12.00 12.00 12.00
  620. // +----------------------------------------+------+------+------+------+------+------+
  621. // Scenarios to test:
  622. // 1 Single-aggregation
  623. // 1a AggregationProperties=(Cluster)
  624. // 1b AggregationProperties=(Namespace)
  625. // 1c AggregationProperties=(Pod)
  626. // 1d AggregationProperties=(Container)
  627. // 1e AggregationProperties=(ControllerKind)
  628. // 1f AggregationProperties=(Controller)
  629. // 1g AggregationProperties=(Service)
  630. // 1h AggregationProperties=(Label:app)
  631. // 2 Multi-aggregation
  632. // 2a AggregationProperties=(Cluster, Namespace)
  633. // 2b AggregationProperties=(Namespace, Label:app)
  634. // 2c AggregationProperties=(Cluster, Namespace, Pod, Container)
  635. // 2d AggregationProperties=(Label:app, Label:environment)
  636. // 3 Share idle
  637. // 3a AggregationProperties=(Namespace) ShareIdle=ShareWeighted
  638. // 3b AggregationProperties=(Namespace) ShareIdle=ShareEven (TODO niko/etl)
  639. // 4 Share resources
  640. // 4a Share namespace ShareEven
  641. // 4b Share cluster ShareWeighted
  642. // 4c Share label ShareEven
  643. // 4d Share overhead ShareWeighted
  644. // 5 Filters
  645. // 5a Filter by cluster with separate idle
  646. // 5b Filter by cluster with shared idle
  647. // TODO niko/idle more filter tests
  648. // 6 Combinations and options
  649. // 6a SplitIdle
  650. // 6b Share idle with filters
  651. // 6c Share resources with filters
  652. // 6d Share idle and share resources
  653. // 7 Edge cases and errors
  654. // 7a Empty AggregationProperties
  655. // 7b Filter all
  656. // 7c Share all
  657. // 7d Share and filter the same allocations
  658. // Definitions and set-up:
  659. var as *AllocationSet
  660. var err error
  661. endYesterday := time.Now().UTC().Truncate(day)
  662. startYesterday := endYesterday.Add(-day)
  663. numClusters := 2
  664. numNamespaces := 3
  665. numPods := 9
  666. numContainers := 9
  667. numControllerKinds := 3
  668. numControllers := 5
  669. numServices := 1
  670. numLabelApps := 2
  671. // By default, idle is reported as a single, merged allocation
  672. numIdle := 1
  673. // There will only ever be one __unallocated__
  674. numUnallocated := 1
  675. // There are two clusters, so each gets an idle entry when they are split
  676. numSplitIdle := 2
  677. activeTotalCost := 70.0
  678. idleTotalCost := 30.0
  679. sharedOverheadHourlyCost := 7.0
  680. isNamespace3 := func(a *Allocation) bool {
  681. ns, err := a.Properties.GetNamespace()
  682. return err == nil && ns == "namespace3"
  683. }
  684. isApp1 := func(a *Allocation) bool {
  685. ls, _ := a.Properties.GetLabels()
  686. if app, ok := ls["app"]; ok && app == "app1" {
  687. return true
  688. }
  689. return false
  690. }
  691. end := time.Now().UTC().Truncate(day)
  692. start := end.Add(-day)
  693. // Tests:
  694. // 1 Single-aggregation
  695. // 1a AggregationProperties=(Cluster)
  696. as = generateAllocationSet(start)
  697. err = as.AggregateBy(Properties{ClusterProp: ""}, nil)
  698. assertAllocationSetTotals(t, as, "1a", err, numClusters+numIdle, activeTotalCost+idleTotalCost)
  699. assertAllocationTotals(t, as, "1a", map[string]float64{
  700. "cluster1": 40.00,
  701. "cluster2": 30.00,
  702. IdleSuffix: 30.00,
  703. })
  704. assertAllocationWindow(t, as, "1a", startYesterday, endYesterday, 1440.0)
  705. // 1b AggregationProperties=(Namespace)
  706. as = generateAllocationSet(start)
  707. err = as.AggregateBy(Properties{NamespaceProp: true}, nil)
  708. assertAllocationSetTotals(t, as, "1b", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost)
  709. assertAllocationTotals(t, as, "1b", map[string]float64{
  710. "namespace1": 25.00,
  711. "namespace2": 30.00,
  712. "namespace3": 15.00,
  713. IdleSuffix: 30.00,
  714. })
  715. assertAllocationWindow(t, as, "1b", startYesterday, endYesterday, 1440.0)
  716. // 1c AggregationProperties=(Pod)
  717. as = generateAllocationSet(start)
  718. err = as.AggregateBy(Properties{PodProp: true}, nil)
  719. assertAllocationSetTotals(t, as, "1c", err, numPods+numIdle, activeTotalCost+idleTotalCost)
  720. assertAllocationTotals(t, as, "1c", map[string]float64{
  721. "pod-jkl": 5.00,
  722. "pod-stu": 5.00,
  723. "pod-abc": 5.00,
  724. "pod-pqr": 5.00,
  725. "pod-def": 5.00,
  726. "pod-vwx": 10.00,
  727. "pod1": 15.00,
  728. "pod-mno": 10.00,
  729. "pod-ghi": 10.00,
  730. IdleSuffix: 30.00,
  731. })
  732. assertAllocationWindow(t, as, "1c", startYesterday, endYesterday, 1440.0)
  733. // 1d AggregationProperties=(Container)
  734. as = generateAllocationSet(start)
  735. err = as.AggregateBy(Properties{ContainerProp: true}, nil)
  736. assertAllocationSetTotals(t, as, "1d", err, numContainers+numIdle, activeTotalCost+idleTotalCost)
  737. assertAllocationTotals(t, as, "1d", map[string]float64{
  738. "container2": 5.00,
  739. "container9": 5.00,
  740. "container6": 10.00,
  741. "container3": 5.00,
  742. "container4": 10.00,
  743. "container7": 5.00,
  744. "container8": 5.00,
  745. "container5": 10.00,
  746. "container1": 15.00,
  747. IdleSuffix: 30.00,
  748. })
  749. assertAllocationWindow(t, as, "1d", startYesterday, endYesterday, 1440.0)
  750. // 1e AggregationProperties=(ControllerKind)
  751. as = generateAllocationSet(start)
  752. err = as.AggregateBy(Properties{ControllerKindProp: true}, nil)
  753. assertAllocationSetTotals(t, as, "1e", err, numControllerKinds+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
  754. assertAllocationTotals(t, as, "1e", map[string]float64{
  755. "daemonset": 10.00,
  756. "deployment": 35.00,
  757. "statefulset": 10.00,
  758. IdleSuffix: 30.00,
  759. UnallocatedSuffix: 15.00,
  760. })
  761. assertAllocationWindow(t, as, "1e", startYesterday, endYesterday, 1440.0)
  762. // 1f AggregationProperties=(Controller)
  763. as = generateAllocationSet(start)
  764. err = as.AggregateBy(Properties{ControllerProp: true}, nil)
  765. assertAllocationSetTotals(t, as, "1f", err, numControllers+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
  766. assertAllocationTotals(t, as, "1f", map[string]float64{
  767. "deployment/deployment2": 20.00,
  768. "daemonset/daemonset1": 10.00,
  769. "deployment/deployment3": 5.00,
  770. "statefulset/statefulset1": 10.00,
  771. "deployment/deployment1": 10.00,
  772. IdleSuffix: 30.00,
  773. UnallocatedSuffix: 15.00,
  774. })
  775. assertAllocationWindow(t, as, "1f", startYesterday, endYesterday, 1440.0)
  776. // 1g AggregationProperties=(Service)
  777. as = generateAllocationSet(start)
  778. err = as.AggregateBy(Properties{ServiceProp: true}, nil)
  779. assertAllocationSetTotals(t, as, "1g", err, numServices+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
  780. assertAllocationTotals(t, as, "1g", map[string]float64{
  781. "service1": 10.00,
  782. IdleSuffix: 30.00,
  783. UnallocatedSuffix: 60.00,
  784. })
  785. assertAllocationWindow(t, as, "1g", startYesterday, endYesterday, 1440.0)
  786. // 1h AggregationProperties=(Label:app)
  787. as = generateAllocationSet(start)
  788. err = as.AggregateBy(Properties{LabelProp: map[string]string{"app": ""}}, nil)
  789. assertAllocationSetTotals(t, as, "1h", err, numLabelApps+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
  790. assertAllocationTotals(t, as, "1h", map[string]float64{
  791. "app=app1": 15.00,
  792. "app=app2": 20.00,
  793. IdleSuffix: 30.00,
  794. UnallocatedSuffix: 35.00,
  795. })
  796. assertAllocationWindow(t, as, "1h", startYesterday, endYesterday, 1440.0)
  797. // 1i AggregationProperties=(ControllerKind:deployment)
  798. as = generateAllocationSet(start)
  799. err = as.AggregateBy(Properties{ControllerKindProp: "deployment"}, nil)
  800. assertAllocationSetTotals(t, as, "1i", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
  801. assertAllocationTotals(t, as, "1i", map[string]float64{
  802. "deployment": 35.00,
  803. IdleSuffix: 30.00,
  804. UnallocatedSuffix: 35.00,
  805. })
  806. assertAllocationWindow(t, as, "1i", startYesterday, endYesterday, 1440.0)
  807. // 1j AggregationProperties=(Annotation:team)
  808. as = generateAllocationSet(start)
  809. err = as.AggregateBy(Properties{AnnotationProp: map[string]string{"team": ""}}, nil)
  810. assertAllocationSetTotals(t, as, "1j", err, 2+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
  811. assertAllocationTotals(t, as, "1j", map[string]float64{
  812. "team=team1": 10.00,
  813. "team=team2": 5.00,
  814. IdleSuffix: 30.00,
  815. UnallocatedSuffix: 55.00,
  816. })
  817. assertAllocationWindow(t, as, "1i", startYesterday, endYesterday, 1440.0)
  818. // 2 Multi-aggregation
  819. // 2a AggregationProperties=(Cluster, Namespace)
  820. // 2b AggregationProperties=(Namespace, Label:app)
  821. // 2c AggregationProperties=(Cluster, Namespace, Pod, Container)
  822. // 2d AggregationProperties=(Label:app, Label:environment)
  823. as = generateAllocationSet(start)
  824. err = as.AggregateBy(Properties{LabelProp: map[string]string{"app": "", "env": ""}}, nil)
  825. // sets should be {idle, unallocated, app1/env1, app2/env2, app2/unallocated}
  826. assertAllocationSetTotals(t, as, "2d", err, numIdle+numUnallocated+3, activeTotalCost+idleTotalCost)
  827. assertAllocationTotals(t, as, "2d", map[string]float64{
  828. "app=app1/env=env1": 15.00,
  829. "app=app2/env=env2": 10.00,
  830. "app=app2/" + UnallocatedSuffix: 10.00,
  831. IdleSuffix: 30.00,
  832. UnallocatedSuffix: 35.00,
  833. })
  834. // 2e AggregationProperties=(Cluster, Label:app, Label:environment)
  835. as = generateAllocationSet(start)
  836. err = as.AggregateBy(Properties{ClusterProp: "", LabelProp: map[string]string{"app": "", "env": ""}}, nil)
  837. assertAllocationSetTotals(t, as, "2e", err, 6, activeTotalCost+idleTotalCost)
  838. assertAllocationTotals(t, as, "2e", map[string]float64{
  839. "cluster1/app=app2/env=env2": 10.00,
  840. "__idle__": 30.00,
  841. "cluster1/app=app1/env=env1": 15.00,
  842. "cluster1/" + UnallocatedSuffix: 15.00,
  843. "cluster2/app=app2/" + UnallocatedSuffix: 10.00,
  844. "cluster2/" + UnallocatedSuffix: 20.00,
  845. })
  846. // 2f AggregationProperties=(annotation:team, pod)
  847. as = generateAllocationSet(start)
  848. err = as.AggregateBy(Properties{AnnotationProp: map[string]string{"team": ""}, PodProp: ""}, nil)
  849. assertAllocationSetTotals(t, as, "2e", err, 11, activeTotalCost+idleTotalCost)
  850. assertAllocationTotals(t, as, "2e", map[string]float64{
  851. "pod-jkl/" + UnallocatedSuffix: 5.00,
  852. "pod-stu/team=team1": 5.00,
  853. "pod-abc/" + UnallocatedSuffix: 5.00,
  854. "pod-pqr/" + UnallocatedSuffix: 5.00,
  855. "pod-def/" + UnallocatedSuffix: 5.00,
  856. "pod-vwx/team=team1": 5.00,
  857. "pod-vwx/team=team2": 5.00,
  858. "pod1/" + UnallocatedSuffix: 15.00,
  859. "pod-mno/" + UnallocatedSuffix: 10.00,
  860. "pod-ghi/" + UnallocatedSuffix: 10.00,
  861. IdleSuffix: 30.00,
  862. })
  863. // // TODO niko/etl
  864. // // 3 Share idle
  865. // 3a AggregationProperties=(Namespace) ShareIdle=ShareWeighted
  866. // namespace1: 39.6875 = 25.00 + 5.00*(3.00/6.00) + 15.0*(13.0/16.0)
  867. // namespace2: 40.3125 = 30.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
  868. // namespace3: 20.0000 = 15.00 + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
  869. as = generateAllocationSet(start)
  870. err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{ShareIdle: ShareWeighted})
  871. assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
  872. assertAllocationTotals(t, as, "3a", map[string]float64{
  873. "namespace1": 39.69,
  874. "namespace2": 40.31,
  875. "namespace3": 20.00,
  876. })
  877. assertAllocationWindow(t, as, "3a", startYesterday, endYesterday, 1440.0)
  878. // 3b AggregationProperties=(Namespace) ShareIdle=ShareEven
  879. // namespace1: 35.0000 = 25.00 + 5.00*(1.0/2.0) + 15.0*(1.0/2.0)
  880. // namespace2: 45.0000 = 30.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
  881. // namespace3: 20.0000 = 15.00 + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
  882. as = generateAllocationSet(start)
  883. err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{ShareIdle: ShareEven})
  884. assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
  885. assertAllocationTotals(t, as, "3a", map[string]float64{
  886. "namespace1": 35.00,
  887. "namespace2": 45.00,
  888. "namespace3": 20.00,
  889. })
  890. assertAllocationWindow(t, as, "3b", startYesterday, endYesterday, 1440.0)
  891. // 4 Share resources
  892. // 4a Share namespace ShareEven
  893. // namespace1: 32.5000 = 25.00 + 15.00*(1.0/2.0)
  894. // namespace2: 37.5000 = 30.00 + 15.00*(1.0/2.0)
  895. // idle: 30.0000
  896. as = generateAllocationSet(start)
  897. err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
  898. ShareFuncs: []AllocationMatchFunc{isNamespace3},
  899. ShareSplit: ShareEven,
  900. })
  901. assertAllocationSetTotals(t, as, "4a", err, numNamespaces, activeTotalCost+idleTotalCost)
  902. assertAllocationTotals(t, as, "4a", map[string]float64{
  903. "namespace1": 32.50,
  904. "namespace2": 37.50,
  905. IdleSuffix: 30.00,
  906. })
  907. assertAllocationWindow(t, as, "4a", startYesterday, endYesterday, 1440.0)
  908. // 4b Share namespace ShareWeighted
  909. // namespace1: 32.5000 =
  910. // namespace2: 37.5000 =
  911. // idle: 30.0000
  912. as = generateAllocationSet(start)
  913. err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
  914. ShareFuncs: []AllocationMatchFunc{isNamespace3},
  915. ShareSplit: ShareWeighted,
  916. })
  917. assertAllocationSetTotals(t, as, "4b", err, numNamespaces, activeTotalCost+idleTotalCost)
  918. assertAllocationTotals(t, as, "4b", map[string]float64{
  919. "namespace1": 31.82,
  920. "namespace2": 38.18,
  921. IdleSuffix: 30.00,
  922. })
  923. assertAllocationWindow(t, as, "4b", startYesterday, endYesterday, 1440.0)
  924. // 4c Share label ShareEven
  925. // namespace1: 15.0000 = 25.00 - 15.00 + 15.00*(1.0/3.0)
  926. // namespace2: 35.0000 = 30.00 + 15.00*(1.0/3.0)
  927. // namespace3: 20.0000 = 15.00 + 15.00*(1.0/3.0)
  928. // idle: 30.0000
  929. as = generateAllocationSet(start)
  930. err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
  931. ShareFuncs: []AllocationMatchFunc{isApp1},
  932. ShareSplit: ShareEven,
  933. })
  934. assertAllocationSetTotals(t, as, "4c", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost)
  935. assertAllocationTotals(t, as, "4c", map[string]float64{
  936. "namespace1": 15.00,
  937. "namespace2": 35.00,
  938. "namespace3": 20.00,
  939. IdleSuffix: 30.00,
  940. })
  941. assertAllocationWindow(t, as, "4c", startYesterday, endYesterday, 1440.0)
  942. // 4d Share overhead ShareWeighted
  943. // namespace1: 37.5000 = 25.00 + (7.0*24.0)*(25.00/70.00)
  944. // namespace2: 45.0000 = 30.00 + (7.0*24.0)*(30.00/70.00)
  945. // namespace3: 22.5000 = 15.00 + (7.0*24.0)*(15.00/70.00)
  946. // idle: 30.0000
  947. as = generateAllocationSet(start)
  948. err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
  949. SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
  950. ShareSplit: ShareWeighted,
  951. })
  952. assertAllocationSetTotals(t, as, "4d", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost+(sharedOverheadHourlyCost*24.0))
  953. assertAllocationTotals(t, as, "4d", map[string]float64{
  954. "namespace1": 85.00,
  955. "namespace2": 102.00,
  956. "namespace3": 51.00,
  957. IdleSuffix: 30.00,
  958. })
  959. assertAllocationWindow(t, as, "4d", startYesterday, endYesterday, 1440.0)
  960. // 5 Filters
  961. isCluster := func(matchCluster string) func(*Allocation) bool {
  962. return func(a *Allocation) bool {
  963. cluster, err := a.Properties.GetCluster()
  964. return err == nil && cluster == matchCluster
  965. }
  966. }
  967. isNamespace := func(matchNamespace string) func(*Allocation) bool {
  968. return func(a *Allocation) bool {
  969. namespace, err := a.Properties.GetNamespace()
  970. return err == nil && namespace == matchNamespace
  971. }
  972. }
  973. // 5a Filter by cluster with separate idle
  974. as = generateAllocationSet(start)
  975. err = as.AggregateBy(Properties{ClusterProp: ""}, &AllocationAggregationOptions{
  976. FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
  977. ShareIdle: ShareNone,
  978. })
  979. assertAllocationSetTotals(t, as, "5a", err, 2, 60.0)
  980. assertAllocationTotals(t, as, "5a", map[string]float64{
  981. "cluster1": 40.00,
  982. IdleSuffix: 20.00,
  983. })
  984. assertAllocationWindow(t, as, "5a", startYesterday, endYesterday, 1440.0)
  985. // 5b Filter by cluster with shared idle
  986. as = generateAllocationSet(start)
  987. err = as.AggregateBy(Properties{ClusterProp: ""}, &AllocationAggregationOptions{
  988. FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
  989. ShareIdle: ShareWeighted,
  990. })
  991. assertAllocationSetTotals(t, as, "5b", err, 1, 60.0)
  992. assertAllocationTotals(t, as, "5b", map[string]float64{
  993. "cluster1": 60.00,
  994. })
  995. assertAllocationWindow(t, as, "5b", startYesterday, endYesterday, 1440.0)
  996. // 5c Filter by cluster, agg by namespace, with separate idle
  997. as = generateAllocationSet(start)
  998. err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
  999. FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
  1000. ShareIdle: ShareNone,
  1001. })
  1002. assertAllocationSetTotals(t, as, "5c", err, 3, 60.0)
  1003. assertAllocationTotals(t, as, "5c", map[string]float64{
  1004. "namespace1": 25.00,
  1005. "namespace2": 15.00,
  1006. IdleSuffix: 20.00,
  1007. })
  1008. assertAllocationWindow(t, as, "5c", startYesterday, endYesterday, 1440.0)
  1009. // 5d Filter by namespace, agg by cluster, with separate idle
  1010. as = generateAllocationSet(start)
  1011. err = as.AggregateBy(Properties{ClusterProp: ""}, &AllocationAggregationOptions{
  1012. FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
  1013. ShareIdle: ShareNone,
  1014. })
  1015. assertAllocationSetTotals(t, as, "5d", err, 3, 40.31)
  1016. assertAllocationTotals(t, as, "5d", map[string]float64{
  1017. "cluster1": 15.00,
  1018. "cluster2": 15.00,
  1019. IdleSuffix: 10.31,
  1020. })
  1021. assertAllocationWindow(t, as, "5d", startYesterday, endYesterday, 1440.0)
  1022. // 6 Combinations and options
  1023. // 6a SplitIdle
  1024. as = generateAllocationSet(start)
  1025. err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{SplitIdle: true})
  1026. assertAllocationSetTotals(t, as, "6a", err, numNamespaces+numSplitIdle, activeTotalCost+idleTotalCost)
  1027. assertAllocationTotals(t, as, "6a", map[string]float64{
  1028. "namespace1": 25.00,
  1029. "namespace2": 30.00,
  1030. "namespace3": 15.00,
  1031. fmt.Sprintf("cluster1/%s", IdleSuffix): 20.00,
  1032. fmt.Sprintf("cluster2/%s", IdleSuffix): 10.00,
  1033. })
  1034. assertAllocationWindow(t, as, "6a", startYesterday, endYesterday, 1440.0)
  1035. // 6b Share idle weighted with filters
  1036. // Should match values from unfiltered aggregation
  1037. // as = generateAllocationSet(start)
  1038. // err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{ShareIdle: ShareWeighted})
  1039. // printAllocationSet("6b unfiltered", as)
  1040. as = generateAllocationSet(start)
  1041. err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
  1042. FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
  1043. ShareIdle: ShareWeighted,
  1044. })
  1045. assertAllocationSetTotals(t, as, "6b", err, 1, 40.31)
  1046. assertAllocationTotals(t, as, "6b", map[string]float64{
  1047. "namespace2": 40.31,
  1048. })
  1049. assertAllocationWindow(t, as, "6b", startYesterday, endYesterday, 1440.0)
  1050. // 6c Share idle even with filters
  1051. // Should match values from unfiltered aggregation
  1052. // as = generateAllocationSet(start)
  1053. // err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{ShareIdle: ShareEven})
  1054. // printAllocationSet("6c unfiltered", as)
  1055. as = generateAllocationSet(start)
  1056. err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
  1057. FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
  1058. ShareIdle: ShareEven,
  1059. })
  1060. assertAllocationSetTotals(t, as, "6b", err, 1, 45.00)
  1061. assertAllocationTotals(t, as, "6b", map[string]float64{
  1062. "namespace2": 45.00,
  1063. })
  1064. assertAllocationWindow(t, as, "6b", startYesterday, endYesterday, 1440.0)
  1065. // 6d Share resources with filters
  1066. // 6e Share idle and share resources
  1067. // 7 Edge cases and errors
  1068. // 7a Empty AggregationProperties
  1069. // 7b Filter all
  1070. // 7c Share all
  1071. // 7d Share and filter the same allocations
  1072. }
  1073. // TODO niko/etl
  1074. //func TestAllocationSet_Clone(t *testing.T) {}
  1075. func TestAllocationSet_ComputeIdleAllocations(t *testing.T) {
  1076. var as *AllocationSet
  1077. var err error
  1078. var idles map[string]*Allocation
  1079. end := time.Now().UTC().Truncate(day)
  1080. start := end.Add(-day)
  1081. // Generate AllocationSet and strip out any existing idle allocations
  1082. as = generateAllocationSet(start)
  1083. for key := range as.idleKeys {
  1084. as.Delete(key)
  1085. }
  1086. // Create an AssetSet representing cluster costs for two clusters (cluster1
  1087. // and cluster2). Include Nodes and Disks for both, even though only
  1088. // Nodes will be counted. Whereas in practice, Assets should be aggregated
  1089. // by type, here we will provide multiple Nodes for one of the clusters to
  1090. // make sure the function still holds.
  1091. // NOTE: we're re-using generateAllocationSet so this has to line up with
  1092. // the allocated node costs from that function. See table above.
  1093. // | Hierarchy | Cost | CPU | RAM | GPU | Adjustment |
  1094. // +-----------------------------------------+------+------+------+------+------------+
  1095. // cluster1:
  1096. // nodes 100.00 55.00 44.00 11.00 -10.00
  1097. // +-----------------------------------------+------+------+------+------+------------+
  1098. // cluster1 subtotal (adjusted) 100.00 50.00 40.00 10.00 0.00
  1099. // +-----------------------------------------+------+------+------+------+------------+
  1100. // cluster1 allocated 48.00 6.00 16.00 6.00 0.00
  1101. // +-----------------------------------------+------+------+------+------+------------+
  1102. // cluster1 idle 72.00 44.00 24.00 4.00 0.00
  1103. // +-----------------------------------------+------+------+------+------+------------+
  1104. // cluster2:
  1105. // node1 35.00 20.00 15.00 0.00 0.00
  1106. // node2 35.00 20.00 15.00 0.00 0.00
  1107. // node3 30.00 10.00 10.00 10.00 0.00
  1108. // (disks should not matter for idle)
  1109. // +-----------------------------------------+------+------+------+------+------------+
  1110. // cluster2 subtotal 100.00 50.00 40.00 10.00 0.00
  1111. // +-----------------------------------------+------+------+------+------+------------+
  1112. // cluster2 allocated 28.00 6.00 6.00 6.00 0.00
  1113. // +-----------------------------------------+------+------+------+------+------------+
  1114. // cluster2 idle 82.00 44.00 34.00 4.00 0.00
  1115. // +-----------------------------------------+------+------+------+------+------------+
  1116. cluster1Nodes := NewNode("", "cluster1", "", start, end, NewWindow(&start, &end))
  1117. cluster1Nodes.CPUCost = 55.0
  1118. cluster1Nodes.RAMCost = 44.0
  1119. cluster1Nodes.GPUCost = 11.0
  1120. cluster1Nodes.adjustment = -10.00
  1121. cluster2Node1 := NewNode("node1", "cluster2", "node1", start, end, NewWindow(&start, &end))
  1122. cluster2Node1.CPUCost = 20.0
  1123. cluster2Node1.RAMCost = 15.0
  1124. cluster2Node1.GPUCost = 0.0
  1125. cluster2Node2 := NewNode("node2", "cluster2", "node2", start, end, NewWindow(&start, &end))
  1126. cluster2Node2.CPUCost = 20.0
  1127. cluster2Node2.RAMCost = 15.0
  1128. cluster2Node2.GPUCost = 0.0
  1129. cluster2Node3 := NewNode("node3", "cluster2", "node3", start, end, NewWindow(&start, &end))
  1130. cluster2Node3.CPUCost = 10.0
  1131. cluster2Node3.RAMCost = 10.0
  1132. cluster2Node3.GPUCost = 10.0
  1133. cluster2Disk1 := NewDisk("disk1", "cluster2", "disk1", start, end, NewWindow(&start, &end))
  1134. cluster2Disk1.Cost = 5.0
  1135. assetSet := NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1)
  1136. idles, err = as.ComputeIdleAllocations(assetSet)
  1137. if err != nil {
  1138. t.Fatalf("unexpected error: %s", err)
  1139. }
  1140. if len(idles) != 2 {
  1141. t.Fatalf("idles: expected length %d; got length %d", 2, len(idles))
  1142. }
  1143. if idle, ok := idles["cluster1"]; !ok {
  1144. t.Fatalf("expected idle cost for %s", "cluster1")
  1145. } else {
  1146. if !util.IsApproximately(idle.TotalCost(), 72.0) {
  1147. t.Fatalf("%s idle: expected total cost %f; got total cost %f", "cluster1", 72.0, idle.TotalCost())
  1148. }
  1149. }
  1150. if !util.IsApproximately(idles["cluster1"].CPUCost, 44.0) {
  1151. t.Fatalf("expected idle CPU cost for %s to be %.2f; got %.2f", "cluster1", 44.0, idles["cluster1"].CPUCost)
  1152. }
  1153. if !util.IsApproximately(idles["cluster1"].RAMCost, 24.0) {
  1154. t.Fatalf("expected idle RAM cost for %s to be %.2f; got %.2f", "cluster1", 24.0, idles["cluster1"].RAMCost)
  1155. }
  1156. if !util.IsApproximately(idles["cluster1"].GPUCost, 4.0) {
  1157. t.Fatalf("expected idle GPU cost for %s to be %.2f; got %.2f", "cluster1", 4.0, idles["cluster1"].GPUCost)
  1158. }
  1159. if idle, ok := idles["cluster2"]; !ok {
  1160. t.Fatalf("expected idle cost for %s", "cluster2")
  1161. } else {
  1162. if !util.IsApproximately(idle.TotalCost(), 82.0) {
  1163. t.Fatalf("%s idle: expected total cost %f; got total cost %f", "cluster2", 82.0, idle.TotalCost())
  1164. }
  1165. }
  1166. // NOTE: we're re-using generateAllocationSet so this has to line up with
  1167. // the allocated node costs from that function. See table above.
  1168. // | Hierarchy | Cost | CPU | RAM | GPU | Adjustment |
  1169. // +-----------------------------------------+------+------+------+------+------------+
  1170. // cluster1:
  1171. // nodes 100.00 5.00 4.00 1.00 90.00
  1172. // +-----------------------------------------+------+------+------+------+------------+
  1173. // cluster1 subtotal (adjusted) 100.00 50.00 40.00 10.00 0.00
  1174. // +-----------------------------------------+------+------+------+------+------------+
  1175. // cluster1 allocated 48.00 6.00 16.00 6.00 0.00
  1176. // +-----------------------------------------+------+------+------+------+------------+
  1177. // cluster1 idle 72.00 44.00 24.00 4.00 0.00
  1178. // +-----------------------------------------+------+------+------+------+------------+
  1179. // cluster2:
  1180. // node1 35.00 20.00 15.00 0.00 0.00
  1181. // node2 35.00 20.00 15.00 0.00 0.00
  1182. // node3 30.00 10.00 10.00 10.00 0.00
  1183. // (disks should not matter for idle)
  1184. // +-----------------------------------------+------+------+------+------+------------+
  1185. // cluster2 subtotal 100.00 50.00 40.00 10.00 0.00
  1186. // +-----------------------------------------+------+------+------+------+------------+
  1187. // cluster2 allocated 28.00 6.00 6.00 6.00 0.00
  1188. // +-----------------------------------------+------+------+------+------+------------+
  1189. // cluster2 idle 82.00 44.00 34.00 4.00 0.00
  1190. // +-----------------------------------------+------+------+------+------+------------+
  1191. cluster1Nodes = NewNode("", "cluster1", "", start, end, NewWindow(&start, &end))
  1192. cluster1Nodes.CPUCost = 5.0
  1193. cluster1Nodes.RAMCost = 4.0
  1194. cluster1Nodes.GPUCost = 1.0
  1195. cluster1Nodes.adjustment = 90.00
  1196. cluster2Node1 = NewNode("node1", "cluster2", "node1", start, end, NewWindow(&start, &end))
  1197. cluster2Node1.CPUCost = 20.0
  1198. cluster2Node1.RAMCost = 15.0
  1199. cluster2Node1.GPUCost = 0.0
  1200. cluster2Node2 = NewNode("node2", "cluster2", "node2", start, end, NewWindow(&start, &end))
  1201. cluster2Node2.CPUCost = 20.0
  1202. cluster2Node2.RAMCost = 15.0
  1203. cluster2Node2.GPUCost = 0.0
  1204. cluster2Node3 = NewNode("node3", "cluster2", "node3", start, end, NewWindow(&start, &end))
  1205. cluster2Node3.CPUCost = 10.0
  1206. cluster2Node3.RAMCost = 10.0
  1207. cluster2Node3.GPUCost = 10.0
  1208. cluster2Disk1 = NewDisk("disk1", "cluster2", "disk1", start, end, NewWindow(&start, &end))
  1209. cluster2Disk1.Cost = 5.0
  1210. assetSet = NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1)
  1211. idles, err = as.ComputeIdleAllocations(assetSet)
  1212. if err != nil {
  1213. t.Fatalf("unexpected error: %s", err)
  1214. }
  1215. if len(idles) != 2 {
  1216. t.Fatalf("idles: expected length %d; got length %d", 2, len(idles))
  1217. }
  1218. if idle, ok := idles["cluster1"]; !ok {
  1219. t.Fatalf("expected idle cost for %s", "cluster1")
  1220. } else {
  1221. if !util.IsApproximately(idle.TotalCost(), 72.0) {
  1222. t.Fatalf("%s idle: expected total cost %f; got total cost %f", "cluster1", 72.0, idle.TotalCost())
  1223. }
  1224. }
  1225. if !util.IsApproximately(idles["cluster1"].CPUCost, 44.0) {
  1226. t.Fatalf("expected idle CPU cost for %s to be %.2f; got %.2f", "cluster1", 44.0, idles["cluster1"].CPUCost)
  1227. }
  1228. if !util.IsApproximately(idles["cluster1"].RAMCost, 24.0) {
  1229. t.Fatalf("expected idle RAM cost for %s to be %.2f; got %.2f", "cluster1", 24.0, idles["cluster1"].RAMCost)
  1230. }
  1231. if !util.IsApproximately(idles["cluster1"].GPUCost, 4.0) {
  1232. t.Fatalf("expected idle GPU cost for %s to be %.2f; got %.2f", "cluster1", 4.0, idles["cluster1"].GPUCost)
  1233. }
  1234. if idle, ok := idles["cluster2"]; !ok {
  1235. t.Fatalf("expected idle cost for %s", "cluster2")
  1236. } else {
  1237. if !util.IsApproximately(idle.TotalCost(), 82.0) {
  1238. t.Fatalf("%s idle: expected total cost %f; got total cost %f", "cluster2", 82.0, idle.TotalCost())
  1239. }
  1240. }
  1241. // TODO assert value of each resource cost precisely
  1242. }
  1243. // TODO niko/etl
  1244. //func TestAllocationSet_Delete(t *testing.T) {}
  1245. // TODO niko/etl
  1246. //func TestAllocationSet_End(t *testing.T) {}
  1247. // TODO niko/etl
  1248. //func TestAllocationSet_IdleAllocations(t *testing.T) {}
  1249. // TODO niko/etl
  1250. //func TestAllocationSet_Insert(t *testing.T) {}
  1251. // TODO niko/etl
  1252. //func TestAllocationSet_IsEmpty(t *testing.T) {}
  1253. // TODO niko/etl
  1254. //func TestAllocationSet_Length(t *testing.T) {}
  1255. // TODO niko/etl
  1256. //func TestAllocationSet_Map(t *testing.T) {}
  1257. // TODO niko/etl
  1258. //func TestAllocationSet_MarshalJSON(t *testing.T) {}
  1259. // TODO niko/etl
  1260. //func TestAllocationSet_Resolution(t *testing.T) {}
  1261. // TODO niko/etl
  1262. //func TestAllocationSet_Seconds(t *testing.T) {}
  1263. // TODO niko/etl
  1264. //func TestAllocationSet_Set(t *testing.T) {}
  1265. // TODO niko/etl
  1266. //func TestAllocationSet_Start(t *testing.T) {}
  1267. // TODO niko/etl
  1268. //func TestAllocationSet_TotalCost(t *testing.T) {}
  1269. // TODO niko/etl
  1270. //func TestNewAllocationSetRange(t *testing.T) {}
  1271. func TestAllocationSetRange_Accumulate(t *testing.T) {
  1272. ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
  1273. yesterday := time.Now().UTC().Truncate(day).Add(-day)
  1274. today := time.Now().UTC().Truncate(day)
  1275. tomorrow := time.Now().UTC().Truncate(day).Add(day)
  1276. // Accumulating any combination of nil and/or empty set should result in empty set
  1277. result, err := NewAllocationSetRange(nil).Accumulate()
  1278. if err != nil {
  1279. t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
  1280. }
  1281. if !result.IsEmpty() {
  1282. t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
  1283. }
  1284. result, err = NewAllocationSetRange(nil, nil).Accumulate()
  1285. if err != nil {
  1286. t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
  1287. }
  1288. if !result.IsEmpty() {
  1289. t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
  1290. }
  1291. result, err = NewAllocationSetRange(NewAllocationSet(yesterday, today)).Accumulate()
  1292. if err != nil {
  1293. t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
  1294. }
  1295. if !result.IsEmpty() {
  1296. t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
  1297. }
  1298. result, err = NewAllocationSetRange(nil, NewAllocationSet(ago2d, yesterday), nil, NewAllocationSet(today, tomorrow), nil).Accumulate()
  1299. if err != nil {
  1300. t.Fatalf("unexpected error accumulating nil AllocationSetRange: %s", err)
  1301. }
  1302. if !result.IsEmpty() {
  1303. t.Fatalf("accumulating nil AllocationSetRange: expected empty; actual %s", result)
  1304. }
  1305. todayAS := NewAllocationSet(today, tomorrow)
  1306. todayAS.Set(NewUnitAllocation("", today, day, nil))
  1307. yesterdayAS := NewAllocationSet(yesterday, today)
  1308. yesterdayAS.Set(NewUnitAllocation("", yesterday, day, nil))
  1309. // Accumulate non-nil with nil should result in copy of non-nil, regardless of order
  1310. result, err = NewAllocationSetRange(nil, todayAS).Accumulate()
  1311. if err != nil {
  1312. t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
  1313. }
  1314. if result == nil {
  1315. t.Fatalf("accumulating AllocationSetRange: expected AllocationSet; actual %s", result)
  1316. }
  1317. if result.TotalCost() != 5.0 {
  1318. t.Fatalf("accumulating AllocationSetRange: expected total cost 5.0; actual %f", result.TotalCost())
  1319. }
  1320. result, err = NewAllocationSetRange(todayAS, nil).Accumulate()
  1321. if err != nil {
  1322. t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
  1323. }
  1324. if result == nil {
  1325. t.Fatalf("accumulating AllocationSetRange: expected AllocationSet; actual %s", result)
  1326. }
  1327. if result.TotalCost() != 5.0 {
  1328. t.Fatalf("accumulating AllocationSetRange: expected total cost 5.0; actual %f", result.TotalCost())
  1329. }
  1330. result, err = NewAllocationSetRange(nil, todayAS, nil).Accumulate()
  1331. if err != nil {
  1332. t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
  1333. }
  1334. if result == nil {
  1335. t.Fatalf("accumulating AllocationSetRange: expected AllocationSet; actual %s", result)
  1336. }
  1337. if result.TotalCost() != 5.0 {
  1338. t.Fatalf("accumulating AllocationSetRange: expected total cost 5.0; actual %f", result.TotalCost())
  1339. }
  1340. // Accumulate two non-nil should result in sum of both with appropriate start, end
  1341. result, err = NewAllocationSetRange(yesterdayAS, todayAS).Accumulate()
  1342. if err != nil {
  1343. t.Fatalf("unexpected error accumulating AllocationSetRange of length 1: %s", err)
  1344. }
  1345. if result == nil {
  1346. t.Fatalf("accumulating AllocationSetRange: expected AllocationSet; actual %s", result)
  1347. }
  1348. if result.TotalCost() != 10.0 {
  1349. t.Fatalf("accumulating AllocationSetRange: expected total cost 10.0; actual %f", result.TotalCost())
  1350. }
  1351. allocMap := result.Map()
  1352. if len(allocMap) != 1 {
  1353. t.Fatalf("accumulating AllocationSetRange: expected length 1; actual length %d", len(allocMap))
  1354. }
  1355. alloc := allocMap["cluster1/namespace1/pod1/container1"]
  1356. if alloc == nil {
  1357. t.Fatalf("accumulating AllocationSetRange: expected allocation 'cluster1/namespace1/pod1/container1'")
  1358. }
  1359. if alloc.CPUCoreHours != 2.0 {
  1360. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", result.TotalCost())
  1361. }
  1362. if alloc.CPUCost != 2.0 {
  1363. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.CPUCost)
  1364. }
  1365. if alloc.CPUEfficiency() != 1.0 {
  1366. t.Fatalf("accumulating AllocationSetRange: expected 1.0; actual %f", alloc.CPUEfficiency())
  1367. }
  1368. if alloc.GPUHours != 2.0 {
  1369. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.GPUHours)
  1370. }
  1371. if alloc.GPUCost != 2.0 {
  1372. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.GPUCost)
  1373. }
  1374. if alloc.NetworkCost != 2.0 {
  1375. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.NetworkCost)
  1376. }
  1377. if alloc.PVByteHours != 2.0 {
  1378. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.PVByteHours)
  1379. }
  1380. if alloc.PVCost != 2.0 {
  1381. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.PVCost)
  1382. }
  1383. if alloc.RAMByteHours != 2.0 {
  1384. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.RAMByteHours)
  1385. }
  1386. if alloc.RAMCost != 2.0 {
  1387. t.Fatalf("accumulating AllocationSetRange: expected 2.0; actual %f", alloc.RAMCost)
  1388. }
  1389. if alloc.RAMEfficiency() != 1.0 {
  1390. t.Fatalf("accumulating AllocationSetRange: expected 1.0; actual %f", alloc.RAMEfficiency())
  1391. }
  1392. if alloc.TotalCost() != 10.0 {
  1393. t.Fatalf("accumulating AllocationSetRange: expected 10.0; actual %f", alloc.TotalCost())
  1394. }
  1395. if alloc.TotalEfficiency() != 1.0 {
  1396. t.Fatalf("accumulating AllocationSetRange: expected 1.0; actual %f", alloc.TotalEfficiency())
  1397. }
  1398. if !alloc.Start.Equal(yesterday) {
  1399. t.Fatalf("accumulating AllocationSetRange: expected to start %s; actual %s", yesterday, alloc.Start)
  1400. }
  1401. if !alloc.End.Equal(tomorrow) {
  1402. t.Fatalf("accumulating AllocationSetRange: expected to end %s; actual %s", tomorrow, alloc.End)
  1403. }
  1404. if alloc.Minutes() != 2880.0 {
  1405. t.Fatalf("accumulating AllocationSetRange: expected %f minutes; actual %f", 2880.0, alloc.Minutes())
  1406. }
  1407. }
  1408. // TODO niko/etl
  1409. // func TestAllocationSetRange_AccumulateBy(t *testing.T) {}
  1410. // TODO niko/etl
  1411. // func TestAllocationSetRange_AggregateBy(t *testing.T) {}
  1412. // TODO niko/etl
  1413. // func TestAllocationSetRange_Append(t *testing.T) {}
  1414. // TODO niko/etl
  1415. // func TestAllocationSetRange_Each(t *testing.T) {}
  1416. // TODO niko/etl
  1417. // func TestAllocationSetRange_Get(t *testing.T) {}
  1418. func TestAllocationSetRange_InsertRange(t *testing.T) {
  1419. // Set up
  1420. ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
  1421. yesterday := time.Now().UTC().Truncate(day).Add(-day)
  1422. today := time.Now().UTC().Truncate(day)
  1423. tomorrow := time.Now().UTC().Truncate(day).Add(day)
  1424. unit := NewUnitAllocation("", today, day, nil)
  1425. ago2dAS := NewAllocationSet(ago2d, yesterday)
  1426. ago2dAS.Set(NewUnitAllocation("a", ago2d, day, nil))
  1427. ago2dAS.Set(NewUnitAllocation("b", ago2d, day, nil))
  1428. ago2dAS.Set(NewUnitAllocation("c", ago2d, day, nil))
  1429. yesterdayAS := NewAllocationSet(yesterday, today)
  1430. yesterdayAS.Set(NewUnitAllocation("a", yesterday, day, nil))
  1431. yesterdayAS.Set(NewUnitAllocation("b", yesterday, day, nil))
  1432. yesterdayAS.Set(NewUnitAllocation("c", yesterday, day, nil))
  1433. todayAS := NewAllocationSet(today, tomorrow)
  1434. todayAS.Set(NewUnitAllocation("a", today, day, nil))
  1435. todayAS.Set(NewUnitAllocation("b", today, day, nil))
  1436. todayAS.Set(NewUnitAllocation("c", today, day, nil))
  1437. var nilASR *AllocationSetRange
  1438. thisASR := NewAllocationSetRange(yesterdayAS.Clone(), todayAS.Clone())
  1439. thatASR := NewAllocationSetRange(yesterdayAS.Clone())
  1440. longASR := NewAllocationSetRange(ago2dAS.Clone(), yesterdayAS.Clone(), todayAS.Clone())
  1441. var err error
  1442. // Expect an error calling InsertRange on nil
  1443. err = nilASR.InsertRange(thatASR)
  1444. if err == nil {
  1445. t.Fatalf("expected error, got nil")
  1446. }
  1447. // Expect nothing to happen calling InsertRange(nil) on non-nil ASR
  1448. err = thisASR.InsertRange(nil)
  1449. if err != nil {
  1450. t.Fatalf("unexpected error: %s", err)
  1451. }
  1452. thisASR.Each(func(i int, as *AllocationSet) {
  1453. as.Each(func(k string, a *Allocation) {
  1454. if !util.IsApproximately(a.CPUCoreHours, unit.CPUCoreHours) {
  1455. t.Fatalf("allocation %s: expected %f; got %f", k, unit.CPUCoreHours, a.CPUCoreHours)
  1456. }
  1457. if !util.IsApproximately(a.CPUCost, unit.CPUCost) {
  1458. t.Fatalf("allocation %s: expected %f; got %f", k, unit.CPUCost, a.CPUCost)
  1459. }
  1460. if !util.IsApproximately(a.RAMByteHours, unit.RAMByteHours) {
  1461. t.Fatalf("allocation %s: expected %f; got %f", k, unit.RAMByteHours, a.RAMByteHours)
  1462. }
  1463. if !util.IsApproximately(a.RAMCost, unit.RAMCost) {
  1464. t.Fatalf("allocation %s: expected %f; got %f", k, unit.RAMCost, a.RAMCost)
  1465. }
  1466. if !util.IsApproximately(a.GPUHours, unit.GPUHours) {
  1467. t.Fatalf("allocation %s: expected %f; got %f", k, unit.GPUHours, a.GPUHours)
  1468. }
  1469. if !util.IsApproximately(a.GPUCost, unit.GPUCost) {
  1470. t.Fatalf("allocation %s: expected %f; got %f", k, unit.GPUCost, a.GPUCost)
  1471. }
  1472. if !util.IsApproximately(a.PVByteHours, unit.PVByteHours) {
  1473. t.Fatalf("allocation %s: expected %f; got %f", k, unit.PVByteHours, a.PVByteHours)
  1474. }
  1475. if !util.IsApproximately(a.PVCost, unit.PVCost) {
  1476. t.Fatalf("allocation %s: expected %f; got %f", k, unit.PVCost, a.PVCost)
  1477. }
  1478. if !util.IsApproximately(a.NetworkCost, unit.NetworkCost) {
  1479. t.Fatalf("allocation %s: expected %f; got %f", k, unit.NetworkCost, a.NetworkCost)
  1480. }
  1481. if !util.IsApproximately(a.TotalCost(), unit.TotalCost()) {
  1482. t.Fatalf("allocation %s: expected %f; got %f", k, unit.TotalCost(), a.TotalCost())
  1483. }
  1484. })
  1485. })
  1486. // Expect an error calling InsertRange with a range exceeding the receiver
  1487. err = thisASR.InsertRange(longASR)
  1488. if err == nil {
  1489. t.Fatalf("expected error calling InsertRange with a range exceeding the receiver")
  1490. }
  1491. // Expect each Allocation in "today" to stay the same, but "yesterday" to
  1492. // precisely double when inserting a range that only has a duplicate of
  1493. // "yesterday", but no entry for "today"
  1494. err = thisASR.InsertRange(thatASR)
  1495. if err != nil {
  1496. t.Fatalf("unexpected error: %s", err)
  1497. }
  1498. yAS, err := thisASR.Get(0)
  1499. yAS.Each(func(k string, a *Allocation) {
  1500. if !util.IsApproximately(a.CPUCoreHours, 2*unit.CPUCoreHours) {
  1501. t.Fatalf("allocation %s: expected %f; got %f", k, unit.CPUCoreHours, a.CPUCoreHours)
  1502. }
  1503. if !util.IsApproximately(a.CPUCost, 2*unit.CPUCost) {
  1504. t.Fatalf("allocation %s: expected %f; got %f", k, unit.CPUCost, a.CPUCost)
  1505. }
  1506. if !util.IsApproximately(a.RAMByteHours, 2*unit.RAMByteHours) {
  1507. t.Fatalf("allocation %s: expected %f; got %f", k, unit.RAMByteHours, a.RAMByteHours)
  1508. }
  1509. if !util.IsApproximately(a.RAMCost, 2*unit.RAMCost) {
  1510. t.Fatalf("allocation %s: expected %f; got %f", k, unit.RAMCost, a.RAMCost)
  1511. }
  1512. if !util.IsApproximately(a.GPUHours, 2*unit.GPUHours) {
  1513. t.Fatalf("allocation %s: expected %f; got %f", k, unit.GPUHours, a.GPUHours)
  1514. }
  1515. if !util.IsApproximately(a.GPUCost, 2*unit.GPUCost) {
  1516. t.Fatalf("allocation %s: expected %f; got %f", k, unit.GPUCost, a.GPUCost)
  1517. }
  1518. if !util.IsApproximately(a.PVByteHours, 2*unit.PVByteHours) {
  1519. t.Fatalf("allocation %s: expected %f; got %f", k, unit.PVByteHours, a.PVByteHours)
  1520. }
  1521. if !util.IsApproximately(a.PVCost, 2*unit.PVCost) {
  1522. t.Fatalf("allocation %s: expected %f; got %f", k, unit.PVCost, a.PVCost)
  1523. }
  1524. if !util.IsApproximately(a.NetworkCost, 2*unit.NetworkCost) {
  1525. t.Fatalf("allocation %s: expected %f; got %f", k, unit.NetworkCost, a.NetworkCost)
  1526. }
  1527. if !util.IsApproximately(a.TotalCost(), 2*unit.TotalCost()) {
  1528. t.Fatalf("allocation %s: expected %f; got %f", k, unit.TotalCost(), a.TotalCost())
  1529. }
  1530. })
  1531. tAS, err := thisASR.Get(1)
  1532. tAS.Each(func(k string, a *Allocation) {
  1533. if !util.IsApproximately(a.CPUCoreHours, unit.CPUCoreHours) {
  1534. t.Fatalf("allocation %s: expected %f; got %f", k, unit.CPUCoreHours, a.CPUCoreHours)
  1535. }
  1536. if !util.IsApproximately(a.CPUCost, unit.CPUCost) {
  1537. t.Fatalf("allocation %s: expected %f; got %f", k, unit.CPUCost, a.CPUCost)
  1538. }
  1539. if !util.IsApproximately(a.RAMByteHours, unit.RAMByteHours) {
  1540. t.Fatalf("allocation %s: expected %f; got %f", k, unit.RAMByteHours, a.RAMByteHours)
  1541. }
  1542. if !util.IsApproximately(a.RAMCost, unit.RAMCost) {
  1543. t.Fatalf("allocation %s: expected %f; got %f", k, unit.RAMCost, a.RAMCost)
  1544. }
  1545. if !util.IsApproximately(a.GPUHours, unit.GPUHours) {
  1546. t.Fatalf("allocation %s: expected %f; got %f", k, unit.GPUHours, a.GPUHours)
  1547. }
  1548. if !util.IsApproximately(a.GPUCost, unit.GPUCost) {
  1549. t.Fatalf("allocation %s: expected %f; got %f", k, unit.GPUCost, a.GPUCost)
  1550. }
  1551. if !util.IsApproximately(a.PVByteHours, unit.PVByteHours) {
  1552. t.Fatalf("allocation %s: expected %f; got %f", k, unit.PVByteHours, a.PVByteHours)
  1553. }
  1554. if !util.IsApproximately(a.PVCost, unit.PVCost) {
  1555. t.Fatalf("allocation %s: expected %f; got %f", k, unit.PVCost, a.PVCost)
  1556. }
  1557. if !util.IsApproximately(a.NetworkCost, unit.NetworkCost) {
  1558. t.Fatalf("allocation %s: expected %f; got %f", k, unit.NetworkCost, a.NetworkCost)
  1559. }
  1560. if !util.IsApproximately(a.TotalCost(), unit.TotalCost()) {
  1561. t.Fatalf("allocation %s: expected %f; got %f", k, unit.TotalCost(), a.TotalCost())
  1562. }
  1563. })
  1564. }
  1565. // TODO niko/etl
  1566. // func TestAllocationSetRange_Length(t *testing.T) {}
  1567. // TODO niko/etl
  1568. // func TestAllocationSetRange_MarshalJSON(t *testing.T) {}
  1569. // TODO niko/etl
  1570. // func TestAllocationSetRange_Slice(t *testing.T) {}
  1571. // TODO niko/etl
  1572. // func TestAllocationSetRange_Window(t *testing.T) {}