cloud_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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. wantPrice := "0.1337"
  85. c := &cloud.CSVProvider{
  86. CSVLocation: "../configs/pricing_schema_pv.csv",
  87. CustomProvider: &cloud.CustomProvider{
  88. Config: cloud.NewProviderConfig("../configs/default.json"),
  89. },
  90. }
  91. c.DownloadPricingData()
  92. k := c.GetPVKey(pv, make(map[string]string), "")
  93. resPV, err := c.PVPricing(k)
  94. if err != nil {
  95. t.Errorf("Error in NodePricing: %s", err.Error())
  96. } else {
  97. gotPrice := resPV.Cost
  98. if gotPrice != wantPrice {
  99. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  100. }
  101. }
  102. }
  103. func TestNodePriceFromCSV(t *testing.T) {
  104. providerIDWant := "providerid"
  105. nameWant := "gke-standard-cluster-1-pool-1-91dc432d-cg69"
  106. labelFooWant := "labelfoo"
  107. confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  108. LocalConfigPath: "./",
  109. })
  110. n := &v1.Node{}
  111. n.Spec.ProviderID = providerIDWant
  112. n.Name = nameWant
  113. n.Labels = make(map[string]string)
  114. n.Labels["foo"] = labelFooWant
  115. wantPrice := "0.1337"
  116. c := &cloud.CSVProvider{
  117. CSVLocation: "../configs/pricing_schema.csv",
  118. CustomProvider: &cloud.CustomProvider{
  119. Config: cloud.NewProviderConfig(confMan, "../configs/default.json"),
  120. },
  121. }
  122. c.DownloadPricingData()
  123. k := c.GetKey(n.Labels, n)
  124. resN, err := c.NodePricing(k)
  125. if err != nil {
  126. t.Errorf("Error in NodePricing: %s", err.Error())
  127. } else {
  128. gotPrice := resN.Cost
  129. if gotPrice != wantPrice {
  130. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  131. }
  132. }
  133. unknownN := &v1.Node{}
  134. unknownN.Spec.ProviderID = providerIDWant
  135. unknownN.Name = "unknownname"
  136. unknownN.Labels = make(map[string]string)
  137. unknownN.Labels["foo"] = labelFooWant
  138. unknownN.Labels["topology.kubernetes.io/region"] = "fakeregion"
  139. k2 := c.GetKey(unknownN.Labels, unknownN)
  140. resN2, _ := c.NodePricing(k2)
  141. if resN2 != nil {
  142. t.Errorf("CSV provider should return nil on missing node")
  143. }
  144. c2 := &cloud.CSVProvider{
  145. CSVLocation: "../configs/fake.csv",
  146. CustomProvider: &cloud.CustomProvider{
  147. Config: cloud.NewProviderConfig(confMan, "../configs/default.json"),
  148. },
  149. }
  150. k3 := c.GetKey(n.Labels, n)
  151. resN3, _ := c2.NodePricing(k3)
  152. if resN3 != nil {
  153. t.Errorf("CSV provider should return nil on missing csv")
  154. }
  155. }
  156. func TestNodePriceFromCSVWithRegion(t *testing.T) {
  157. providerIDWant := "gke-standard-cluster-1-pool-1-91dc432d-cg69"
  158. nameWant := "foo"
  159. labelFooWant := "labelfoo"
  160. confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  161. LocalConfigPath: "./",
  162. })
  163. n := &v1.Node{}
  164. n.Spec.ProviderID = providerIDWant
  165. n.Name = nameWant
  166. n.Labels = make(map[string]string)
  167. n.Labels["foo"] = labelFooWant
  168. n.Labels[v1.LabelZoneRegion] = "regionone"
  169. wantPrice := "0.1337"
  170. n2 := &v1.Node{}
  171. n2.Spec.ProviderID = providerIDWant
  172. n2.Name = nameWant
  173. n2.Labels = make(map[string]string)
  174. n2.Labels["foo"] = labelFooWant
  175. n2.Labels[v1.LabelZoneRegion] = "regiontwo"
  176. wantPrice2 := "0.1338"
  177. n3 := &v1.Node{}
  178. n3.Spec.ProviderID = providerIDWant
  179. n3.Name = nameWant
  180. n3.Labels = make(map[string]string)
  181. n3.Labels["foo"] = labelFooWant
  182. n3.Labels[v1.LabelZoneRegion] = "fakeregion"
  183. wantPrice3 := "0.1339"
  184. c := &cloud.CSVProvider{
  185. CSVLocation: "../configs/pricing_schema_region.csv",
  186. CustomProvider: &cloud.CustomProvider{
  187. Config: cloud.NewProviderConfig(confMan, "../configs/default.json"),
  188. },
  189. }
  190. c.DownloadPricingData()
  191. k := c.GetKey(n.Labels, n)
  192. resN, err := c.NodePricing(k)
  193. if err != nil {
  194. t.Errorf("Error in NodePricing: %s", err.Error())
  195. } else {
  196. gotPrice := resN.Cost
  197. if gotPrice != wantPrice {
  198. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  199. }
  200. }
  201. k2 := c.GetKey(n2.Labels, n2)
  202. resN2, err := c.NodePricing(k2)
  203. if err != nil {
  204. t.Errorf("Error in NodePricing: %s", err.Error())
  205. } else {
  206. gotPrice := resN2.Cost
  207. if gotPrice != wantPrice2 {
  208. t.Errorf("Wanted price '%s' got price '%s'", wantPrice2, gotPrice)
  209. }
  210. }
  211. k3 := c.GetKey(n3.Labels, n3)
  212. resN3, err := c.NodePricing(k3)
  213. if err != nil {
  214. t.Errorf("Error in NodePricing: %s", err.Error())
  215. } else {
  216. gotPrice := resN3.Cost
  217. if gotPrice != wantPrice3 {
  218. t.Errorf("Wanted price '%s' got price '%s'", wantPrice3, gotPrice)
  219. }
  220. }
  221. unknownN := &v1.Node{}
  222. unknownN.Spec.ProviderID = "fake providerID"
  223. unknownN.Name = "unknownname"
  224. unknownN.Labels = make(map[string]string)
  225. unknownN.Labels["topology.kubernetes.io/region"] = "fakeregion"
  226. unknownN.Labels["foo"] = labelFooWant
  227. k4 := c.GetKey(unknownN.Labels, unknownN)
  228. resN4, _ := c.NodePricing(k4)
  229. if resN4 != nil {
  230. t.Errorf("CSV provider should return nil on missing node, instead returned %+v", resN4)
  231. }
  232. c2 := &cloud.CSVProvider{
  233. CSVLocation: "../configs/fake.csv",
  234. CustomProvider: &cloud.CustomProvider{
  235. Config: cloud.NewProviderConfig(confMan, "../configs/default.json"),
  236. },
  237. }
  238. k5 := c.GetKey(n.Labels, n)
  239. resN5, _ := c2.NodePricing(k5)
  240. if resN5 != nil {
  241. t.Errorf("CSV provider should return nil on missing csv")
  242. }
  243. }
  244. type FakeCache struct {
  245. nodes []*v1.Node
  246. clustercache.ClusterCache
  247. }
  248. func (f FakeCache) GetAllNodes() []*v1.Node {
  249. return f.nodes
  250. }
  251. func (f FakeCache) GetAllDaemonSets() []*appsv1.DaemonSet {
  252. return nil
  253. }
  254. func NewFakeNodeCache(nodes []*v1.Node) FakeCache {
  255. return FakeCache{
  256. nodes: nodes,
  257. }
  258. }
  259. type FakeClusterMap struct {
  260. clusters.ClusterMap
  261. }
  262. func TestNodePriceFromCSVWithBadConfig(t *testing.T) {
  263. os.Setenv("CONFIG_PATH", "../config")
  264. confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  265. LocalConfigPath: "./",
  266. })
  267. c := &cloud.CSVProvider{
  268. CSVLocation: "../configs/pricing_schema_case.csv",
  269. CustomProvider: &cloud.CustomProvider{
  270. Config: cloud.NewProviderConfig(confMan, "invalid.json"),
  271. },
  272. }
  273. c.DownloadPricingData()
  274. n := &v1.Node{}
  275. n.Spec.ProviderID = "fake"
  276. n.Name = "nameWant"
  277. n.Labels = make(map[string]string)
  278. n.Labels["foo"] = "labelFooWant"
  279. n.Labels[v1.LabelZoneRegion] = "regionone"
  280. fc := NewFakeNodeCache([]*v1.Node{n})
  281. fm := FakeClusterMap{}
  282. d, _ := time.ParseDuration("1m")
  283. model := costmodel.NewCostModel(nil, nil, fc, fm, d)
  284. _, err := model.GetNodeCost(c)
  285. if err != nil {
  286. t.Errorf("Error in node pricing: %s", err)
  287. }
  288. }
  289. func TestSourceMatchesFromCSV(t *testing.T) {
  290. os.Setenv("CONFIG_PATH", "../configs")
  291. confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  292. LocalConfigPath: "./",
  293. })
  294. c := &cloud.CSVProvider{
  295. CSVLocation: "../configs/pricing_schema_case.csv",
  296. CustomProvider: &cloud.CustomProvider{
  297. Config: cloud.NewProviderConfig(confMan, "/default.json"),
  298. },
  299. }
  300. c.DownloadPricingData()
  301. n := &v1.Node{}
  302. n.Spec.ProviderID = "fake"
  303. n.Name = "nameWant"
  304. n.Labels = make(map[string]string)
  305. n.Labels["foo"] = "labelFooWant"
  306. n.Labels[v1.LabelZoneRegion] = "regionone"
  307. n2 := &v1.Node{}
  308. 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"
  309. n2.Labels = make(map[string]string)
  310. n2.Labels[v1.LabelZoneRegion] = "eastus2"
  311. n2.Labels["foo"] = "labelFooWant"
  312. k := c.GetKey(n2.Labels, n2)
  313. resN, err := c.NodePricing(k)
  314. if err != nil {
  315. t.Errorf("Error in NodePricing: %s", err.Error())
  316. } else {
  317. wantPrice := "0.13370357"
  318. gotPrice := resN.Cost
  319. if gotPrice != wantPrice {
  320. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  321. }
  322. }
  323. n3 := &v1.Node{}
  324. n3.Spec.ProviderID = "fake"
  325. n3.Name = "nameWant"
  326. n3.Labels = make(map[string]string)
  327. n.Labels[v1.LabelZoneRegion] = "eastus2"
  328. n.Labels[v1.LabelInstanceType] = "Standard_F32s_v2"
  329. fc := NewFakeNodeCache([]*v1.Node{n, n2, n3})
  330. fm := FakeClusterMap{}
  331. d, _ := time.ParseDuration("1m")
  332. model := costmodel.NewCostModel(nil, nil, fc, fm, d)
  333. _, err = model.GetNodeCost(c)
  334. if err != nil {
  335. t.Errorf("Error in node pricing: %s", err)
  336. }
  337. p, err := model.GetPricingSourceCounts()
  338. if err != nil {
  339. t.Errorf("Error in pricing source counts: %s", err)
  340. } else if p.TotalNodes != 3 {
  341. t.Errorf("Wanted 3 nodes got %d", p.TotalNodes)
  342. }
  343. if p.PricingTypeCounts[""] != 1 {
  344. t.Errorf("Wanted 1 default match got %d: %+v", p.PricingTypeCounts[""], p.PricingTypeCounts)
  345. }
  346. if p.PricingTypeCounts["csvExact"] != 1 {
  347. t.Errorf("Wanted 1 exact match got %d: %+v", p.PricingTypeCounts["csvExact"], p.PricingTypeCounts)
  348. }
  349. if p.PricingTypeCounts["csvClass"] != 1 {
  350. t.Errorf("Wanted 1 class match got %d: %+v", p.PricingTypeCounts["csvClass"], p.PricingTypeCounts)
  351. }
  352. }
  353. func TestNodePriceFromCSVWithCase(t *testing.T) {
  354. n := &v1.Node{}
  355. 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"
  356. n.Labels = make(map[string]string)
  357. n.Labels[v1.LabelZoneRegion] = "eastus2"
  358. wantPrice := "0.13370357"
  359. confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  360. LocalConfigPath: "./",
  361. })
  362. c := &cloud.CSVProvider{
  363. CSVLocation: "../configs/pricing_schema_case.csv",
  364. CustomProvider: &cloud.CustomProvider{
  365. Config: cloud.NewProviderConfig(confMan, "../configs/default.json"),
  366. },
  367. }
  368. c.DownloadPricingData()
  369. k := c.GetKey(n.Labels, n)
  370. resN, err := c.NodePricing(k)
  371. if err != nil {
  372. t.Errorf("Error in NodePricing: %s", err.Error())
  373. } else {
  374. gotPrice := resN.Cost
  375. if gotPrice != wantPrice {
  376. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  377. }
  378. }
  379. }
  380. func TestNodePriceFromCSVByClass(t *testing.T) {
  381. n := &v1.Node{}
  382. n.Spec.ProviderID = "fakeproviderid"
  383. n.Labels = make(map[string]string)
  384. n.Labels[v1.LabelZoneRegion] = "eastus2"
  385. n.Labels[v1.LabelInstanceType] = "Standard_F32s_v2"
  386. wantpricefloat := 0.13370357
  387. wantPrice := fmt.Sprintf("%f", (math.Round(wantpricefloat*1000000) / 1000000))
  388. confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  389. LocalConfigPath: "./",
  390. })
  391. c := &cloud.CSVProvider{
  392. CSVLocation: "../configs/pricing_schema_case.csv",
  393. CustomProvider: &cloud.CustomProvider{
  394. Config: cloud.NewProviderConfig(confMan, "../configs/default.json"),
  395. },
  396. }
  397. c.DownloadPricingData()
  398. k := c.GetKey(n.Labels, n)
  399. resN, err := c.NodePricing(k)
  400. if err != nil {
  401. t.Errorf("Error in NodePricing: %s", err.Error())
  402. } else {
  403. gotPrice := resN.Cost
  404. if gotPrice != wantPrice {
  405. t.Errorf("Wanted price '%s' got price '%s'", wantPrice, gotPrice)
  406. }
  407. }
  408. n2 := &v1.Node{}
  409. n2.Spec.ProviderID = "fakeproviderid"
  410. n2.Labels = make(map[string]string)
  411. n2.Labels[v1.LabelZoneRegion] = "fakeregion"
  412. n2.Labels[v1.LabelInstanceType] = "Standard_F32s_v2"
  413. k2 := c.GetKey(n2.Labels, n)
  414. c.DownloadPricingData()
  415. resN2, err := c.NodePricing(k2)
  416. if resN2 != nil {
  417. t.Errorf("CSV provider should return nil on missing node, instead returned %+v", resN2)
  418. }
  419. }