cloud_test.go 14 KB

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