asset_test.go 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287
  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. var start1 = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
  11. var start2 = start1.Add(day)
  12. var start3 = start2.Add(day)
  13. var start4 = start2.Add(day)
  14. var windows = []Window{
  15. NewWindow(&start1, &start2),
  16. NewWindow(&start2, &start3),
  17. NewWindow(&start3, &start4),
  18. }
  19. const gb = 1024 * 1024 * 1024
  20. // generateAssetSet generates the following topology:
  21. //
  22. // | Asset | Cost | Adj |
  23. // +------------------------------+------+------+
  24. // cluster1:
  25. // node1: 6.00 1.00
  26. // node2: 4.00 1.50
  27. // node3: 7.00 -0.50
  28. // disk1: 2.50 0.00
  29. // disk2: 1.50 0.00
  30. // clusterManagement1: 3.00 0.00
  31. // +------------------------------+------+------+
  32. // cluster1 subtotal 24.00 2.00
  33. // +------------------------------+------+------+
  34. // cluster2:
  35. // node4: 12.00 -1.00
  36. // disk3: 2.50 0.00
  37. // disk4: 1.50 0.00
  38. // clusterManagement2: 0.00 0.00
  39. // +------------------------------+------+------+
  40. // cluster2 subtotal 16.00 -1.00
  41. // +------------------------------+------+------+
  42. // cluster3:
  43. // node5: 17.00 2.00
  44. // +------------------------------+------+------+
  45. // cluster3 subtotal 17.00 2.00
  46. // +------------------------------+------+------+
  47. // total 57.00 3.00
  48. // +------------------------------+------+------+
  49. func generateAssetSet(start time.Time) *AssetSet {
  50. end := start.Add(day)
  51. window := NewWindow(&start, &end)
  52. hours := window.Duration().Hours()
  53. node1 := NewNode("node1", "cluster1", "gcp-node1", *window.Clone().start, *window.Clone().end, window.Clone())
  54. node1.CPUCost = 4.0
  55. node1.RAMCost = 4.0
  56. node1.GPUCost = 2.0
  57. node1.Discount = 0.5
  58. node1.CPUCoreHours = 2.0 * hours
  59. node1.RAMByteHours = 4.0 * gb * hours
  60. node1.GPUHours = 1.0 * hours
  61. node1.SetAdjustment(1.0)
  62. node1.SetLabels(map[string]string{"test": "test"})
  63. node2 := NewNode("node2", "cluster1", "gcp-node2", *window.Clone().start, *window.Clone().end, window.Clone())
  64. node2.CPUCost = 4.0
  65. node2.RAMCost = 4.0
  66. node2.GPUCost = 0.0
  67. node2.Discount = 0.5
  68. node2.CPUCoreHours = 2.0 * hours
  69. node2.RAMByteHours = 4.0 * gb * hours
  70. node2.GPUHours = 0.0 * hours
  71. node2.SetAdjustment(1.5)
  72. node3 := NewNode("node3", "cluster1", "gcp-node3", *window.Clone().start, *window.Clone().end, window.Clone())
  73. node3.CPUCost = 4.0
  74. node3.RAMCost = 4.0
  75. node3.GPUCost = 3.0
  76. node3.Discount = 0.5
  77. node3.CPUCoreHours = 2.0 * hours
  78. node3.RAMByteHours = 4.0 * gb * hours
  79. node3.GPUHours = 2.0 * hours
  80. node3.SetAdjustment(-0.5)
  81. node4 := NewNode("node4", "cluster2", "gcp-node4", *window.Clone().start, *window.Clone().end, window.Clone())
  82. node4.CPUCost = 10.0
  83. node4.RAMCost = 6.0
  84. node4.GPUCost = 0.0
  85. node4.Discount = 0.25
  86. node4.CPUCoreHours = 4.0 * hours
  87. node4.RAMByteHours = 12.0 * gb * hours
  88. node4.GPUHours = 0.0 * hours
  89. node4.SetAdjustment(-1.0)
  90. node5 := NewNode("node5", "cluster3", "aws-node5", *window.Clone().start, *window.Clone().end, window.Clone())
  91. node5.CPUCost = 10.0
  92. node5.RAMCost = 7.0
  93. node5.GPUCost = 0.0
  94. node5.Discount = 0.0
  95. node5.CPUCoreHours = 8.0 * hours
  96. node5.RAMByteHours = 24.0 * gb * hours
  97. node5.GPUHours = 0.0 * hours
  98. node5.SetAdjustment(2.0)
  99. disk1 := NewDisk("disk1", "cluster1", "gcp-disk1", *window.Clone().start, *window.Clone().end, window.Clone())
  100. disk1.Cost = 2.5
  101. disk1.ByteHours = 100 * gb * hours
  102. disk2 := NewDisk("disk2", "cluster1", "gcp-disk2", *window.Clone().start, *window.Clone().end, window.Clone())
  103. disk2.Cost = 1.5
  104. disk2.ByteHours = 60 * gb * hours
  105. disk3 := NewDisk("disk3", "cluster2", "gcp-disk3", *window.Clone().start, *window.Clone().end, window.Clone())
  106. disk3.Cost = 2.5
  107. disk3.ByteHours = 100 * gb * hours
  108. disk4 := NewDisk("disk4", "cluster2", "gcp-disk4", *window.Clone().start, *window.Clone().end, window.Clone())
  109. disk4.Cost = 1.5
  110. disk4.ByteHours = 100 * gb * hours
  111. cm1 := NewClusterManagement("gcp", "cluster1", window.Clone())
  112. cm1.Cost = 3.0
  113. cm2 := NewClusterManagement("gcp", "cluster2", window.Clone())
  114. cm2.Cost = 0.0
  115. return NewAssetSet(
  116. start, end,
  117. // cluster 1
  118. node1, node2, node3, disk1, disk2, cm1,
  119. // cluster 2
  120. node4, disk3, disk4, cm2,
  121. // cluster 3
  122. node5,
  123. )
  124. }
  125. func assertAssetSet(t *testing.T, as *AssetSet, msg string, window Window, exps map[string]float64, err error) {
  126. if err != nil {
  127. t.Fatalf("AssetSet.AggregateBy[%s]: unexpected error: %s", msg, err)
  128. }
  129. if as.Length() != len(exps) {
  130. t.Fatalf("AssetSet.AggregateBy[%s]: expected set of length %d, actual %d", msg, len(exps), as.Length())
  131. }
  132. if !as.Window.Equal(window) {
  133. t.Fatalf("AssetSet.AggregateBy[%s]: expected window %s, actual %s", msg, window, as.Window)
  134. }
  135. as.Each(func(key string, a Asset) {
  136. if exp, ok := exps[key]; ok {
  137. if math.Round(a.TotalCost()*100) != math.Round(exp*100) {
  138. t.Fatalf("AssetSet.AggregateBy[%s]: key %s expected total cost %.2f, actual %.2f", msg, key, exp, a.TotalCost())
  139. }
  140. if !a.Window().Equal(window) {
  141. t.Fatalf("AssetSet.AggregateBy[%s]: key %s expected window %s, actual %s", msg, key, window, a.Window())
  142. }
  143. } else {
  144. t.Fatalf("AssetSet.AggregateBy[%s]: unexpected asset: %s", msg, key)
  145. }
  146. })
  147. }
  148. func printAssetSet(msg string, as *AssetSet) {
  149. fmt.Printf("--- %s ---\n", msg)
  150. as.Each(func(key string, a Asset) {
  151. fmt.Printf(" > %s: %s\n", key, a)
  152. })
  153. }
  154. func TestAny_Add(t *testing.T) {
  155. any1 := NewAsset(*windows[0].start, *windows[0].end, windows[0])
  156. any1.SetProperties(&AssetProperties{
  157. Name: "any1",
  158. Cluster: "cluster1",
  159. ProviderID: "any1",
  160. })
  161. any1.Cost = 9.0
  162. any1.SetAdjustment(1.0)
  163. any2 := NewAsset(*windows[0].start, *windows[0].end, windows[0])
  164. any2.SetProperties(&AssetProperties{
  165. Name: "any2",
  166. Cluster: "cluster1",
  167. ProviderID: "any2",
  168. })
  169. any2.Cost = 4.0
  170. any2.SetAdjustment(1.0)
  171. any3 := any1.Add(any2)
  172. // Check that the sums and properties are correct
  173. if any3.TotalCost() != 15.0 {
  174. t.Fatalf("Any.Add: expected %f; got %f", 15.0, any3.TotalCost())
  175. }
  176. if any3.Adjustment() != 2.0 {
  177. t.Fatalf("Any.Add: expected %f; got %f", 2.0, any3.Adjustment())
  178. }
  179. if any3.Properties().Cluster != "cluster1" {
  180. t.Fatalf("Any.Add: expected %s; got %s", "cluster1", any3.Properties().Cluster)
  181. }
  182. if any3.Type() != AnyAssetType {
  183. t.Fatalf("Any.Add: expected %s; got %s", AnyAssetType, any3.Type())
  184. }
  185. if any3.Properties().ProviderID != "" {
  186. t.Fatalf("Any.Add: expected %s; got %s", "", any3.Properties().ProviderID)
  187. }
  188. if any3.Properties().Name != "" {
  189. t.Fatalf("Any.Add: expected %s; got %s", "", any3.Properties().Name)
  190. }
  191. // Check that the original assets are unchanged
  192. if any1.TotalCost() != 10.0 {
  193. t.Fatalf("Any.Add: expected %f; got %f", 10.0, any1.TotalCost())
  194. }
  195. if any1.Adjustment() != 1.0 {
  196. t.Fatalf("Any.Add: expected %f; got %f", 1.0, any1.Adjustment())
  197. }
  198. if any2.TotalCost() != 5.0 {
  199. t.Fatalf("Any.Add: expected %f; got %f", 5.0, any2.TotalCost())
  200. }
  201. if any2.Adjustment() != 1.0 {
  202. t.Fatalf("Any.Add: expected %f; got %f", 1.0, any2.Adjustment())
  203. }
  204. }
  205. func TestAny_Clone(t *testing.T) {
  206. any1 := NewAsset(*windows[0].start, *windows[0].end, windows[0])
  207. any1.SetProperties(&AssetProperties{
  208. Name: "any1",
  209. Cluster: "cluster1",
  210. ProviderID: "any1",
  211. })
  212. any1.Cost = 9.0
  213. any1.SetAdjustment(1.0)
  214. any2 := any1.Clone()
  215. any1.Cost = 18.0
  216. any1.SetAdjustment(2.0)
  217. // any2 should match any1, even after mutating any1
  218. if any2.TotalCost() != 10.0 {
  219. t.Fatalf("Any.Clone: expected %f; got %f", 10.0, any2.TotalCost())
  220. }
  221. if any2.Adjustment() != 1.0 {
  222. t.Fatalf("Any.Clone: expected %f; got %f", 1.0, any2.Adjustment())
  223. }
  224. }
  225. func TestAny_MarshalJSON(t *testing.T) {
  226. any1 := NewAsset(*windows[0].start, *windows[0].end, windows[0])
  227. any1.SetProperties(&AssetProperties{
  228. Name: "any1",
  229. Cluster: "cluster1",
  230. ProviderID: "any1",
  231. })
  232. any1.Cost = 9.0
  233. any1.SetAdjustment(1.0)
  234. _, err := json.Marshal(any1)
  235. if err != nil {
  236. t.Fatalf("Any.MarshalJSON: unexpected error: %s", err)
  237. }
  238. any2 := NewAsset(*windows[0].start, *windows[0].end, windows[0])
  239. any2.SetProperties(&AssetProperties{
  240. Name: "any2",
  241. Cluster: "cluster1",
  242. ProviderID: "any2",
  243. })
  244. any2.Cost = math.NaN()
  245. any2.SetAdjustment(1.0)
  246. _, err = json.Marshal(any2)
  247. if err != nil {
  248. t.Fatalf("Any.MarshalJSON: unexpected error: %s", err)
  249. }
  250. }
  251. func TestDisk_Add(t *testing.T) {
  252. // 1. aggregate: add size, local
  253. // 2. accumulate: don't add size, local
  254. hours := windows[0].Duration().Hours()
  255. // Aggregate: two disks, one window
  256. disk1 := NewDisk("disk1", "cluster1", "disk1", *windows[0].start, *windows[0].end, windows[0])
  257. disk1.ByteHours = 100.0 * gb * hours
  258. disk1.Cost = 9.0
  259. disk1.SetAdjustment(1.0)
  260. if disk1.Bytes() != 100.0*gb {
  261. t.Fatalf("Disk.Add: expected %f; got %f", 100.0*gb, disk1.Bytes())
  262. }
  263. disk2 := NewDisk("disk2", "cluster1", "disk2", *windows[0].start, *windows[0].end, windows[0])
  264. disk2.ByteHours = 60.0 * gb * hours
  265. disk2.Cost = 4.0
  266. disk2.Local = 1.0
  267. disk2.SetAdjustment(1.0)
  268. if disk2.Bytes() != 60.0*gb {
  269. t.Fatalf("Disk.Add: expected %f; got %f", 60.0*gb, disk2.Bytes())
  270. }
  271. diskT := disk1.Add(disk2).(*Disk)
  272. // Check that the sums and properties are correct
  273. if diskT.TotalCost() != 15.0 {
  274. t.Fatalf("Disk.Add: expected %f; got %f", 15.0, diskT.TotalCost())
  275. }
  276. if diskT.Adjustment() != 2.0 {
  277. t.Fatalf("Disk.Add: expected %f; got %f", 2.0, diskT.Adjustment())
  278. }
  279. if diskT.Properties().Cluster != "cluster1" {
  280. t.Fatalf("Disk.Add: expected %s; got %s", "cluster1", diskT.Properties().Cluster)
  281. }
  282. if diskT.Type() != DiskAssetType {
  283. t.Fatalf("Disk.Add: expected %s; got %s", AnyAssetType, diskT.Type())
  284. }
  285. if diskT.Properties().ProviderID != "" {
  286. t.Fatalf("Disk.Add: expected %s; got %s", "", diskT.Properties().ProviderID)
  287. }
  288. if diskT.Properties().Name != "" {
  289. t.Fatalf("Disk.Add: expected %s; got %s", "", diskT.Properties().Name)
  290. }
  291. if diskT.Bytes() != 160.0*gb {
  292. t.Fatalf("Disk.Add: expected %f; got %f", 160.0*gb, diskT.Bytes())
  293. }
  294. if !util.IsApproximately(diskT.Local, 0.333333) {
  295. t.Fatalf("Disk.Add: expected %f; got %f", 0.333333, diskT.Local)
  296. }
  297. // Check that the original assets are unchanged
  298. if disk1.TotalCost() != 10.0 {
  299. t.Fatalf("Disk.Add: expected %f; got %f", 10.0, disk1.TotalCost())
  300. }
  301. if disk1.Adjustment() != 1.0 {
  302. t.Fatalf("Disk.Add: expected %f; got %f", 1.0, disk1.Adjustment())
  303. }
  304. if disk1.Local != 0.0 {
  305. t.Fatalf("Disk.Add: expected %f; got %f", 0.0, disk1.Local)
  306. }
  307. if disk2.TotalCost() != 5.0 {
  308. t.Fatalf("Disk.Add: expected %f; got %f", 5.0, disk2.TotalCost())
  309. }
  310. if disk2.Adjustment() != 1.0 {
  311. t.Fatalf("Disk.Add: expected %f; got %f", 1.0, disk2.Adjustment())
  312. }
  313. if disk2.Local != 1.0 {
  314. t.Fatalf("Disk.Add: expected %f; got %f", 1.0, disk2.Local)
  315. }
  316. disk3 := NewDisk("disk3", "cluster1", "disk3", *windows[0].start, *windows[0].end, windows[0])
  317. disk3.ByteHours = 0.0 * hours
  318. disk3.Cost = 0.0
  319. disk3.Local = 0.0
  320. disk3.SetAdjustment(0.0)
  321. disk4 := NewDisk("disk4", "cluster1", "disk4", *windows[0].start, *windows[0].end, windows[0])
  322. disk4.ByteHours = 0.0 * hours
  323. disk4.Cost = 0.0
  324. disk4.Local = 1.0
  325. disk4.SetAdjustment(0.0)
  326. diskT = disk3.Add(disk4).(*Disk)
  327. if diskT.TotalCost() != 0.0 {
  328. t.Fatalf("Disk.Add: expected %f; got %f", 0.0, diskT.TotalCost())
  329. }
  330. if diskT.Local != 0.5 {
  331. t.Fatalf("Disk.Add: expected %f; got %f", 0.5, diskT.Local)
  332. }
  333. // Accumulate: one disks, two windows
  334. diskA1 := NewDisk("diskA1", "cluster1", "diskA1", *windows[0].start, *windows[0].end, windows[0])
  335. diskA1.ByteHours = 100 * gb * hours
  336. diskA1.Cost = 9.0
  337. diskA1.SetAdjustment(1.0)
  338. diskA2 := NewDisk("diskA2", "cluster1", "diskA2", *windows[1].start, *windows[1].end, windows[1])
  339. diskA2.ByteHours = 100 * gb * hours
  340. diskA2.Cost = 9.0
  341. diskA2.SetAdjustment(1.0)
  342. diskAT := diskA1.Add(diskA2).(*Disk)
  343. // Check that the sums and properties are correct
  344. if diskAT.TotalCost() != 20.0 {
  345. t.Fatalf("Disk.Add: expected %f; got %f", 20.0, diskAT.TotalCost())
  346. }
  347. if diskAT.Adjustment() != 2.0 {
  348. t.Fatalf("Disk.Add: expected %f; got %f", 2.0, diskAT.Adjustment())
  349. }
  350. if diskAT.Properties().Cluster != "cluster1" {
  351. t.Fatalf("Disk.Add: expected %s; got %s", "cluster1", diskAT.Properties().Cluster)
  352. }
  353. if diskAT.Type() != DiskAssetType {
  354. t.Fatalf("Disk.Add: expected %s; got %s", AnyAssetType, diskAT.Type())
  355. }
  356. if diskAT.Properties().ProviderID != "" {
  357. t.Fatalf("Disk.Add: expected %s; got %s", "", diskAT.Properties().ProviderID)
  358. }
  359. if diskAT.Properties().Name != "" {
  360. t.Fatalf("Disk.Add: expected %s; got %s", "", diskAT.Properties().Name)
  361. }
  362. if diskAT.Bytes() != 100.0*gb {
  363. t.Fatalf("Disk.Add: expected %f; got %f", 100.0*gb, diskT.Bytes())
  364. }
  365. if diskAT.Local != 0.0 {
  366. t.Fatalf("Disk.Add: expected %f; got %f", 0.0, diskAT.Local)
  367. }
  368. // Check that the original assets are unchanged
  369. if diskA1.TotalCost() != 10.0 {
  370. t.Fatalf("Disk.Add: expected %f; got %f", 10.0, diskA1.TotalCost())
  371. }
  372. if diskA1.Adjustment() != 1.0 {
  373. t.Fatalf("Disk.Add: expected %f; got %f", 1.0, diskA1.Adjustment())
  374. }
  375. if diskA1.Local != 0.0 {
  376. t.Fatalf("Disk.Add: expected %f; got %f", 0.0, diskA1.Local)
  377. }
  378. if diskA2.TotalCost() != 10.0 {
  379. t.Fatalf("Disk.Add: expected %f; got %f", 10.0, diskA2.TotalCost())
  380. }
  381. if diskA2.Adjustment() != 1.0 {
  382. t.Fatalf("Disk.Add: expected %f; got %f", 1.0, diskA2.Adjustment())
  383. }
  384. if diskA2.Local != 0.0 {
  385. t.Fatalf("Disk.Add: expected %f; got %f", 0.0, diskA2.Local)
  386. }
  387. }
  388. func TestDisk_Clone(t *testing.T) {
  389. disk1 := NewDisk("disk1", "cluster1", "disk1", *windows[0].start, *windows[0].end, windows[0])
  390. disk1.Local = 0.0
  391. disk1.Cost = 9.0
  392. disk1.SetAdjustment(1.0)
  393. disk2 := disk1.Clone().(*Disk)
  394. disk2.Local = 1.0
  395. disk1.Cost = 18.0
  396. disk1.SetAdjustment(2.0)
  397. // disk2 should match disk1, even after mutating disk1
  398. if disk2.TotalCost() != 10.0 {
  399. t.Fatalf("Any.Clone: expected %f; got %f", 10.0, disk2.TotalCost())
  400. }
  401. if disk2.Adjustment() != 1.0 {
  402. t.Fatalf("Any.Clone: expected %f; got %f", 1.0, disk2.Adjustment())
  403. }
  404. if disk2.Local != 1.0 {
  405. t.Fatalf("Disk.Add: expected %f; got %f", 1.0, disk2.Local)
  406. }
  407. }
  408. func TestDisk_MarshalJSON(t *testing.T) {
  409. disk := NewDisk("disk", "cluster", "providerID", *windows[0].start, *windows[0].end, windows[0])
  410. disk.SetLabels(AssetLabels{
  411. "label": "value",
  412. })
  413. disk.Cost = 9.0
  414. disk.SetAdjustment(1.0)
  415. _, err := json.Marshal(disk)
  416. if err != nil {
  417. t.Fatalf("Disk.MarshalJSON: unexpected error: %s", err)
  418. }
  419. }
  420. func TestNode_Add(t *testing.T) {
  421. // 1. aggregate: add size, local
  422. // 2. accumulate: don't add size, local
  423. hours := windows[0].Duration().Hours()
  424. // Aggregate: two nodes, one window
  425. node1 := NewNode("node1", "cluster1", "node1", *windows[0].start, *windows[0].end, windows[0])
  426. node1.CPUCoreHours = 1.0 * hours
  427. node1.RAMByteHours = 2.0 * gb * hours
  428. node1.GPUHours = 0.0 * hours
  429. node1.GPUCost = 0.0
  430. node1.CPUCost = 8.0
  431. node1.RAMCost = 4.0
  432. node1.Discount = 0.3
  433. node1.CPUBreakdown = &Breakdown{
  434. Idle: 0.6,
  435. System: 0.2,
  436. User: 0.2,
  437. Other: 0.0,
  438. }
  439. node1.RAMBreakdown = &Breakdown{
  440. Idle: 0.6,
  441. System: 0.2,
  442. User: 0.2,
  443. Other: 0.0,
  444. }
  445. node1.SetAdjustment(1.6)
  446. node2 := NewNode("node2", "cluster1", "node2", *windows[0].start, *windows[0].end, windows[0])
  447. node2.CPUCoreHours = 1.0 * hours
  448. node2.RAMByteHours = 2.0 * gb * hours
  449. node2.GPUHours = 0.0 * hours
  450. node2.GPUCost = 0.0
  451. node2.CPUCost = 3.0
  452. node2.RAMCost = 1.0
  453. node2.Discount = 0.0
  454. node1.CPUBreakdown = &Breakdown{
  455. Idle: 0.9,
  456. System: 0.05,
  457. User: 0.0,
  458. Other: 0.05,
  459. }
  460. node1.RAMBreakdown = &Breakdown{
  461. Idle: 0.9,
  462. System: 0.05,
  463. User: 0.0,
  464. Other: 0.05,
  465. }
  466. node2.SetAdjustment(1.0)
  467. nodeT := node1.Add(node2).(*Node)
  468. // Check that the sums and properties are correct
  469. if !util.IsApproximately(nodeT.TotalCost(), 15.0) {
  470. t.Fatalf("Node.Add: expected %f; got %f", 15.0, nodeT.TotalCost())
  471. }
  472. if nodeT.Adjustment() != 2.6 {
  473. t.Fatalf("Node.Add: expected %f; got %f", 2.6, nodeT.Adjustment())
  474. }
  475. if nodeT.Properties().Cluster != "cluster1" {
  476. t.Fatalf("Node.Add: expected %s; got %s", "cluster1", nodeT.Properties().Cluster)
  477. }
  478. if nodeT.Type() != NodeAssetType {
  479. t.Fatalf("Node.Add: expected %s; got %s", AnyAssetType, nodeT.Type())
  480. }
  481. if nodeT.Properties().ProviderID != "" {
  482. t.Fatalf("Node.Add: expected %s; got %s", "", nodeT.Properties().ProviderID)
  483. }
  484. if nodeT.Properties().Name != "" {
  485. t.Fatalf("Node.Add: expected %s; got %s", "", nodeT.Properties().Name)
  486. }
  487. if nodeT.CPUCores() != 2.0 {
  488. t.Fatalf("Node.Add: expected %f; got %f", 2.0, nodeT.CPUCores())
  489. }
  490. if nodeT.RAMBytes() != 4.0*gb {
  491. t.Fatalf("Node.Add: expected %f; got %f", 4.0*gb, nodeT.RAMBytes())
  492. }
  493. // Check that the original assets are unchanged
  494. if !util.IsApproximately(node1.TotalCost(), 10.0) {
  495. t.Fatalf("Node.Add: expected %f; got %f", 10.0, node1.TotalCost())
  496. }
  497. if node1.Adjustment() != 1.6 {
  498. t.Fatalf("Node.Add: expected %f; got %f", 1.0, node1.Adjustment())
  499. }
  500. if !util.IsApproximately(node2.TotalCost(), 5.0) {
  501. t.Fatalf("Node.Add: expected %f; got %f", 5.0, node2.TotalCost())
  502. }
  503. if node2.Adjustment() != 1.0 {
  504. t.Fatalf("Node.Add: expected %f; got %f", 1.0, node2.Adjustment())
  505. }
  506. // Check that we don't divide by zero computing Local
  507. node3 := NewNode("node3", "cluster1", "node3", *windows[0].start, *windows[0].end, windows[0])
  508. node3.CPUCoreHours = 0 * hours
  509. node3.RAMByteHours = 0 * hours
  510. node3.GPUHours = 0.0 * hours
  511. node3.GPUCost = 0
  512. node3.CPUCost = 0.0
  513. node3.RAMCost = 0.0
  514. node3.Discount = 0.3
  515. node3.SetAdjustment(0.0)
  516. node4 := NewNode("node4", "cluster1", "node4", *windows[0].start, *windows[0].end, windows[0])
  517. node4.CPUCoreHours = 0 * hours
  518. node4.RAMByteHours = 0 * hours
  519. node4.GPUHours = 0.0 * hours
  520. node4.GPUCost = 0
  521. node4.CPUCost = 0.0
  522. node4.RAMCost = 0.0
  523. node4.Discount = 0.1
  524. node4.SetAdjustment(0.0)
  525. nodeT = node3.Add(node4).(*Node)
  526. // Check that the sums and properties are correct and without NaNs
  527. if nodeT.TotalCost() != 0.0 {
  528. t.Fatalf("Node.Add: expected %f; got %f", 0.0, nodeT.TotalCost())
  529. }
  530. if nodeT.Discount != 0.2 {
  531. t.Fatalf("Node.Add: expected %f; got %f", 0.2, nodeT.Discount)
  532. }
  533. // Accumulate: one nodes, two window
  534. nodeA1 := NewNode("nodeA1", "cluster1", "nodeA1", *windows[0].start, *windows[0].end, windows[0])
  535. nodeA1.CPUCoreHours = 1.0 * hours
  536. nodeA1.RAMByteHours = 2.0 * gb * hours
  537. nodeA1.GPUHours = 0.0 * hours
  538. nodeA1.GPUCost = 0.0
  539. nodeA1.CPUCost = 8.0
  540. nodeA1.RAMCost = 4.0
  541. nodeA1.Discount = 0.3
  542. nodeA1.SetAdjustment(1.6)
  543. nodeA2 := NewNode("nodeA2", "cluster1", "nodeA2", *windows[1].start, *windows[1].end, windows[1])
  544. nodeA2.CPUCoreHours = 1.0 * hours
  545. nodeA2.RAMByteHours = 2.0 * gb * hours
  546. nodeA2.GPUHours = 0.0 * hours
  547. nodeA2.GPUCost = 0.0
  548. nodeA2.CPUCost = 3.0
  549. nodeA2.RAMCost = 1.0
  550. nodeA2.Discount = 0.0
  551. nodeA2.SetAdjustment(1.0)
  552. nodeAT := nodeA1.Add(nodeA2).(*Node)
  553. // Check that the sums and properties are correct
  554. if !util.IsApproximately(nodeAT.TotalCost(), 15.0) {
  555. t.Fatalf("Node.Add: expected %f; got %f", 15.0, nodeAT.TotalCost())
  556. }
  557. if nodeAT.Adjustment() != 2.6 {
  558. t.Fatalf("Node.Add: expected %f; got %f", 2.6, nodeAT.Adjustment())
  559. }
  560. if nodeAT.Properties().Cluster != "cluster1" {
  561. t.Fatalf("Node.Add: expected %s; got %s", "cluster1", nodeAT.Properties().Cluster)
  562. }
  563. if nodeAT.Type() != NodeAssetType {
  564. t.Fatalf("Node.Add: expected %s; got %s", AnyAssetType, nodeAT.Type())
  565. }
  566. if nodeAT.Properties().ProviderID != "" {
  567. t.Fatalf("Node.Add: expected %s; got %s", "", nodeAT.Properties().ProviderID)
  568. }
  569. if nodeAT.Properties().Name != "" {
  570. t.Fatalf("Node.Add: expected %s; got %s", "", nodeAT.Properties().Name)
  571. }
  572. if nodeAT.CPUCores() != 1.0 {
  573. t.Fatalf("Node.Add: expected %f; got %f", 1.0, nodeAT.CPUCores())
  574. }
  575. if nodeAT.RAMBytes() != 2.0*gb {
  576. t.Fatalf("Node.Add: expected %f; got %f", 2.0*gb, nodeAT.RAMBytes())
  577. }
  578. if nodeAT.GPUs() != 0.0 {
  579. t.Fatalf("Node.Add: expected %f; got %f", 0.0, nodeAT.GPUs())
  580. }
  581. // Check that the original assets are unchanged
  582. if !util.IsApproximately(nodeA1.TotalCost(), 10.0) {
  583. t.Fatalf("Node.Add: expected %f; got %f", 10.0, nodeA1.TotalCost())
  584. }
  585. if nodeA1.Adjustment() != 1.6 {
  586. t.Fatalf("Node.Add: expected %f; got %f", 1.0, nodeA1.Adjustment())
  587. }
  588. if !util.IsApproximately(nodeA2.TotalCost(), 5.0) {
  589. t.Fatalf("Node.Add: expected %f; got %f", 5.0, nodeA2.TotalCost())
  590. }
  591. if nodeA2.Adjustment() != 1.0 {
  592. t.Fatalf("Node.Add: expected %f; got %f", 1.0, nodeA2.Adjustment())
  593. }
  594. }
  595. func TestNode_Clone(t *testing.T) {
  596. // TODO
  597. }
  598. func TestNode_MarshalJSON(t *testing.T) {
  599. node := NewNode("node", "cluster", "providerID", *windows[0].start, *windows[0].end, windows[0])
  600. node.SetLabels(AssetLabels{
  601. "label": "value",
  602. })
  603. node.CPUCost = 9.0
  604. node.RAMCost = 0.0
  605. node.RAMCost = 21.0
  606. node.CPUCoreHours = 123.0
  607. node.RAMByteHours = 13323.0
  608. node.GPUHours = 123.0
  609. node.SetAdjustment(1.0)
  610. _, err := json.Marshal(node)
  611. if err != nil {
  612. t.Fatalf("Node.MarshalJSON: unexpected error: %s", err)
  613. }
  614. }
  615. func TestClusterManagement_Add(t *testing.T) {
  616. cm1 := NewClusterManagement("gcp", "cluster1", windows[0])
  617. cm1.Cost = 9.0
  618. cm2 := NewClusterManagement("gcp", "cluster1", windows[0])
  619. cm2.Cost = 4.0
  620. cm3 := cm1.Add(cm2)
  621. // Check that the sums and properties are correct
  622. if cm3.TotalCost() != 13.0 {
  623. t.Fatalf("ClusterManagement.Add: expected %f; got %f", 13.0, cm3.TotalCost())
  624. }
  625. if cm3.Properties().Cluster != "cluster1" {
  626. t.Fatalf("ClusterManagement.Add: expected %s; got %s", "cluster1", cm3.Properties().Cluster)
  627. }
  628. if cm3.Type() != ClusterManagementAssetType {
  629. t.Fatalf("ClusterManagement.Add: expected %s; got %s", ClusterManagementAssetType, cm3.Type())
  630. }
  631. // Check that the original assets are unchanged
  632. if cm1.TotalCost() != 9.0 {
  633. t.Fatalf("ClusterManagement.Add: expected %f; got %f", 9.0, cm1.TotalCost())
  634. }
  635. if cm2.TotalCost() != 4.0 {
  636. t.Fatalf("ClusterManagement.Add: expected %f; got %f", 4.0, cm2.TotalCost())
  637. }
  638. }
  639. func TestClusterManagement_Clone(t *testing.T) {
  640. // TODO
  641. }
  642. func TestCloudAny_Add(t *testing.T) {
  643. ca1 := NewCloud(ComputeCategory, "ca1", *windows[0].start, *windows[0].end, windows[0])
  644. ca1.Cost = 9.0
  645. ca1.SetAdjustment(1.0)
  646. ca2 := NewCloud(StorageCategory, "ca2", *windows[0].start, *windows[0].end, windows[0])
  647. ca2.Cost = 4.0
  648. ca2.SetAdjustment(1.0)
  649. ca3 := ca1.Add(ca2)
  650. // Check that the sums and properties are correct
  651. if ca3.TotalCost() != 15.0 {
  652. t.Fatalf("Any.Add: expected %f; got %f", 15.0, ca3.TotalCost())
  653. }
  654. if ca3.Adjustment() != 2.0 {
  655. t.Fatalf("Any.Add: expected %f; got %f", 2.0, ca3.Adjustment())
  656. }
  657. if ca3.Type() != CloudAssetType {
  658. t.Fatalf("Any.Add: expected %s; got %s", CloudAssetType, ca3.Type())
  659. }
  660. // Check that the original assets are unchanged
  661. if ca1.TotalCost() != 10.0 {
  662. t.Fatalf("Any.Add: expected %f; got %f", 10.0, ca1.TotalCost())
  663. }
  664. if ca1.Adjustment() != 1.0 {
  665. t.Fatalf("Any.Add: expected %f; got %f", 1.0, ca1.Adjustment())
  666. }
  667. if ca2.TotalCost() != 5.0 {
  668. t.Fatalf("Any.Add: expected %f; got %f", 5.0, ca2.TotalCost())
  669. }
  670. if ca2.Adjustment() != 1.0 {
  671. t.Fatalf("Any.Add: expected %f; got %f", 1.0, ca2.Adjustment())
  672. }
  673. }
  674. func TestCloudAny_Clone(t *testing.T) {
  675. // TODO
  676. }
  677. func TestAssetSet_AggregateBy(t *testing.T) {
  678. endYesterday := time.Now().UTC().Truncate(day)
  679. startYesterday := endYesterday.Add(-day)
  680. window := NewWindow(&startYesterday, &endYesterday)
  681. // Scenarios to test:
  682. // 1 Single-aggregation
  683. // 1a []AssetProperty=[Cluster]
  684. // 1b []AssetProperty=[Type]
  685. // 1c []AssetProperty=[Nil]
  686. // 1d []AssetProperty=nil
  687. // 1e aggregateBy []string=["label:test"]
  688. // 2 Multi-aggregation
  689. // 2a []AssetProperty=[Cluster,Type]
  690. // 3 Share resources
  691. // 3a Shared hourly cost > 0.0
  692. // Definitions and set-up:
  693. var as *AssetSet
  694. var err error
  695. // Tests:
  696. // 1 Single-aggregation
  697. // 1a []AssetProperty=[Cluster]
  698. as = generateAssetSet(startYesterday)
  699. err = as.AggregateBy([]string{string(AssetClusterProp)}, nil)
  700. if err != nil {
  701. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  702. }
  703. assertAssetSet(t, as, "1a", window, map[string]float64{
  704. "cluster1": 26.0,
  705. "cluster2": 15.0,
  706. "cluster3": 19.0,
  707. }, nil)
  708. // 1b []AssetProperty=[Type]
  709. as = generateAssetSet(startYesterday)
  710. err = as.AggregateBy([]string{string(AssetTypeProp)}, nil)
  711. if err != nil {
  712. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  713. }
  714. assertAssetSet(t, as, "1b", window, map[string]float64{
  715. "Node": 49.0,
  716. "Disk": 8.0,
  717. "ClusterManagement": 3.0,
  718. }, nil)
  719. // 1c []AssetProperty=[Nil]
  720. as = generateAssetSet(startYesterday)
  721. err = as.AggregateBy([]string{}, nil)
  722. if err != nil {
  723. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  724. }
  725. assertAssetSet(t, as, "1c", window, map[string]float64{
  726. "": 60.0,
  727. }, nil)
  728. // 1d []AssetProperty=nil
  729. as = generateAssetSet(startYesterday)
  730. err = as.AggregateBy(nil, nil)
  731. if err != nil {
  732. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  733. }
  734. assertAssetSet(t, as, "1d", window, map[string]float64{
  735. "__undefined__/__undefined__/__undefined__/Compute/cluster1/Node/Kubernetes/gcp-node1/node1": 7.00,
  736. "__undefined__/__undefined__/__undefined__/Compute/cluster1/Node/Kubernetes/gcp-node2/node2": 5.50,
  737. "__undefined__/__undefined__/__undefined__/Compute/cluster1/Node/Kubernetes/gcp-node3/node3": 6.50,
  738. "__undefined__/__undefined__/__undefined__/Storage/cluster1/Disk/Kubernetes/gcp-disk1/disk1": 2.50,
  739. "__undefined__/__undefined__/__undefined__/Storage/cluster1/Disk/Kubernetes/gcp-disk2/disk2": 1.50,
  740. "GCP/__undefined__/__undefined__/Management/cluster1/ClusterManagement/Kubernetes/__undefined__/__undefined__": 3.00,
  741. "__undefined__/__undefined__/__undefined__/Compute/cluster2/Node/Kubernetes/gcp-node4/node4": 11.00,
  742. "__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk3/disk3": 2.50,
  743. "__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4": 1.50,
  744. "GCP/__undefined__/__undefined__/Management/cluster2/ClusterManagement/Kubernetes/__undefined__/__undefined__": 0.00,
  745. "__undefined__/__undefined__/__undefined__/Compute/cluster3/Node/Kubernetes/aws-node5/node5": 19.00,
  746. }, nil)
  747. // 1e aggregateBy []string=["label:test"]
  748. as = generateAssetSet(startYesterday)
  749. err = as.AggregateBy([]string{"label:test"}, nil)
  750. if err != nil {
  751. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  752. }
  753. assertAssetSet(t, as, "1e", window, map[string]float64{
  754. "__undefined__": 53.00,
  755. "test=test": 7.00,
  756. }, nil)
  757. // 2 Multi-aggregation
  758. // 2a []AssetProperty=[Cluster,Type]
  759. as = generateAssetSet(startYesterday)
  760. err = as.AggregateBy([]string{string(AssetClusterProp), string(AssetTypeProp)}, nil)
  761. if err != nil {
  762. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  763. }
  764. assertAssetSet(t, as, "2a", window, map[string]float64{
  765. "cluster1/Node": 19.0,
  766. "cluster1/Disk": 4.0,
  767. "cluster1/ClusterManagement": 3.0,
  768. "cluster2/Node": 11.0,
  769. "cluster2/Disk": 4.0,
  770. "cluster2/ClusterManagement": 0.0,
  771. "cluster3/Node": 19.0,
  772. }, nil)
  773. // 3 Share resources
  774. // 3a Shared hourly cost > 0.0
  775. as = generateAssetSet(startYesterday)
  776. err = as.AggregateBy([]string{string(AssetTypeProp)}, &AssetAggregationOptions{
  777. SharedHourlyCosts: map[string]float64{"shared1": 0.5},
  778. })
  779. if err != nil {
  780. t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
  781. }
  782. assertAssetSet(t, as, "1a", window, map[string]float64{
  783. "Node": 49.0,
  784. "Disk": 8.0,
  785. "ClusterManagement": 3.0,
  786. "Shared": 12.0,
  787. }, nil)
  788. }
  789. func TestAssetSet_FindMatch(t *testing.T) {
  790. endYesterday := time.Now().UTC().Truncate(day)
  791. startYesterday := endYesterday.Add(-day)
  792. s, e := startYesterday, endYesterday
  793. w := NewWindow(&s, &e)
  794. var query, match Asset
  795. var as *AssetSet
  796. var err error
  797. // Assert success of a simple match of Type and ProviderID
  798. as = generateAssetSet(startYesterday)
  799. query = NewNode("", "", "gcp-node3", s, e, w)
  800. match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)})
  801. if err != nil {
  802. t.Fatalf("AssetSet.FindMatch: unexpected error: %s", err)
  803. }
  804. // Assert error of a simple non-match of Type and ProviderID
  805. as = generateAssetSet(startYesterday)
  806. query = NewNode("", "", "aws-node3", s, e, w)
  807. match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)})
  808. if err == nil {
  809. t.Fatalf("AssetSet.FindMatch: expected error (no match); found %s", match)
  810. }
  811. // Assert error of matching ProviderID, but not Type
  812. as = generateAssetSet(startYesterday)
  813. query = NewCloud(ComputeCategory, "gcp-node3", s, e, w)
  814. match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)})
  815. if err == nil {
  816. t.Fatalf("AssetSet.FindMatch: expected error (no match); found %s", match)
  817. }
  818. }
  819. func TestAssetSetRange_Accumulate(t *testing.T) {
  820. endYesterday := time.Now().UTC().Truncate(day)
  821. startYesterday := endYesterday.Add(-day)
  822. startD2 := startYesterday
  823. startD1 := startD2.Add(-day)
  824. startD0 := startD1.Add(-day)
  825. window := NewWindow(&startD0, &endYesterday)
  826. var asr *AssetSetRange
  827. var as *AssetSet
  828. var err error
  829. asr = NewAssetSetRange(
  830. generateAssetSet(startD0),
  831. generateAssetSet(startD1),
  832. generateAssetSet(startD2),
  833. )
  834. err = asr.AggregateBy(nil, nil)
  835. as, err = asr.Accumulate()
  836. if err != nil {
  837. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  838. }
  839. assertAssetSet(t, as, "1a", window, map[string]float64{
  840. "__undefined__/__undefined__/__undefined__/Compute/cluster1/Node/Kubernetes/gcp-node1/node1": 21.00,
  841. "__undefined__/__undefined__/__undefined__/Compute/cluster1/Node/Kubernetes/gcp-node2/node2": 16.50,
  842. "__undefined__/__undefined__/__undefined__/Compute/cluster1/Node/Kubernetes/gcp-node3/node3": 19.50,
  843. "__undefined__/__undefined__/__undefined__/Storage/cluster1/Disk/Kubernetes/gcp-disk1/disk1": 7.50,
  844. "__undefined__/__undefined__/__undefined__/Storage/cluster1/Disk/Kubernetes/gcp-disk2/disk2": 4.50,
  845. "GCP/__undefined__/__undefined__/Management/cluster1/ClusterManagement/Kubernetes/__undefined__/__undefined__": 9.00,
  846. "__undefined__/__undefined__/__undefined__/Compute/cluster2/Node/Kubernetes/gcp-node4/node4": 33.00,
  847. "__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk3/disk3": 7.50,
  848. "__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4": 4.50,
  849. "GCP/__undefined__/__undefined__/Management/cluster2/ClusterManagement/Kubernetes/__undefined__/__undefined__": 0.00,
  850. "__undefined__/__undefined__/__undefined__/Compute/cluster3/Node/Kubernetes/aws-node5/node5": 57.00,
  851. }, nil)
  852. asr = NewAssetSetRange(
  853. generateAssetSet(startD0),
  854. generateAssetSet(startD1),
  855. generateAssetSet(startD2),
  856. )
  857. err = asr.AggregateBy([]string{}, nil)
  858. as, err = asr.Accumulate()
  859. if err != nil {
  860. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  861. }
  862. assertAssetSet(t, as, "1b", window, map[string]float64{
  863. "": 180.00,
  864. }, nil)
  865. asr = NewAssetSetRange(
  866. generateAssetSet(startD0),
  867. generateAssetSet(startD1),
  868. generateAssetSet(startD2),
  869. )
  870. err = asr.AggregateBy([]string{string(AssetTypeProp)}, nil)
  871. if err != nil {
  872. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  873. }
  874. as, err = asr.Accumulate()
  875. if err != nil {
  876. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  877. }
  878. assertAssetSet(t, as, "1c", window, map[string]float64{
  879. "Node": 147.0,
  880. "Disk": 24.0,
  881. "ClusterManagement": 9.0,
  882. }, nil)
  883. asr = NewAssetSetRange(
  884. generateAssetSet(startD0),
  885. generateAssetSet(startD1),
  886. generateAssetSet(startD2),
  887. )
  888. err = asr.AggregateBy([]string{string(AssetClusterProp)}, nil)
  889. if err != nil {
  890. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  891. }
  892. as, err = asr.Accumulate()
  893. if err != nil {
  894. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  895. }
  896. assertAssetSet(t, as, "1c", window, map[string]float64{
  897. "cluster1": 78.0,
  898. "cluster2": 45.0,
  899. "cluster3": 57.0,
  900. }, nil)
  901. // Accumulation with aggregation should work, even when the first AssetSet
  902. // is empty (this was previously an issue)
  903. asr = NewAssetSetRange(
  904. NewAssetSet(startD0, startD1),
  905. generateAssetSet(startD1),
  906. generateAssetSet(startD2),
  907. )
  908. err = asr.AggregateBy([]string{string(AssetTypeProp)}, nil)
  909. as, err = asr.Accumulate()
  910. if err != nil {
  911. t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
  912. }
  913. assertAssetSet(t, as, "1d", window, map[string]float64{
  914. "Node": 98.00,
  915. "Disk": 16.00,
  916. "ClusterManagement": 6.00,
  917. }, nil)
  918. }
  919. func TestAssetToExternalAllocation(t *testing.T) {
  920. var asset Asset
  921. var alloc *Allocation
  922. var err error
  923. alloc, err = AssetToExternalAllocation(asset, []string{"namespace"}, map[string]string{})
  924. if err == nil {
  925. t.Fatalf("expected error due to nil asset")
  926. }
  927. // Consider this Asset:
  928. // Cloud {
  929. // TotalCost: 10.00,
  930. // Labels{
  931. // "kubernetes_namespace":"monitoring",
  932. // "env":"prod"
  933. // }
  934. // }
  935. cloud := NewCloud(ComputeCategory, "abc123", start1, start2, windows[0])
  936. cloud.SetLabels(map[string]string{
  937. "namespace": "monitoring",
  938. "env": "prod",
  939. "product": "cost-analyzer",
  940. })
  941. cloud.Cost = 10.00
  942. asset = cloud
  943. alloc, err = AssetToExternalAllocation(asset, []string{"namespace"}, map[string]string{})
  944. if err != nil {
  945. t.Fatalf("expected to not error")
  946. }
  947. alloc, err = AssetToExternalAllocation(asset, nil, map[string]string{})
  948. if err == nil {
  949. t.Fatalf("expected error due to nil aggregateBy")
  950. }
  951. // Given the following parameters, we expect to return:
  952. //
  953. // 1) single-prop full match
  954. // aggregateBy = ["namespace"]
  955. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  956. // => Allocation{Name: "monitoring", ExternalCost: 10.00, TotalCost: 10.00}, nil
  957. //
  958. // 2) multi-prop full match
  959. // aggregateBy = ["namespace", "label:env"]
  960. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  961. // => Allocation{Name: "monitoring/env=prod", ExternalCost: 10.00, TotalCost: 10.00}, nil
  962. //
  963. // 3) multi-prop partial match
  964. // aggregateBy = ["namespace", "label:foo"]
  965. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  966. // => Allocation{Name: "monitoring/__unallocated__", ExternalCost: 10.00, TotalCost: 10.00}, nil
  967. //
  968. // 4) no match
  969. // aggregateBy = ["cluster"]
  970. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  971. // => nil, err
  972. // 1) single-prop full match
  973. alloc, err = AssetToExternalAllocation(asset, []string{"namespace"}, map[string]string{})
  974. if err != nil {
  975. t.Fatalf("unexpected error: %s", err)
  976. }
  977. if alloc.Name != "monitoring/__external__" {
  978. t.Fatalf("expected external allocation with name '%s'; got '%s'", "monitoring/__external__", alloc.Name)
  979. }
  980. if ns := alloc.Properties.Namespace; ns != "monitoring" {
  981. t.Fatalf("expected external allocation with AllocationProperties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
  982. }
  983. if alloc.ExternalCost != 10.00 {
  984. t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)
  985. }
  986. if alloc.TotalCost() != 10.00 {
  987. t.Fatalf("expected external allocation with TotalCost %f; got %f", 10.00, alloc.TotalCost())
  988. }
  989. // 2) multi-prop full match
  990. alloc, err = AssetToExternalAllocation(asset, []string{"namespace", "label:env"}, map[string]string{})
  991. if err != nil {
  992. t.Fatalf("unexpected error: %s", err)
  993. }
  994. if alloc.Name != "monitoring/env=prod/__external__" {
  995. t.Fatalf("expected external allocation with name '%s'; got '%s'", "monitoring/env=prod/__external__", alloc.Name)
  996. }
  997. if ns := alloc.Properties.Namespace; ns != "monitoring" {
  998. t.Fatalf("expected external allocation with AllocationProperties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
  999. }
  1000. if ls := alloc.Properties.Labels; len(ls) == 0 || ls["env"] != "prod" {
  1001. t.Fatalf("expected external allocation with AllocationProperties.Labels[\"env\"] '%s'; got '%s' (%s)", "prod", ls["env"], err)
  1002. }
  1003. if alloc.ExternalCost != 10.00 {
  1004. t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)
  1005. }
  1006. if alloc.TotalCost() != 10.00 {
  1007. t.Fatalf("expected external allocation with TotalCost %f; got %f", 10.00, alloc.TotalCost())
  1008. }
  1009. // 3) multi-prop partial match
  1010. alloc, err = AssetToExternalAllocation(asset, []string{"namespace", "label:foo"}, map[string]string{})
  1011. if err != nil {
  1012. t.Fatalf("unexpected error: %s", err)
  1013. }
  1014. if alloc.Name != "monitoring/__unallocated__/__external__" {
  1015. t.Fatalf("expected external allocation with name '%s'; got '%s'", "monitoring/__unallocated__/__external__", alloc.Name)
  1016. }
  1017. if ns := alloc.Properties.Namespace; ns != "monitoring" {
  1018. t.Fatalf("expected external allocation with AllocationProperties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
  1019. }
  1020. if alloc.ExternalCost != 10.00 {
  1021. t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)
  1022. }
  1023. if alloc.TotalCost() != 10.00 {
  1024. t.Fatalf("expected external allocation with TotalCost %f; got %f", 10.00, alloc.TotalCost())
  1025. }
  1026. // 3) no match
  1027. alloc, err = AssetToExternalAllocation(asset, []string{"cluster"}, map[string]string{})
  1028. if err == nil {
  1029. t.Fatalf("expected 'no match' error")
  1030. }
  1031. alloc, err = AssetToExternalAllocation(asset, []string{"namespace", "label:app"}, map[string]string{"app": "product"})
  1032. if alloc.ExternalCost != 10.00 {
  1033. t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)
  1034. }
  1035. if alloc.TotalCost() != 10.00 {
  1036. t.Fatalf("expected external allocation with TotalCost %f; got %f", 10.00, alloc.TotalCost())
  1037. }
  1038. }
  1039. // TODO merge conflict had this:
  1040. // as.Each(func(key string, a Asset) {
  1041. // if exp, ok := exps[key]; ok {
  1042. // if math.Round(a.TotalCost()*100) != math.Round(exp*100) {
  1043. // t.Fatalf("AssetSet.AggregateBy[%s]: key %s expected total cost %.2f, actual %.2f", msg, key, exp, a.TotalCost())
  1044. // }
  1045. // if !a.Window().Equal(window) {
  1046. // t.Fatalf("AssetSet.AggregateBy[%s]: key %s expected window %s, actual %s", msg, key, window, as.Window)
  1047. // }
  1048. // } else {
  1049. // t.Fatalf("AssetSet.AggregateBy[%s]: unexpected asset: %s", msg, key)
  1050. // }
  1051. // })
  1052. // }
  1053. // // generateAssetSet generates the following topology:
  1054. // //
  1055. // // | Asset | Cost | Adj |
  1056. // // +------------------------------+------+------+
  1057. // // cluster1:
  1058. // // node1: 6.00 1.00
  1059. // // node2: 4.00 1.50
  1060. // // node3: 7.00 -0.50
  1061. // // disk1: 2.50 0.00
  1062. // // disk2: 1.50 0.00
  1063. // // clusterManagement1: 3.00 0.00
  1064. // // +------------------------------+------+------+
  1065. // // cluster1 subtotal 24.00 2.00
  1066. // // +------------------------------+------+------+
  1067. // // cluster2:
  1068. // // node4: 12.00 -1.00
  1069. // // disk3: 2.50 0.00
  1070. // // disk4: 1.50 0.00
  1071. // // clusterManagement2: 0.00 0.00
  1072. // // +------------------------------+------+------+
  1073. // // cluster2 subtotal 16.00 -1.00
  1074. // // +------------------------------+------+------+
  1075. // // cluster3:
  1076. // // node5: 17.00 2.00
  1077. // // +------------------------------+------+------+
  1078. // // cluster3 subtotal 17.00 2.00
  1079. // // +------------------------------+------+------+
  1080. // // total 57.00 3.00
  1081. // // +------------------------------+------+------+
  1082. // func generateAssetSet(start time.Time) *AssetSet {
  1083. // end := start.Add(day)
  1084. // window := NewWindow(&start, &end)
  1085. // hours := window.Duration().Hours()
  1086. // node1 := NewNode("node1", "cluster1", "gcp-node1", *window.Clone().start, *window.Clone().end, window.Clone())
  1087. // node1.CPUCost = 4.0
  1088. // node1.RAMCost = 4.0
  1089. // node1.GPUCost = 2.0
  1090. // node1.Discount = 0.5
  1091. // node1.CPUCoreHours = 2.0 * hours
  1092. // node1.RAMByteHours = 4.0 * gb * hours
  1093. // node1.SetAdjustment(1.0)
  1094. // node1.SetLabels(map[string]string{"test": "test"})
  1095. // node2 := NewNode("node2", "cluster1", "gcp-node2", *window.Clone().start, *window.Clone().end, window.Clone())
  1096. // node2.CPUCost = 4.0
  1097. // node2.RAMCost = 4.0
  1098. // node2.GPUCost = 0.0
  1099. // node2.Discount = 0.5
  1100. // node2.CPUCoreHours = 2.0 * hours
  1101. // node2.RAMByteHours = 4.0 * gb * hours
  1102. // node2.SetAdjustment(1.5)
  1103. // node3 := NewNode("node3", "cluster1", "gcp-node3", *window.Clone().start, *window.Clone().end, window.Clone())
  1104. // node3.CPUCost = 4.0
  1105. // node3.RAMCost = 4.0
  1106. // node3.GPUCost = 3.0
  1107. // node3.Discount = 0.5
  1108. // node3.CPUCoreHours = 2.0 * hours
  1109. // node3.RAMByteHours = 4.0 * gb * hours
  1110. // node3.SetAdjustment(-0.5)
  1111. // node4 := NewNode("node4", "cluster2", "gcp-node4", *window.Clone().start, *window.Clone().end, window.Clone())
  1112. // node4.CPUCost = 10.0
  1113. // node4.RAMCost = 6.0
  1114. // node4.GPUCost = 0.0
  1115. // node4.Discount = 0.25
  1116. // node4.CPUCoreHours = 4.0 * hours
  1117. // node4.RAMByteHours = 12.0 * gb * hours
  1118. // node4.SetAdjustment(-1.0)
  1119. // node5 := NewNode("node5", "cluster3", "aws-node5", *window.Clone().start, *window.Clone().end, window.Clone())
  1120. // node5.CPUCost = 10.0
  1121. // node5.RAMCost = 7.0
  1122. // node5.GPUCost = 0.0
  1123. // node5.Discount = 0.0
  1124. // node5.CPUCoreHours = 8.0 * hours
  1125. // node5.RAMByteHours = 24.0 * gb * hours
  1126. // node5.SetAdjustment(2.0)
  1127. // disk1 := NewDisk("disk1", "cluster1", "gcp-disk1", *window.Clone().start, *window.Clone().end, window.Clone())
  1128. // disk1.Cost = 2.5
  1129. // disk1.ByteHours = 100 * gb * hours
  1130. // disk2 := NewDisk("disk2", "cluster1", "gcp-disk2", *window.Clone().start, *window.Clone().end, window.Clone())
  1131. // disk2.Cost = 1.5
  1132. // disk2.ByteHours = 60 * gb * hours
  1133. // disk3 := NewDisk("disk3", "cluster2", "gcp-disk3", *window.Clone().start, *window.Clone().end, window.Clone())
  1134. // disk3.Cost = 2.5
  1135. // disk3.ByteHours = 100 * gb * hours
  1136. // disk4 := NewDisk("disk4", "cluster2", "gcp-disk4", *window.Clone().start, *window.Clone().end, window.Clone())
  1137. // disk4.Cost = 1.5
  1138. // disk4.ByteHours = 100 * gb * hours
  1139. // cm1 := NewClusterManagement("gcp", "cluster1", window.Clone())
  1140. // cm1.Cost = 3.0