provider_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. package digitalocean
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "os"
  6. "testing"
  7. "github.com/opencost/opencost/pkg/cloud/models"
  8. )
  9. func newTestProviderWithFile(t *testing.T, filename string) (*DOKS, func() int) {
  10. t.Helper()
  11. data, err := os.ReadFile(filename)
  12. if err != nil {
  13. t.Fatalf("Failed to read file: %v", err)
  14. }
  15. var count int
  16. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  17. count++
  18. w.Header().Set("Content-Type", "application/json")
  19. _, _ = w.Write(data)
  20. }))
  21. t.Cleanup(server.Close)
  22. provider := NewDOKSProvider(server.URL)
  23. return provider, func() int { return count }
  24. }
  25. func newTestProviderWith404(t *testing.T) *DOKS {
  26. t.Helper()
  27. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  28. w.WriteHeader(http.StatusNotFound)
  29. }))
  30. t.Cleanup(server.Close)
  31. provider := NewDOKSProvider(server.URL)
  32. return provider
  33. }
  34. func TestNodePricing_APIMatches(t *testing.T) {
  35. provider, callCount := newTestProviderWithFile(t, "testdata/do_pricing.json")
  36. key := &doksKey{
  37. Labels: map[string]string{
  38. "node.kubernetes.io/instance-type": "s-1vcpu-2gb",
  39. "kubernetes.io/arch": "amd64",
  40. },
  41. }
  42. node, meta, err := provider.NodePricing(key)
  43. if err != nil {
  44. t.Fatalf("expected no error, got: %v", err)
  45. }
  46. if node == nil {
  47. t.Fatal("expected node pricing, got nil")
  48. }
  49. assertEqual := func(name, got, want string) {
  50. if got != want {
  51. t.Errorf("%s: got %s, want %s", name, got, want)
  52. }
  53. }
  54. assertEqual("Cost", node.Cost, "0.01199")
  55. assertEqual("VCPUCost", node.VCPUCost, "0.00400") // 1/3
  56. assertEqual("RAMCost", node.RAMCost, "0.00799") // 2/3
  57. assertEqual("VCPU", node.VCPU, "1")
  58. assertEqual("RAM", node.RAM, "2GiB")
  59. assertEqual("ArchType", node.ArchType, "amd64")
  60. assertEqual("PricingType", string(node.PricingType), string(models.DefaultPrices))
  61. if meta.Source != "digitalocean" {
  62. t.Errorf("expected metadata source to be digitalocean, got: %s", meta.Source)
  63. }
  64. if c := callCount(); c != 1 {
  65. t.Errorf("expected 1 API call, got %d", c)
  66. }
  67. }
  68. func TestNodePricing_Fallback(t *testing.T) {
  69. provider, callCount := newTestProviderWithFile(t, "testdata/do_pricing.json")
  70. key := &doksKey{
  71. Labels: map[string]string{
  72. "node.kubernetes.io/instance-type": "s-2vcpu-4gb",
  73. "kubernetes.io/arch": "amd64",
  74. },
  75. }
  76. node, meta, err := provider.NodePricing(key)
  77. if err != nil {
  78. t.Fatalf("expected no error, got: %v", err)
  79. }
  80. if node == nil {
  81. t.Fatal("expected node pricing, got nil")
  82. }
  83. assertEqual := func(name, got, want string) {
  84. if got != want {
  85. t.Errorf("%s: got %s, want %s", name, got, want)
  86. }
  87. }
  88. assertEqual("Cost", node.Cost, "0.03571")
  89. assertEqual("VCPUCost", node.VCPUCost, "0.00595")
  90. assertEqual("RAMCost", node.RAMCost, "0.00595")
  91. assertEqual("VCPU", node.VCPU, "2")
  92. assertEqual("RAM", node.RAM, "4GiB")
  93. assertEqual("ArchType", node.ArchType, "amd64")
  94. assertEqual("PricingType", string(node.PricingType), string(models.DefaultPrices))
  95. if meta.Source != "static-fallback" {
  96. t.Errorf("expected metadata source to be static-fallback, got: %s", meta.Source)
  97. }
  98. if c := callCount(); c != 1 {
  99. t.Errorf("expected 1 API call, got %d", c)
  100. }
  101. }
  102. func TestNodePricing_Estimation_C8Intel(t *testing.T) {
  103. provider := newTestProviderWith404(t)
  104. key := &doksKey{
  105. Labels: map[string]string{
  106. "node.kubernetes.io/instance-type": "c-8-intel",
  107. "kubernetes.io/arch": "amd64",
  108. },
  109. }
  110. node, meta, err := provider.NodePricing(key)
  111. if err != nil {
  112. t.Fatalf("expected no error, got: %v", err)
  113. }
  114. expectedCost := "0.32440"
  115. expectedVCPUCost := "0.01352"
  116. expectedRAMCost := "0.01352"
  117. if node.Cost != expectedCost {
  118. t.Errorf("Cost: got %s, want %s", node.Cost, expectedCost)
  119. }
  120. if node.VCPUCost != expectedVCPUCost {
  121. t.Errorf("VCPUCost: got %s, want %s", node.VCPUCost, expectedVCPUCost)
  122. }
  123. if node.RAMCost != expectedRAMCost {
  124. t.Errorf("RAMCost: got %s, want %s", node.RAMCost, expectedRAMCost)
  125. }
  126. if node.VCPU != "8" {
  127. t.Errorf("VCPU: got %s, want 8", node.VCPU)
  128. }
  129. if node.RAM != "16GiB" {
  130. t.Errorf("RAM: got %s, want 16GiB", node.RAM)
  131. }
  132. if meta.Source != "static-fallback" {
  133. t.Errorf("expected metadata source to be estimated, got: %s", meta.Source)
  134. }
  135. }
  136. func TestNodePricing_EstimationFromSlug(t *testing.T) {
  137. tests := []struct {
  138. name string
  139. slug string
  140. expectedVCPU string
  141. expectedRAM string
  142. expectedCost string
  143. expectedCPU string
  144. expectedRAMCost string
  145. }{
  146. {
  147. name: "s-4vcpu-8gb",
  148. slug: "s-4vcpu-8gb",
  149. expectedVCPU: "4",
  150. expectedRAM: "8GiB",
  151. expectedCost: "0.07143",
  152. expectedCPU: "0.00595",
  153. expectedRAMCost: "0.00595",
  154. },
  155. {
  156. name: "m-8vcpu-64gb",
  157. slug: "m-8vcpu-64gb",
  158. expectedVCPU: "8",
  159. expectedRAM: "64GiB",
  160. expectedCost: "0.50000",
  161. expectedCPU: "0.00694",
  162. expectedRAMCost: "0.00694",
  163. },
  164. {
  165. name: "g-4vcpu-16gb-intel",
  166. slug: "g-4vcpu-16gb-intel",
  167. expectedVCPU: "4",
  168. expectedRAM: "16GiB",
  169. expectedCost: "0.22470",
  170. expectedCPU: "0.01124",
  171. expectedRAMCost: "0.01124",
  172. },
  173. }
  174. provider := newTestProviderWith404(t) // Force fallback/estimate
  175. for _, tc := range tests {
  176. t.Run(tc.name, func(t *testing.T) {
  177. key := &doksKey{
  178. Labels: map[string]string{
  179. "node.kubernetes.io/instance-type": tc.slug,
  180. "kubernetes.io/arch": "amd64",
  181. },
  182. }
  183. node, meta, err := provider.NodePricing(key)
  184. if err != nil {
  185. t.Fatalf("unexpected error: %v", err)
  186. }
  187. if node == nil {
  188. t.Fatal("expected node to be non-nil")
  189. }
  190. assertEqual := func(field, got, want string) {
  191. if got != want {
  192. t.Errorf("%s: got %s, want %s", field, got, want)
  193. }
  194. }
  195. assertEqual("Cost", node.Cost, tc.expectedCost)
  196. assertEqual("VCPUCost", node.VCPUCost, tc.expectedCPU)
  197. assertEqual("RAMCost", node.RAMCost, tc.expectedRAMCost)
  198. assertEqual("VCPU", node.VCPU, tc.expectedVCPU)
  199. assertEqual("RAM", node.RAM, tc.expectedRAM)
  200. assertEqual("ArchType", node.ArchType, "amd64")
  201. if meta.Source != "static-fallback" {
  202. t.Errorf("expected metadata source to be 'estimated', got: %s", meta.Source)
  203. }
  204. })
  205. }
  206. }
  207. func TestNodePricing_Estimation_BaseSlugs(t *testing.T) {
  208. tests := []struct {
  209. name string
  210. slug string
  211. expectedVCPU string
  212. expectedRAM string
  213. expectedCost string
  214. expectedCPU string
  215. expectedRAMCost string
  216. }{
  217. {
  218. name: "c-8-intel",
  219. slug: "c-8-intel",
  220. expectedVCPU: "8",
  221. expectedRAM: "16GiB",
  222. expectedCost: "0.32440",
  223. expectedCPU: "0.01352",
  224. expectedRAMCost: "0.01352",
  225. },
  226. {
  227. name: "s-2vcpu-4gb",
  228. slug: "s-2vcpu-4gb",
  229. expectedVCPU: "2",
  230. expectedRAM: "4GiB",
  231. expectedCost: "0.03571",
  232. expectedCPU: "0.00595",
  233. expectedRAMCost: "0.00595",
  234. },
  235. {
  236. name: "m-4vcpu-32gb",
  237. slug: "m-4vcpu-32gb",
  238. expectedVCPU: "4",
  239. expectedRAM: "32GiB",
  240. expectedCost: "0.25000",
  241. expectedCPU: "0.00694",
  242. expectedRAMCost: "0.00694",
  243. },
  244. {
  245. name: "g-16vcpu-64gb-intel",
  246. slug: "g-16vcpu-64gb-intel",
  247. expectedVCPU: "16",
  248. expectedRAM: "64GiB",
  249. expectedCost: "0.89880",
  250. expectedCPU: "0.01124",
  251. expectedRAMCost: "0.01124",
  252. },
  253. }
  254. provider := newTestProviderWith404(t) // ensures fallback path is tested
  255. for _, tc := range tests {
  256. t.Run(tc.name, func(t *testing.T) {
  257. key := &doksKey{
  258. Labels: map[string]string{
  259. "node.kubernetes.io/instance-type": tc.slug,
  260. "kubernetes.io/arch": "amd64",
  261. },
  262. }
  263. node, meta, err := provider.NodePricing(key)
  264. if err != nil {
  265. t.Fatalf("unexpected error: %v", err)
  266. }
  267. if node == nil {
  268. t.Fatal("expected node to be non-nil")
  269. }
  270. assertEqual := func(field, got, want string) {
  271. if got != want {
  272. t.Errorf("%s: got %s, want %s", field, got, want)
  273. }
  274. }
  275. assertEqual("Cost", node.Cost, tc.expectedCost)
  276. assertEqual("VCPUCost", node.VCPUCost, tc.expectedCPU)
  277. assertEqual("RAMCost", node.RAMCost, tc.expectedRAMCost)
  278. assertEqual("VCPU", node.VCPU, tc.expectedVCPU)
  279. assertEqual("RAM", node.RAM, tc.expectedRAM)
  280. assertEqual("ArchType", node.ArchType, "amd64")
  281. if meta.Source != "static-fallback" {
  282. t.Errorf("expected metadata source to be 'static-fallback', got: %s", meta.Source)
  283. }
  284. })
  285. }
  286. }
  287. func TestNodePricing_Estimation_FamilySeeds(t *testing.T) {
  288. tests := []struct {
  289. name string
  290. slug string
  291. expectedVCPU string
  292. expectedRAM string
  293. expectedCost string
  294. expectedCPU string
  295. expectedRAMCost string
  296. }{
  297. {
  298. name: "c-16",
  299. slug: "c-16",
  300. expectedVCPU: "16",
  301. expectedRAM: "32GiB",
  302. expectedCost: "0.50000",
  303. expectedCPU: "0.01042",
  304. expectedRAMCost: "0.01042",
  305. },
  306. {
  307. name: "c-16-intel",
  308. slug: "c-16-intel",
  309. expectedVCPU: "16",
  310. expectedRAM: "32GiB",
  311. expectedCost: "0.64880",
  312. expectedCPU: "0.01352",
  313. expectedRAMCost: "0.01352",
  314. },
  315. {
  316. name: "c2-8vcpu-16gb",
  317. slug: "c2-8vcpu-16gb",
  318. expectedVCPU: "8",
  319. expectedRAM: "16GiB",
  320. expectedCost: "0.27976",
  321. expectedCPU: "0.01166",
  322. expectedRAMCost: "0.01166",
  323. },
  324. {
  325. name: "c2-8vcpu-16gb-intel",
  326. slug: "c2-8vcpu-16gb-intel",
  327. expectedVCPU: "8",
  328. expectedRAM: "16GiB",
  329. expectedCost: "0.36310",
  330. expectedCPU: "0.01513",
  331. expectedRAMCost: "0.01513",
  332. },
  333. {
  334. name: "g-8vcpu-32gb",
  335. slug: "g-8vcpu-32gb",
  336. expectedVCPU: "8",
  337. expectedRAM: "32GiB",
  338. expectedCost: "0.37500",
  339. expectedCPU: "0.00937",
  340. expectedRAMCost: "0.00937",
  341. },
  342. {
  343. name: "g-8vcpu-32gb-intel",
  344. slug: "g-8vcpu-32gb-intel",
  345. expectedVCPU: "8",
  346. expectedRAM: "32GiB",
  347. expectedCost: "0.44940",
  348. expectedCPU: "0.01124",
  349. expectedRAMCost: "0.01124",
  350. },
  351. {
  352. name: "gd-40vcpu-160gb",
  353. slug: "gd-40vcpu-160gb",
  354. expectedVCPU: "40",
  355. expectedRAM: "160GiB",
  356. expectedCost: "2.02380",
  357. expectedCPU: "0.01012",
  358. expectedRAMCost: "0.01012",
  359. },
  360. {
  361. name: "gd-16vcpu-64gb-intel",
  362. slug: "gd-16vcpu-64gb-intel",
  363. expectedVCPU: "16",
  364. expectedRAM: "64GiB",
  365. expectedCost: "0.94048",
  366. expectedCPU: "0.01176",
  367. expectedRAMCost: "0.01176",
  368. },
  369. {
  370. name: "m-16vcpu-128gb",
  371. slug: "m-16vcpu-128gb",
  372. expectedVCPU: "16",
  373. expectedRAM: "128GiB",
  374. expectedCost: "1.00000",
  375. expectedCPU: "0.00694",
  376. expectedRAMCost: "0.00694",
  377. },
  378. {
  379. name: "m-16vcpu-128gb-intel",
  380. slug: "m-16vcpu-128gb-intel",
  381. expectedVCPU: "16",
  382. expectedRAM: "128GiB",
  383. expectedCost: "1.17858",
  384. expectedCPU: "0.00818",
  385. expectedRAMCost: "0.00818",
  386. },
  387. // m3
  388. {
  389. name: "m3-8vcpu-64gb",
  390. slug: "m3-8vcpu-64gb",
  391. expectedVCPU: "8",
  392. expectedRAM: "64GiB",
  393. expectedCost: "0.61905",
  394. expectedCPU: "0.00860",
  395. expectedRAMCost: "0.00860",
  396. },
  397. {
  398. name: "m3-32vcpu-256gb-intel",
  399. slug: "m3-32vcpu-256gb-intel",
  400. expectedVCPU: "32",
  401. expectedRAM: "256GiB",
  402. expectedCost: "2.61904",
  403. expectedCPU: "0.00909",
  404. expectedRAMCost: "0.00909",
  405. },
  406. {
  407. name: "m6-8vcpu-64gb",
  408. slug: "m6-8vcpu-64gb",
  409. expectedVCPU: "8",
  410. expectedRAM: "64GiB",
  411. expectedCost: "0.77976",
  412. expectedCPU: "0.01083",
  413. expectedRAMCost: "0.01083",
  414. },
  415. {
  416. name: "m6-24vcpu-192gb",
  417. slug: "m6-24vcpu-192gb",
  418. expectedVCPU: "24",
  419. expectedRAM: "192GiB",
  420. expectedCost: "2.33928",
  421. expectedCPU: "0.01083",
  422. expectedRAMCost: "0.01083",
  423. },
  424. {
  425. name: "s-1vcpu-2gb",
  426. slug: "s-1vcpu-2gb",
  427. expectedVCPU: "1",
  428. expectedRAM: "2GiB",
  429. expectedCost: "0.01786",
  430. expectedCPU: "0.00595",
  431. expectedRAMCost: "0.00595",
  432. },
  433. {
  434. name: "s-8vcpu-16gb-intel",
  435. slug: "s-8vcpu-16gb-intel",
  436. expectedVCPU: "8",
  437. expectedRAM: "16GiB",
  438. expectedCost: "0.16666",
  439. expectedCPU: "0.00694",
  440. expectedRAMCost: "0.00694",
  441. },
  442. {
  443. name: "so-8vcpu-64gb",
  444. slug: "so-8vcpu-64gb",
  445. expectedVCPU: "8",
  446. expectedRAM: "64GiB",
  447. expectedCost: "0.77976",
  448. expectedCPU: "0.01083",
  449. expectedRAMCost: "0.01083",
  450. },
  451. {
  452. name: "so-8vcpu-64gb-intel",
  453. slug: "so-8vcpu-64gb-intel",
  454. expectedVCPU: "8",
  455. expectedRAM: "64GiB",
  456. expectedCost: "0.77976",
  457. expectedCPU: "0.01083",
  458. expectedRAMCost: "0.01083",
  459. },
  460. {
  461. name: "so1_5-8vcpu-64gb",
  462. slug: "so1_5-8vcpu-64gb",
  463. expectedVCPU: "8",
  464. expectedRAM: "64GiB",
  465. expectedCost: "0.97024",
  466. expectedCPU: "0.01348",
  467. expectedRAMCost: "0.01348",
  468. },
  469. {
  470. name: "so1_5-8vcpu-64gb-intel",
  471. slug: "so1_5-8vcpu-64gb-intel",
  472. expectedVCPU: "8",
  473. expectedRAM: "64GiB",
  474. expectedCost: "0.82738",
  475. expectedCPU: "0.01149",
  476. expectedRAMCost: "0.01149",
  477. },
  478. }
  479. provider := newTestProviderWith404(t)
  480. for _, tc := range tests {
  481. t.Run(tc.name, func(t *testing.T) {
  482. key := &doksKey{
  483. Labels: map[string]string{
  484. "node.kubernetes.io/instance-type": tc.slug,
  485. "kubernetes.io/arch": "amd64",
  486. },
  487. }
  488. node, meta, err := provider.NodePricing(key)
  489. if err != nil {
  490. t.Fatalf("unexpected error: %v", err)
  491. }
  492. if node == nil {
  493. t.Fatal("expected node to be non-nil")
  494. }
  495. assertEqual := func(field, got, want string) {
  496. if got != want {
  497. t.Errorf("%s: got %s, want %s", field, got, want)
  498. }
  499. }
  500. assertEqual("Cost", node.Cost, tc.expectedCost)
  501. assertEqual("VCPUCost", node.VCPUCost, tc.expectedCPU)
  502. assertEqual("RAMCost", node.RAMCost, tc.expectedRAMCost)
  503. assertEqual("VCPU", node.VCPU, tc.expectedVCPU)
  504. assertEqual("RAM", node.RAM, tc.expectedRAM)
  505. assertEqual("ArchType", node.ArchType, "amd64")
  506. if meta.Source != "static-fallback" {
  507. t.Errorf("expected metadata source to be 'static-fallback', got: %s", meta.Source)
  508. }
  509. })
  510. }
  511. }