cloud_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package test
  2. import (
  3. "fmt"
  4. "math"
  5. "os"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/kubecost/cost-model/pkg/cloud"
  10. "github.com/kubecost/cost-model/pkg/clustercache"
  11. "github.com/kubecost/cost-model/pkg/costmodel"
  12. "github.com/kubecost/cost-model/pkg/costmodel/clusters"
  13. appsv1 "k8s.io/api/apps/v1"
  14. v1 "k8s.io/api/core/v1"
  15. )
  16. const (
  17. providerIDMap = "spec.providerID"
  18. nameMap = "metadata.name"
  19. labelMapFoo = "metadata.labels.foo"
  20. )
  21. func TestRegionValueFromMapField(t *testing.T) {
  22. wantRegion := "useast"
  23. wantpid := strings.ToLower("/subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0")
  24. providerIDWant := wantRegion + "," + wantpid
  25. n := &v1.Node{}
  26. n.Spec.ProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0"
  27. n.Labels = make(map[string]string)
  28. n.Labels[v1.LabelZoneRegion] = wantRegion
  29. got := cloud.NodeValueFromMapField(providerIDMap, n, true)
  30. if got != providerIDWant {
  31. t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant, got)
  32. }
  33. }
  34. func TestTransformedValueFromMapField(t *testing.T) {
  35. providerIDWant := "i-05445591e0d182d42"
  36. n := &v1.Node{}
  37. n.Spec.ProviderID = "aws:///us-east-1a/i-05445591e0d182d42"
  38. got := cloud.NodeValueFromMapField(providerIDMap, n, false)
  39. if got != providerIDWant {
  40. t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant, got)
  41. }
  42. providerIDWant2 := strings.ToLower("/subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0")
  43. n2 := &v1.Node{}
  44. n2.Spec.ProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0"
  45. got2 := cloud.NodeValueFromMapField(providerIDMap, n2, false)
  46. if got2 != providerIDWant2 {
  47. t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant2, got2)
  48. }
  49. providerIDWant3 := strings.ToLower("/subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/mc_testspot_testspot_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-19213364-vmss/virtualMachines/0")
  50. n3 := &v1.Node{}
  51. n3.Spec.ProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/mc_testspot_testspot_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-19213364-vmss/virtualMachines/0"
  52. got3 := cloud.NodeValueFromMapField(providerIDMap, n3, false)
  53. if got3 != providerIDWant3 {
  54. t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant3, got3)
  55. }
  56. }
  57. func TestNodeValueFromMapField(t *testing.T) {
  58. providerIDWant := "providerid"
  59. nameWant := "gke-standard-cluster-1-pool-1-91dc432d-cg69"
  60. labelFooWant := "labelfoo"
  61. n := &v1.Node{}
  62. n.Spec.ProviderID = providerIDWant
  63. n.Name = nameWant
  64. n.Labels = make(map[string]string)
  65. n.Labels["foo"] = labelFooWant
  66. got := cloud.NodeValueFromMapField(providerIDMap, n, false)
  67. if got != providerIDWant {
  68. t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant, got)
  69. }
  70. got = cloud.NodeValueFromMapField(nameMap, n, false)
  71. if got != nameWant {
  72. t.Errorf("Assert on '%s' want '%s' got '%s'", nameMap, nameWant, got)
  73. }
  74. got = cloud.NodeValueFromMapField(labelMapFoo, n, false)
  75. if got != labelFooWant {
  76. t.Errorf("Assert on '%s' want '%s' got '%s'", labelMapFoo, labelFooWant, got)
  77. }
  78. }
  79. func TestPVPriceFromCSV(t *testing.T) {
  80. nameWant := "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d"
  81. pv := &v1.PersistentVolume{}
  82. pv.Name = nameWant
  83. wantPrice := "0.1337"
  84. c := &cloud.CSVProvider{
  85. CSVLocation: "../configs/pricing_schema_pv.csv",
  86. CustomProvider: &cloud.CustomProvider{
  87. Config: cloud.NewProviderConfig("../configs/default.json"),
  88. },
  89. }
  90. c.DownloadPricingData()
  91. k := c.GetPVKey(pv, make(map[string]string), "")
  92. resPV, err := c.PVPricing(k)
  93. if err != nil {
  94. t.Errorf("Error in NodePricing: %s", err.Error())
  95. } else {
  96. gotPrice := resPV.Cost
  97. if gotPrice != wantPrice {
  98. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  99. }
  100. }
  101. }
  102. func TestNodePriceFromCSV(t *testing.T) {
  103. providerIDWant := "providerid"
  104. nameWant := "gke-standard-cluster-1-pool-1-91dc432d-cg69"
  105. labelFooWant := "labelfoo"
  106. n := &v1.Node{}
  107. n.Spec.ProviderID = providerIDWant
  108. n.Name = nameWant
  109. n.Labels = make(map[string]string)
  110. n.Labels["foo"] = labelFooWant
  111. wantPrice := "0.1337"
  112. c := &cloud.CSVProvider{
  113. CSVLocation: "../configs/pricing_schema.csv",
  114. CustomProvider: &cloud.CustomProvider{
  115. Config: cloud.NewProviderConfig("../configs/default.json"),
  116. },
  117. }
  118. c.DownloadPricingData()
  119. k := c.GetKey(n.Labels, n)
  120. resN, err := c.NodePricing(k)
  121. if err != nil {
  122. t.Errorf("Error in NodePricing: %s", err.Error())
  123. } else {
  124. gotPrice := resN.Cost
  125. if gotPrice != wantPrice {
  126. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  127. }
  128. }
  129. unknownN := &v1.Node{}
  130. unknownN.Spec.ProviderID = providerIDWant
  131. unknownN.Name = "unknownname"
  132. unknownN.Labels = make(map[string]string)
  133. unknownN.Labels["foo"] = labelFooWant
  134. unknownN.Labels["topology.kubernetes.io/region"] = "fakeregion"
  135. k2 := c.GetKey(unknownN.Labels, unknownN)
  136. resN2, _ := c.NodePricing(k2)
  137. if resN2 != nil {
  138. t.Errorf("CSV provider should return nil on missing node")
  139. }
  140. c2 := &cloud.CSVProvider{
  141. CSVLocation: "../configs/fake.csv",
  142. CustomProvider: &cloud.CustomProvider{
  143. Config: cloud.NewProviderConfig("../configs/default.json"),
  144. },
  145. }
  146. k3 := c.GetKey(n.Labels, n)
  147. resN3, _ := c2.NodePricing(k3)
  148. if resN3 != nil {
  149. t.Errorf("CSV provider should return nil on missing csv")
  150. }
  151. }
  152. func TestNodePriceFromCSVWithRegion(t *testing.T) {
  153. providerIDWant := "gke-standard-cluster-1-pool-1-91dc432d-cg69"
  154. nameWant := "foo"
  155. labelFooWant := "labelfoo"
  156. n := &v1.Node{}
  157. n.Spec.ProviderID = providerIDWant
  158. n.Name = nameWant
  159. n.Labels = make(map[string]string)
  160. n.Labels["foo"] = labelFooWant
  161. n.Labels[v1.LabelZoneRegion] = "regionone"
  162. wantPrice := "0.1337"
  163. n2 := &v1.Node{}
  164. n2.Spec.ProviderID = providerIDWant
  165. n2.Name = nameWant
  166. n2.Labels = make(map[string]string)
  167. n2.Labels["foo"] = labelFooWant
  168. n2.Labels[v1.LabelZoneRegion] = "regiontwo"
  169. wantPrice2 := "0.1338"
  170. n3 := &v1.Node{}
  171. n3.Spec.ProviderID = providerIDWant
  172. n3.Name = nameWant
  173. n3.Labels = make(map[string]string)
  174. n3.Labels["foo"] = labelFooWant
  175. n3.Labels[v1.LabelZoneRegion] = "fakeregion"
  176. wantPrice3 := "0.1339"
  177. c := &cloud.CSVProvider{
  178. CSVLocation: "../configs/pricing_schema_region.csv",
  179. CustomProvider: &cloud.CustomProvider{
  180. Config: cloud.NewProviderConfig("../configs/default.json"),
  181. },
  182. }
  183. c.DownloadPricingData()
  184. k := c.GetKey(n.Labels, n)
  185. resN, err := c.NodePricing(k)
  186. if err != nil {
  187. t.Errorf("Error in NodePricing: %s", err.Error())
  188. } else {
  189. gotPrice := resN.Cost
  190. if gotPrice != wantPrice {
  191. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  192. }
  193. }
  194. k2 := c.GetKey(n2.Labels, n2)
  195. resN2, err := c.NodePricing(k2)
  196. if err != nil {
  197. t.Errorf("Error in NodePricing: %s", err.Error())
  198. } else {
  199. gotPrice := resN2.Cost
  200. if gotPrice != wantPrice2 {
  201. t.Errorf("Wanted price '%s' got price '%s'", wantPrice2, gotPrice)
  202. }
  203. }
  204. k3 := c.GetKey(n3.Labels, n3)
  205. resN3, err := c.NodePricing(k3)
  206. if err != nil {
  207. t.Errorf("Error in NodePricing: %s", err.Error())
  208. } else {
  209. gotPrice := resN3.Cost
  210. if gotPrice != wantPrice3 {
  211. t.Errorf("Wanted price '%s' got price '%s'", wantPrice3, gotPrice)
  212. }
  213. }
  214. unknownN := &v1.Node{}
  215. unknownN.Spec.ProviderID = "fake providerID"
  216. unknownN.Name = "unknownname"
  217. unknownN.Labels = make(map[string]string)
  218. unknownN.Labels["topology.kubernetes.io/region"] = "fakeregion"
  219. unknownN.Labels["foo"] = labelFooWant
  220. k4 := c.GetKey(unknownN.Labels, unknownN)
  221. resN4, _ := c.NodePricing(k4)
  222. if resN4 != nil {
  223. t.Errorf("CSV provider should return nil on missing node, instead returned %+v", resN4)
  224. }
  225. c2 := &cloud.CSVProvider{
  226. CSVLocation: "../configs/fake.csv",
  227. CustomProvider: &cloud.CustomProvider{
  228. Config: cloud.NewProviderConfig("../configs/default.json"),
  229. },
  230. }
  231. k5 := c.GetKey(n.Labels, n)
  232. resN5, _ := c2.NodePricing(k5)
  233. if resN5 != nil {
  234. t.Errorf("CSV provider should return nil on missing csv")
  235. }
  236. }
  237. type FakeCache struct {
  238. nodes []*v1.Node
  239. clustercache.ClusterCache
  240. }
  241. func (f FakeCache) GetAllNodes() []*v1.Node {
  242. return f.nodes
  243. }
  244. func (f FakeCache) GetAllDaemonSets() []*appsv1.DaemonSet {
  245. return nil
  246. }
  247. func NewFakeNodeCache(nodes []*v1.Node) FakeCache {
  248. return FakeCache{
  249. nodes: nodes,
  250. }
  251. }
  252. type FakeClusterMap struct {
  253. clusters.ClusterMap
  254. }
  255. func TestNodePriceFromCSVWithBadConfig(t *testing.T) {
  256. os.Setenv("CONFIG_PATH", "../config")
  257. c := &cloud.CSVProvider{
  258. CSVLocation: "../configs/pricing_schema_case.csv",
  259. CustomProvider: &cloud.CustomProvider{
  260. Config: cloud.NewProviderConfig("invalid.json"),
  261. },
  262. }
  263. c.DownloadPricingData()
  264. n := &v1.Node{}
  265. n.Spec.ProviderID = "fake"
  266. n.Name = "nameWant"
  267. n.Labels = make(map[string]string)
  268. n.Labels["foo"] = "labelFooWant"
  269. n.Labels[v1.LabelZoneRegion] = "regionone"
  270. fc := NewFakeNodeCache([]*v1.Node{n})
  271. fm := FakeClusterMap{}
  272. d, _ := time.ParseDuration("1m")
  273. model := costmodel.NewCostModel(nil, nil, fc, fm, d)
  274. _, err := model.GetNodeCost(c)
  275. if err != nil {
  276. t.Errorf("Error in node pricing: %s", err)
  277. }
  278. }
  279. func TestSourceMatchesFromCSV(t *testing.T) {
  280. os.Setenv("CONFIG_PATH", "../configs")
  281. c := &cloud.CSVProvider{
  282. CSVLocation: "../configs/pricing_schema_case.csv",
  283. CustomProvider: &cloud.CustomProvider{
  284. Config: cloud.NewProviderConfig("/default.json"),
  285. },
  286. }
  287. c.DownloadPricingData()
  288. n := &v1.Node{}
  289. n.Spec.ProviderID = "fake"
  290. n.Name = "nameWant"
  291. n.Labels = make(map[string]string)
  292. n.Labels["foo"] = "labelFooWant"
  293. n.Labels[v1.LabelZoneRegion] = "regionone"
  294. n2 := &v1.Node{}
  295. n2.Spec.ProviderID = "azure:///subscriptions/123a7sd-asd-1234-578a9-123abcdef/resourceGroups/case_12_STaGe_TeSt7/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-agent-worker0-12stagetest7-ezggnore/virtualMachines/7"
  296. n2.Labels = make(map[string]string)
  297. n2.Labels[v1.LabelZoneRegion] = "eastus2"
  298. n2.Labels["foo"] = "labelFooWant"
  299. k := c.GetKey(n2.Labels, n2)
  300. resN, err := c.NodePricing(k)
  301. if err != nil {
  302. t.Errorf("Error in NodePricing: %s", err.Error())
  303. } else {
  304. wantPrice := "0.13370357"
  305. gotPrice := resN.Cost
  306. if gotPrice != wantPrice {
  307. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  308. }
  309. }
  310. n3 := &v1.Node{}
  311. n3.Spec.ProviderID = "fake"
  312. n3.Name = "nameWant"
  313. n3.Labels = make(map[string]string)
  314. n.Labels[v1.LabelZoneRegion] = "eastus2"
  315. n.Labels[v1.LabelInstanceType] = "Standard_F32s_v2"
  316. fc := NewFakeNodeCache([]*v1.Node{n, n2, n3})
  317. fm := FakeClusterMap{}
  318. d, _ := time.ParseDuration("1m")
  319. model := costmodel.NewCostModel(nil, nil, fc, fm, d)
  320. _, err = model.GetNodeCost(c)
  321. if err != nil {
  322. t.Errorf("Error in node pricing: %s", err)
  323. }
  324. p, err := model.GetPricingSourceCounts()
  325. if err != nil {
  326. t.Errorf("Error in pricing source counts: %s", err)
  327. } else if p.TotalNodes != 3 {
  328. t.Errorf("Wanted 3 nodes got %d", p.TotalNodes)
  329. }
  330. if p.PricingTypeCounts[""] != 1 {
  331. t.Errorf("Wanted 1 default match got %d: %+v", p.PricingTypeCounts[""], p.PricingTypeCounts)
  332. }
  333. if p.PricingTypeCounts["csvExact"] != 1 {
  334. t.Errorf("Wanted 1 exact match got %d: %+v", p.PricingTypeCounts["csvExact"], p.PricingTypeCounts)
  335. }
  336. if p.PricingTypeCounts["csvClass"] != 1 {
  337. t.Errorf("Wanted 1 class match got %d: %+v", p.PricingTypeCounts["csvClass"], p.PricingTypeCounts)
  338. }
  339. }
  340. func TestNodePriceFromCSVWithCase(t *testing.T) {
  341. n := &v1.Node{}
  342. n.Spec.ProviderID = "azure:///subscriptions/123a7sd-asd-1234-578a9-123abcdef/resourceGroups/case_12_STaGe_TeSt7/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-agent-worker0-12stagetest7-ezggnore/virtualMachines/7"
  343. n.Labels = make(map[string]string)
  344. n.Labels[v1.LabelZoneRegion] = "eastus2"
  345. wantPrice := "0.13370357"
  346. c := &cloud.CSVProvider{
  347. CSVLocation: "../configs/pricing_schema_case.csv",
  348. CustomProvider: &cloud.CustomProvider{
  349. Config: cloud.NewProviderConfig("../configs/default.json"),
  350. },
  351. }
  352. c.DownloadPricingData()
  353. k := c.GetKey(n.Labels, n)
  354. resN, err := c.NodePricing(k)
  355. if err != nil {
  356. t.Errorf("Error in NodePricing: %s", err.Error())
  357. } else {
  358. gotPrice := resN.Cost
  359. if gotPrice != wantPrice {
  360. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  361. }
  362. }
  363. }
  364. func TestNodePriceFromCSVByClass(t *testing.T) {
  365. n := &v1.Node{}
  366. n.Spec.ProviderID = "fakeproviderid"
  367. n.Labels = make(map[string]string)
  368. n.Labels[v1.LabelZoneRegion] = "eastus2"
  369. n.Labels[v1.LabelInstanceType] = "Standard_F32s_v2"
  370. wantpricefloat := 0.13370357
  371. wantPrice := fmt.Sprintf("%f", (math.Round(wantpricefloat*1000000) / 1000000))
  372. c := &cloud.CSVProvider{
  373. CSVLocation: "../configs/pricing_schema_case.csv",
  374. CustomProvider: &cloud.CustomProvider{
  375. Config: cloud.NewProviderConfig("../configs/default.json"),
  376. },
  377. }
  378. c.DownloadPricingData()
  379. k := c.GetKey(n.Labels, n)
  380. resN, err := c.NodePricing(k)
  381. if err != nil {
  382. t.Errorf("Error in NodePricing: %s", err.Error())
  383. } else {
  384. gotPrice := resN.Cost
  385. if gotPrice != wantPrice {
  386. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  387. }
  388. }
  389. n2 := &v1.Node{}
  390. n2.Spec.ProviderID = "fakeproviderid"
  391. n2.Labels = make(map[string]string)
  392. n2.Labels[v1.LabelZoneRegion] = "fakeregion"
  393. n2.Labels[v1.LabelInstanceType] = "Standard_F32s_v2"
  394. k2 := c.GetKey(n2.Labels, n)
  395. c.DownloadPricingData()
  396. resN2, err := c.NodePricing(k2)
  397. if resN2 != nil {
  398. t.Errorf("CSV provider should return nil on missing node, instead returned %+v", resN2)
  399. }
  400. }