node_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. package kubemodel
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. "github.com/opencost/opencost/core/pkg/model/kubemodel"
  8. "github.com/opencost/opencost/core/pkg/source"
  9. )
  10. func TestComputeNodes(t *testing.T) {
  11. start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
  12. end := start.Add(time.Hour)
  13. tests := []struct {
  14. name string
  15. overrides map[string]any
  16. want map[string]*kubemodel.Node
  17. }{
  18. {
  19. name: "no data returns empty node map",
  20. overrides: map[string]any{},
  21. want: map[string]*kubemodel.Node{},
  22. },
  23. {
  24. name: "basic node info and uptime",
  25. overrides: map[string]any{
  26. source.QueryNodeInfo: []*source.NodeInfoResult{
  27. {UID: "node-1", Node: "node-a", ProviderID: "aws:///us-east-1a/i-abc"},
  28. {UID: "node-2", Node: "node-b", ProviderID: "aws:///us-east-1b/i-def"},
  29. },
  30. source.QueryNodeUptime: []*source.UptimeResult{
  31. {UID: "node-1", First: start, Last: end},
  32. {UID: "node-2", First: start, Last: end},
  33. },
  34. },
  35. want: map[string]*kubemodel.Node{
  36. "node-1": {
  37. UID: "node-1", Name: "node-a", ProviderID: "aws:///us-east-1a/i-abc",
  38. Start: start,
  39. End: end,
  40. ResourceCapacities: kubemodel.ResourceQuantities{},
  41. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  42. },
  43. "node-2": {
  44. UID: "node-2", Name: "node-b", ProviderID: "aws:///us-east-1b/i-def",
  45. Start: start,
  46. End: end,
  47. ResourceCapacities: kubemodel.ResourceQuantities{},
  48. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  49. },
  50. },
  51. },
  52. {
  53. name: "node without uptime is not registered",
  54. overrides: map[string]any{
  55. source.QueryNodeInfo: []*source.NodeInfoResult{
  56. {UID: "node-1", Node: "node-a"},
  57. },
  58. // QueryNodeUptime intentionally absent
  59. },
  60. want: map[string]*kubemodel.Node{},
  61. },
  62. {
  63. name: "labels are attached to node",
  64. overrides: map[string]any{
  65. source.QueryNodeInfo: []*source.NodeInfoResult{
  66. {UID: "node-1", Node: "node-a"},
  67. },
  68. source.QueryNodeUptime: []*source.UptimeResult{
  69. {UID: "node-1", First: start, Last: end},
  70. },
  71. source.QueryNodeLabels: []*source.NodeLabelsResult{
  72. {UID: "node-1", Labels: map[string]string{"zone": "us-east-1a", "role": "worker"}},
  73. },
  74. },
  75. want: map[string]*kubemodel.Node{
  76. "node-1": {
  77. UID: "node-1", Name: "node-a",
  78. Start: start,
  79. End: end,
  80. Labels: map[string]string{"zone": "us-east-1a", "role": "worker"},
  81. ResourceCapacities: kubemodel.ResourceQuantities{},
  82. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  83. },
  84. },
  85. },
  86. {
  87. name: "resource capacities and allocatable are populated",
  88. overrides: map[string]any{
  89. source.QueryNodeInfo: []*source.NodeInfoResult{
  90. {UID: "node-1", Node: "node-a"},
  91. },
  92. source.QueryNodeUptime: []*source.UptimeResult{
  93. {UID: "node-1", First: start, Last: end},
  94. },
  95. source.QueryNodeResourceCapacities: []*source.ResourceResult{
  96. {UID: "node-1", Resource: "cpu", Unit: "cores", Value: 4.0},
  97. {UID: "node-1", Resource: "memory", Unit: "bytes", Value: 8 * 1024 * 1024 * 1024},
  98. },
  99. source.QueryNodeResourcesAllocatable: []*source.ResourceResult{
  100. {UID: "node-1", Resource: "cpu", Unit: "cores", Value: 3.9},
  101. {UID: "node-1", Resource: "memory", Unit: "bytes", Value: 7 * 1024 * 1024 * 1024},
  102. },
  103. },
  104. want: map[string]*kubemodel.Node{
  105. "node-1": {
  106. UID: "node-1", Name: "node-a",
  107. Start: start,
  108. End: end,
  109. ResourceCapacities: kubemodel.ResourceQuantities{
  110. kubemodel.ResourceCPU: {
  111. Resource: kubemodel.ResourceCPU,
  112. Unit: kubemodel.UnitCore,
  113. Values: kubemodel.Stats{kubemodel.StatAvg: 4.0},
  114. },
  115. kubemodel.ResourceMemory: {
  116. Resource: kubemodel.ResourceMemory,
  117. Unit: kubemodel.UnitByte,
  118. Values: kubemodel.Stats{kubemodel.StatAvg: 8 * 1024 * 1024 * 1024},
  119. },
  120. },
  121. ResourcesAllocatable: kubemodel.ResourceQuantities{
  122. kubemodel.ResourceCPU: {
  123. Resource: kubemodel.ResourceCPU,
  124. Unit: kubemodel.UnitCore,
  125. Values: kubemodel.Stats{kubemodel.StatAvg: 3.9},
  126. },
  127. kubemodel.ResourceMemory: {
  128. Resource: kubemodel.ResourceMemory,
  129. Unit: kubemodel.UnitByte,
  130. Values: kubemodel.Stats{kubemodel.StatAvg: 7 * 1024 * 1024 * 1024},
  131. },
  132. },
  133. },
  134. },
  135. },
  136. {
  137. name: "local storage bytes are populated",
  138. overrides: map[string]any{
  139. source.QueryNodeInfo: []*source.NodeInfoResult{
  140. {UID: "node-1", Node: "node-a"},
  141. },
  142. source.QueryNodeUptime: []*source.UptimeResult{
  143. {UID: "node-1", First: start, Last: end},
  144. },
  145. source.QueryKMLocalStorageBytes: []*source.UIDValueResult{
  146. {UID: "node-1", Value: 500 * 1024 * 1024 * 1024},
  147. },
  148. source.QueryKMLocalStorageUsedAvg: []*source.NodeUIDValueResult{
  149. {UID: "node-1", Value: 100 * 1024 * 1024 * 1024},
  150. },
  151. source.QueryKMLocalStorageUsedMax: []*source.NodeUIDValueResult{
  152. {UID: "node-1", Value: 200 * 1024 * 1024 * 1024},
  153. },
  154. },
  155. want: map[string]*kubemodel.Node{
  156. "node-1": {
  157. UID: "node-1", Name: "node-a",
  158. Start: start,
  159. End: end,
  160. FileSystem: kubemodel.FileSystem{
  161. CapacityBytes: 500 * 1024 * 1024 * 1024,
  162. UsageByteAvg: 100 * 1024 * 1024 * 1024,
  163. UsageByteMax: 200 * 1024 * 1024 * 1024,
  164. },
  165. ResourceCapacities: kubemodel.ResourceQuantities{},
  166. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  167. },
  168. },
  169. },
  170. {
  171. name: "uptime for unknown node is ignored",
  172. overrides: map[string]any{
  173. source.QueryNodeInfo: []*source.NodeInfoResult{
  174. {UID: "node-1", Node: "node-a"},
  175. },
  176. source.QueryNodeUptime: []*source.UptimeResult{
  177. {UID: "node-1", First: start, Last: end},
  178. {UID: "unknown-node", First: start, Last: end},
  179. },
  180. },
  181. want: map[string]*kubemodel.Node{
  182. "node-1": {
  183. UID: "node-1", Name: "node-a",
  184. Start: start,
  185. End: end,
  186. ResourceCapacities: kubemodel.ResourceQuantities{},
  187. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  188. },
  189. },
  190. },
  191. {
  192. name: "local storage for unknown node is ignored",
  193. overrides: map[string]any{
  194. source.QueryNodeInfo: []*source.NodeInfoResult{
  195. {UID: "node-1", Node: "node-a"},
  196. },
  197. source.QueryNodeUptime: []*source.UptimeResult{
  198. {UID: "node-1", First: start, Last: end},
  199. },
  200. source.QueryKMLocalStorageBytes: []*source.UIDValueResult{
  201. {UID: "unknown-node", Value: 999},
  202. },
  203. },
  204. want: map[string]*kubemodel.Node{
  205. "node-1": {
  206. UID: "node-1", Name: "node-a",
  207. Start: start,
  208. End: end,
  209. ResourceCapacities: kubemodel.ResourceQuantities{},
  210. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  211. },
  212. },
  213. },
  214. {
  215. name: "resource capacities for unknown node are ignored",
  216. overrides: map[string]any{
  217. source.QueryNodeInfo: []*source.NodeInfoResult{
  218. {UID: "node-1", Node: "node-a"},
  219. },
  220. source.QueryNodeUptime: []*source.UptimeResult{
  221. {UID: "node-1", First: start, Last: end},
  222. },
  223. source.QueryNodeResourceCapacities: []*source.ResourceResult{
  224. {UID: "unknown-node", Resource: "cpu", Unit: "cores", Value: 4.0},
  225. },
  226. },
  227. want: map[string]*kubemodel.Node{
  228. "node-1": {
  229. UID: "node-1", Name: "node-a",
  230. Start: start,
  231. End: end,
  232. ResourceCapacities: kubemodel.ResourceQuantities{},
  233. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  234. },
  235. },
  236. },
  237. {
  238. name: "cpu usage data is populated via resource capacities",
  239. overrides: map[string]any{
  240. source.QueryNodeInfo: []*source.NodeInfoResult{
  241. {UID: "node-1", Node: "node-a"},
  242. {UID: "node-2", Node: "node-b"},
  243. },
  244. source.QueryNodeUptime: []*source.UptimeResult{
  245. {UID: "node-1", First: start, Last: end},
  246. {UID: "node-2", First: start, Last: end},
  247. },
  248. source.QueryKMLocalStorageUsedAvg: []*source.NodeUIDValueResult{
  249. {UID: "node-1", Value: 50 * 1024 * 1024 * 1024},
  250. },
  251. source.QueryKMLocalStorageUsedMax: []*source.NodeUIDValueResult{
  252. {UID: "node-2", Value: 75 * 1024 * 1024 * 1024},
  253. },
  254. },
  255. want: map[string]*kubemodel.Node{
  256. "node-1": {
  257. UID: "node-1", Name: "node-a",
  258. Start: start, End: end,
  259. FileSystem: kubemodel.FileSystem{UsageByteAvg: 50 * 1024 * 1024 * 1024},
  260. ResourceCapacities: kubemodel.ResourceQuantities{},
  261. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  262. },
  263. "node-2": {
  264. UID: "node-2", Name: "node-b",
  265. Start: start, End: end,
  266. FileSystem: kubemodel.FileSystem{UsageByteMax: 75 * 1024 * 1024 * 1024},
  267. ResourceCapacities: kubemodel.ResourceQuantities{},
  268. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  269. },
  270. },
  271. },
  272. {
  273. name: "gpu count via resource capacities",
  274. overrides: map[string]any{
  275. source.QueryNodeInfo: []*source.NodeInfoResult{
  276. {UID: "node-1", Node: "gpu-node"},
  277. },
  278. source.QueryNodeUptime: []*source.UptimeResult{
  279. {UID: "node-1", First: start, Last: end},
  280. },
  281. source.QueryNodeResourceCapacities: []*source.ResourceResult{
  282. {UID: "node-1", Resource: "nvidia.com/gpu", Unit: "count", Value: 8},
  283. },
  284. },
  285. want: map[string]*kubemodel.Node{
  286. "node-1": {
  287. UID: "node-1", Name: "gpu-node",
  288. Start: start, End: end,
  289. ResourceCapacities: kubemodel.ResourceQuantities{
  290. kubemodel.ResourceNvidia: {
  291. Resource: kubemodel.ResourceNvidia,
  292. Unit: "count",
  293. Values: kubemodel.Stats{kubemodel.StatAvg: 8},
  294. },
  295. },
  296. ResourcesAllocatable: kubemodel.ResourceQuantities{},
  297. },
  298. },
  299. },
  300. }
  301. for _, tt := range tests {
  302. t.Run(tt.name, func(t *testing.T) {
  303. ds := source.NewMockOpenCostDataSource()
  304. ds.ResolutionValue = 5 * time.Minute
  305. seedCluster(ds, start, end)
  306. for method, result := range tt.overrides {
  307. ds.Querier.SetOverride(method, result)
  308. }
  309. km, err := NewKubeModel(testClusterUID, false, ds)
  310. require.NoError(t, err)
  311. kms, err := km.ComputeKubeModelSet(start, end)
  312. require.NoError(t, err)
  313. assert.Equal(t, tt.want, kms.Nodes)
  314. })
  315. }
  316. }