| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- package digitalocean
- import (
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
- "github.com/opencost/opencost/pkg/cloud/models"
- )
- func newTestProviderWithFile(t *testing.T, filename string) (*DOKS, func() int) {
- t.Helper()
- data, err := os.ReadFile(filename)
- if err != nil {
- t.Fatalf("Failed to read file: %v", err)
- }
- // Set a fake token for testing
- t.Setenv("DIGITALOCEAN_ACCESS_TOKEN", "test_token_dop_v1_fake")
- var count int
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- count++
- w.Header().Set("Content-Type", "application/json")
- _, _ = w.Write(data)
- }))
- t.Cleanup(server.Close)
- provider := NewDOKSProvider(server.URL)
- return provider, func() int { return count }
- }
- func newTestProviderWith404(t *testing.T) *DOKS {
- t.Helper()
- // Set a fake token for testing
- t.Setenv("DIGITALOCEAN_ACCESS_TOKEN", "test_token_dop_v1_fake")
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNotFound)
- }))
- t.Cleanup(server.Close)
- provider := NewDOKSProvider(server.URL)
- return provider
- }
- func TestNodePricing_APIMatches(t *testing.T) {
- provider, callCount := newTestProviderWithFile(t, "testdata/do_pricing.json")
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": "s-1vcpu-2gb",
- "kubernetes.io/arch": "amd64",
- },
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("expected no error, got: %v", err)
- }
- if node == nil {
- t.Fatal("expected node pricing, got nil")
- }
- assertEqual := func(name, got, want string) {
- if got != want {
- t.Errorf("%s: got %s, want %s", name, got, want)
- }
- }
- assertEqual("Cost", node.Cost, "0.01786")
- assertEqual("VCPUCost", node.VCPUCost, "0.00595") // 1/3
- assertEqual("RAMCost", node.RAMCost, "0.01191") // 2/3
- assertEqual("VCPU", node.VCPU, "1")
- assertEqual("RAM", node.RAM, "2GiB")
- assertEqual("ArchType", node.ArchType, "amd64")
- assertEqual("PricingType", string(node.PricingType), string(models.DefaultPrices))
- if meta.Source != "digitalocean-sizes-api" {
- t.Errorf("expected metadata source to be digitalocean-sizes-api, got: %s", meta.Source)
- }
- if c := callCount(); c != 1 {
- t.Errorf("expected 1 API call, got %d", c)
- }
- }
- func TestNodePricing_S2(t *testing.T) {
- provider, callCount := newTestProviderWithFile(t, "testdata/do_pricing.json")
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": "s-2vcpu-4gb",
- "kubernetes.io/arch": "amd64",
- },
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("expected no error, got: %v", err)
- }
- if node == nil {
- t.Fatal("expected node pricing, got nil")
- }
- assertEqual := func(name, got, want string) {
- if got != want {
- t.Errorf("%s: got %s, want %s", name, got, want)
- }
- }
- assertEqual("Cost", node.Cost, "0.03571")
- assertEqual("VCPUCost", node.VCPUCost, "0.01190")
- assertEqual("RAMCost", node.RAMCost, "0.02381")
- assertEqual("VCPU", node.VCPU, "2")
- assertEqual("RAM", node.RAM, "4GiB")
- assertEqual("ArchType", node.ArchType, "amd64")
- assertEqual("PricingType", string(node.PricingType), string(models.DefaultPrices))
- if meta.Source != "digitalocean-sizes-api" {
- t.Errorf("expected metadata source to be digitalocean-sizes-api, got: %s", meta.Source)
- }
- if c := callCount(); c != 1 {
- t.Errorf("expected 1 API call, got %d", c)
- }
- }
- func TestNodePricing_Estimation_C8Intel(t *testing.T) {
- provider := newTestProviderWith404(t)
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": "c-8-intel",
- "kubernetes.io/arch": "amd64",
- },
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("expected no error, got: %v", err)
- }
- expectedCost := "0.32440"
- expectedVCPUCost := "0.01352"
- expectedRAMCost := "0.01352"
- if node.Cost != expectedCost {
- t.Errorf("Cost: got %s, want %s", node.Cost, expectedCost)
- }
- if node.VCPUCost != expectedVCPUCost {
- t.Errorf("VCPUCost: got %s, want %s", node.VCPUCost, expectedVCPUCost)
- }
- if node.RAMCost != expectedRAMCost {
- t.Errorf("RAMCost: got %s, want %s", node.RAMCost, expectedRAMCost)
- }
- if node.VCPU != "8" {
- t.Errorf("VCPU: got %s, want 8", node.VCPU)
- }
- if node.RAM != "16GiB" {
- t.Errorf("RAM: got %s, want 16GiB", node.RAM)
- }
- if meta.Source != "static-fallback" {
- t.Errorf("expected metadata source to be estimated, got: %s", meta.Source)
- }
- }
- func TestNodePricing_EstimationFromSlug(t *testing.T) {
- tests := []struct {
- name string
- slug string
- expectedVCPU string
- expectedRAM string
- expectedCost string
- expectedCPU string
- expectedRAMCost string
- }{
- {
- name: "s-4vcpu-8gb",
- slug: "s-4vcpu-8gb",
- expectedVCPU: "4",
- expectedRAM: "8GiB",
- expectedCost: "0.07143",
- expectedCPU: "0.00595",
- expectedRAMCost: "0.00595",
- },
- {
- name: "m-8vcpu-64gb",
- slug: "m-8vcpu-64gb",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.50000",
- expectedCPU: "0.00694",
- expectedRAMCost: "0.00694",
- },
- {
- name: "g-4vcpu-16gb-intel",
- slug: "g-4vcpu-16gb-intel",
- expectedVCPU: "4",
- expectedRAM: "16GiB",
- expectedCost: "0.22470",
- expectedCPU: "0.01124",
- expectedRAMCost: "0.01124",
- },
- }
- provider := newTestProviderWith404(t) // Force fallback/estimate
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": tc.slug,
- "kubernetes.io/arch": "amd64",
- },
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if node == nil {
- t.Fatal("expected node to be non-nil")
- }
- assertEqual := func(field, got, want string) {
- if got != want {
- t.Errorf("%s: got %s, want %s", field, got, want)
- }
- }
- assertEqual("Cost", node.Cost, tc.expectedCost)
- assertEqual("VCPUCost", node.VCPUCost, tc.expectedCPU)
- assertEqual("RAMCost", node.RAMCost, tc.expectedRAMCost)
- assertEqual("VCPU", node.VCPU, tc.expectedVCPU)
- assertEqual("RAM", node.RAM, tc.expectedRAM)
- assertEqual("ArchType", node.ArchType, "amd64")
- if meta.Source != "static-fallback" {
- t.Errorf("expected metadata source to be 'estimated', got: %s", meta.Source)
- }
- })
- }
- }
- func TestNodePricing_Estimation_BaseSlugs(t *testing.T) {
- tests := []struct {
- name string
- slug string
- expectedVCPU string
- expectedRAM string
- expectedCost string
- expectedCPU string
- expectedRAMCost string
- }{
- {
- name: "c-8-intel",
- slug: "c-8-intel",
- expectedVCPU: "8",
- expectedRAM: "16GiB",
- expectedCost: "0.32440",
- expectedCPU: "0.01352",
- expectedRAMCost: "0.01352",
- },
- {
- name: "s-2vcpu-4gb",
- slug: "s-2vcpu-4gb",
- expectedVCPU: "2",
- expectedRAM: "4GiB",
- expectedCost: "0.03571",
- expectedCPU: "0.00595",
- expectedRAMCost: "0.00595",
- },
- {
- name: "m-4vcpu-32gb",
- slug: "m-4vcpu-32gb",
- expectedVCPU: "4",
- expectedRAM: "32GiB",
- expectedCost: "0.25000",
- expectedCPU: "0.00694",
- expectedRAMCost: "0.00694",
- },
- {
- name: "g-16vcpu-64gb-intel",
- slug: "g-16vcpu-64gb-intel",
- expectedVCPU: "16",
- expectedRAM: "64GiB",
- expectedCost: "0.89880",
- expectedCPU: "0.01124",
- expectedRAMCost: "0.01124",
- },
- }
- provider := newTestProviderWith404(t) // ensures fallback path is tested
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": tc.slug,
- "kubernetes.io/arch": "amd64",
- },
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if node == nil {
- t.Fatal("expected node to be non-nil")
- }
- assertEqual := func(field, got, want string) {
- if got != want {
- t.Errorf("%s: got %s, want %s", field, got, want)
- }
- }
- assertEqual("Cost", node.Cost, tc.expectedCost)
- assertEqual("VCPUCost", node.VCPUCost, tc.expectedCPU)
- assertEqual("RAMCost", node.RAMCost, tc.expectedRAMCost)
- assertEqual("VCPU", node.VCPU, tc.expectedVCPU)
- assertEqual("RAM", node.RAM, tc.expectedRAM)
- assertEqual("ArchType", node.ArchType, "amd64")
- if meta.Source != "static-fallback" {
- t.Errorf("expected metadata source to be 'static-fallback', got: %s", meta.Source)
- }
- })
- }
- }
- func TestNodePricing_Estimation_FamilySeeds(t *testing.T) {
- tests := []struct {
- name string
- slug string
- expectedVCPU string
- expectedRAM string
- expectedCost string
- expectedCPU string
- expectedRAMCost string
- }{
- {
- name: "c-16",
- slug: "c-16",
- expectedVCPU: "16",
- expectedRAM: "32GiB",
- expectedCost: "0.50000",
- expectedCPU: "0.01042",
- expectedRAMCost: "0.01042",
- },
- {
- name: "c-16-intel",
- slug: "c-16-intel",
- expectedVCPU: "16",
- expectedRAM: "32GiB",
- expectedCost: "0.64880",
- expectedCPU: "0.01352",
- expectedRAMCost: "0.01352",
- },
- {
- name: "c2-8vcpu-16gb",
- slug: "c2-8vcpu-16gb",
- expectedVCPU: "8",
- expectedRAM: "16GiB",
- expectedCost: "0.27976",
- expectedCPU: "0.01166",
- expectedRAMCost: "0.01166",
- },
- {
- name: "c2-8vcpu-16gb-intel",
- slug: "c2-8vcpu-16gb-intel",
- expectedVCPU: "8",
- expectedRAM: "16GiB",
- expectedCost: "0.36310",
- expectedCPU: "0.01513",
- expectedRAMCost: "0.01513",
- },
- {
- name: "g-8vcpu-32gb",
- slug: "g-8vcpu-32gb",
- expectedVCPU: "8",
- expectedRAM: "32GiB",
- expectedCost: "0.37500",
- expectedCPU: "0.00937",
- expectedRAMCost: "0.00937",
- },
- {
- name: "g-8vcpu-32gb-intel",
- slug: "g-8vcpu-32gb-intel",
- expectedVCPU: "8",
- expectedRAM: "32GiB",
- expectedCost: "0.44940",
- expectedCPU: "0.01124",
- expectedRAMCost: "0.01124",
- },
- {
- name: "gd-40vcpu-160gb",
- slug: "gd-40vcpu-160gb",
- expectedVCPU: "40",
- expectedRAM: "160GiB",
- expectedCost: "2.02380",
- expectedCPU: "0.01012",
- expectedRAMCost: "0.01012",
- },
- {
- name: "gd-16vcpu-64gb-intel",
- slug: "gd-16vcpu-64gb-intel",
- expectedVCPU: "16",
- expectedRAM: "64GiB",
- expectedCost: "0.94048",
- expectedCPU: "0.01176",
- expectedRAMCost: "0.01176",
- },
- {
- name: "m-16vcpu-128gb",
- slug: "m-16vcpu-128gb",
- expectedVCPU: "16",
- expectedRAM: "128GiB",
- expectedCost: "1.00000",
- expectedCPU: "0.00694",
- expectedRAMCost: "0.00694",
- },
- {
- name: "m-16vcpu-128gb-intel",
- slug: "m-16vcpu-128gb-intel",
- expectedVCPU: "16",
- expectedRAM: "128GiB",
- expectedCost: "1.17858",
- expectedCPU: "0.00818",
- expectedRAMCost: "0.00818",
- },
- // m3
- {
- name: "m3-8vcpu-64gb",
- slug: "m3-8vcpu-64gb",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.61905",
- expectedCPU: "0.00860",
- expectedRAMCost: "0.00860",
- },
- {
- name: "m3-32vcpu-256gb-intel",
- slug: "m3-32vcpu-256gb-intel",
- expectedVCPU: "32",
- expectedRAM: "256GiB",
- expectedCost: "2.61904",
- expectedCPU: "0.00909",
- expectedRAMCost: "0.00909",
- },
- {
- name: "m6-8vcpu-64gb",
- slug: "m6-8vcpu-64gb",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.77976",
- expectedCPU: "0.01083",
- expectedRAMCost: "0.01083",
- },
- {
- name: "m6-24vcpu-192gb",
- slug: "m6-24vcpu-192gb",
- expectedVCPU: "24",
- expectedRAM: "192GiB",
- expectedCost: "2.33928",
- expectedCPU: "0.01083",
- expectedRAMCost: "0.01083",
- },
- {
- name: "s-1vcpu-2gb",
- slug: "s-1vcpu-2gb",
- expectedVCPU: "1",
- expectedRAM: "2GiB",
- expectedCost: "0.01786",
- expectedCPU: "0.00595",
- expectedRAMCost: "0.00595",
- },
- {
- name: "s-8vcpu-16gb-intel",
- slug: "s-8vcpu-16gb-intel",
- expectedVCPU: "8",
- expectedRAM: "16GiB",
- expectedCost: "0.16666",
- expectedCPU: "0.00694",
- expectedRAMCost: "0.00694",
- },
- {
- name: "so-8vcpu-64gb",
- slug: "so-8vcpu-64gb",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.77976",
- expectedCPU: "0.01083",
- expectedRAMCost: "0.01083",
- },
- {
- name: "so-8vcpu-64gb-intel",
- slug: "so-8vcpu-64gb-intel",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.77976",
- expectedCPU: "0.01083",
- expectedRAMCost: "0.01083",
- },
- {
- name: "so1_5-8vcpu-64gb",
- slug: "so1_5-8vcpu-64gb",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.97024",
- expectedCPU: "0.01348",
- expectedRAMCost: "0.01348",
- },
- {
- name: "so1_5-8vcpu-64gb-intel",
- slug: "so1_5-8vcpu-64gb-intel",
- expectedVCPU: "8",
- expectedRAM: "64GiB",
- expectedCost: "0.82738",
- expectedCPU: "0.01149",
- expectedRAMCost: "0.01149",
- },
- }
- provider := newTestProviderWith404(t)
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": tc.slug,
- "kubernetes.io/arch": "amd64",
- },
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if node == nil {
- t.Fatal("expected node to be non-nil")
- }
- assertEqual := func(field, got, want string) {
- if got != want {
- t.Errorf("%s: got %s, want %s", field, got, want)
- }
- }
- assertEqual("Cost", node.Cost, tc.expectedCost)
- assertEqual("VCPUCost", node.VCPUCost, tc.expectedCPU)
- assertEqual("RAMCost", node.RAMCost, tc.expectedRAMCost)
- assertEqual("VCPU", node.VCPU, tc.expectedVCPU)
- assertEqual("RAM", node.RAM, tc.expectedRAM)
- assertEqual("ArchType", node.ArchType, "amd64")
- if meta.Source != "static-fallback" {
- t.Errorf("expected metadata source to be 'static-fallback', got: %s", meta.Source)
- }
- })
- }
- }
- func TestNodePricing_GPU(t *testing.T) {
- provider, callCount := newTestProviderWithFile(t, "testdata/do_pricing.json")
- key := &doksKey{
- Labels: map[string]string{
- "node.kubernetes.io/instance-type": "gpu-h100x1-80gb",
- "kubernetes.io/arch": "amd64",
- },
- }
- // Verify key methods - might return defaults but shouldn't panic
- if count := key.GPUCount(); count != 1 {
- t.Errorf("expected GPUCount 1, got %d", count)
- }
- if gpuType := key.GPUType(); gpuType != "h100" {
- t.Errorf("expected GPUType h100, got %s", gpuType)
- }
- node, meta, err := provider.NodePricing(key)
- if err != nil {
- t.Fatalf("expected no error, got: %v", err)
- }
- if node == nil {
- t.Fatal("expected node pricing, got nil")
- }
- assertEqual := func(name, got, want string) {
- if got != want {
- t.Errorf("%s: got %s, want %s", name, got, want)
- }
- }
- assertEqual("Cost", node.Cost, "3.39000")
- assertEqual("VCPUCost", node.VCPUCost, "0.26077") // 3.39 * 20 / 260 = 0.260769...
- assertEqual("RAMCost", node.RAMCost, "3.12923") // 3.39 * 240 / 260 = 3.129230...
- assertEqual("VCPU", node.VCPU, "20")
- assertEqual("RAM", node.RAM, "240GiB")
- assertEqual("GPU", node.GPU, "1")
- assertEqual("GPUName", node.GPUName, "nvidia_h100")
- if meta.Source != "digitalocean-sizes-api" {
- t.Errorf("expected metadata source to be digitalocean-sizes-api, got: %s", meta.Source)
- }
- if c := callCount(); c != 1 {
- t.Errorf("expected 1 API call, got %d", c)
- }
- }
|