provider_test.go 16 KB

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