provider_test.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. package gcp
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "os"
  6. "reflect"
  7. "testing"
  8. "github.com/opencost/opencost/pkg/cloud/models"
  9. )
  10. func TestParseGCPInstanceTypeLabel(t *testing.T) {
  11. cases := []struct {
  12. input string
  13. expected string
  14. }{
  15. {
  16. input: "n1-standard-2",
  17. expected: "n1standard",
  18. },
  19. {
  20. input: "e2-medium",
  21. expected: "e2medium",
  22. },
  23. {
  24. input: "k3s",
  25. expected: "unknown",
  26. },
  27. {
  28. input: "custom-n1-standard-2",
  29. expected: "custom",
  30. },
  31. {
  32. input: "n2d-highmem-8",
  33. expected: "n2dstandard",
  34. },
  35. }
  36. for _, test := range cases {
  37. result := parseGCPInstanceTypeLabel(test.input)
  38. if result != test.expected {
  39. t.Errorf("Input: %s, Expected: %s, Actual: %s", test.input, test.expected, result)
  40. }
  41. }
  42. }
  43. func TestParseGCPProjectID(t *testing.T) {
  44. cases := []struct {
  45. input string
  46. expected string
  47. }{
  48. {
  49. input: "gce://guestbook-12345/...",
  50. expected: "guestbook-12345",
  51. },
  52. {
  53. input: "gce:/guestbook-12345/...",
  54. expected: "",
  55. },
  56. {
  57. input: "asdfa",
  58. expected: "",
  59. },
  60. {
  61. input: "",
  62. expected: "",
  63. },
  64. }
  65. for _, test := range cases {
  66. result := ParseGCPProjectID(test.input)
  67. if result != test.expected {
  68. t.Errorf("Input: %s, Expected: %s, Actual: %s", test.input, test.expected, result)
  69. }
  70. }
  71. }
  72. func TestGetUsageType(t *testing.T) {
  73. cases := []struct {
  74. input map[string]string
  75. expected string
  76. }{
  77. {
  78. input: map[string]string{
  79. GKEPreemptibleLabel: "true",
  80. },
  81. expected: "preemptible",
  82. },
  83. {
  84. input: map[string]string{
  85. GKESpotLabel: "true",
  86. },
  87. expected: "preemptible",
  88. },
  89. {
  90. input: map[string]string{
  91. models.KarpenterCapacityTypeLabel: models.KarpenterCapacitySpotTypeValue,
  92. },
  93. expected: "preemptible",
  94. },
  95. {
  96. input: map[string]string{
  97. "someotherlabel": "true",
  98. },
  99. expected: "ondemand",
  100. },
  101. {
  102. input: map[string]string{},
  103. expected: "ondemand",
  104. },
  105. }
  106. for _, test := range cases {
  107. result := getUsageType(test.input)
  108. if result != test.expected {
  109. t.Errorf("Input: %v, Expected: %s, Actual: %s", test.input, test.expected, result)
  110. }
  111. }
  112. }
  113. func TestKeyFeatures(t *testing.T) {
  114. type testCase struct {
  115. key *gcpKey
  116. exp string
  117. }
  118. testCases := []testCase{
  119. {
  120. key: &gcpKey{
  121. Labels: map[string]string{
  122. "node.kubernetes.io/instance-type": "n2-standard-4",
  123. "topology.kubernetes.io/region": "us-east1",
  124. },
  125. },
  126. exp: "us-east1,n2standard,ondemand",
  127. },
  128. {
  129. key: &gcpKey{
  130. Labels: map[string]string{
  131. "node.kubernetes.io/instance-type": "e2-standard-8",
  132. "topology.kubernetes.io/region": "us-west1",
  133. "cloud.google.com/gke-preemptible": "true",
  134. },
  135. },
  136. exp: "us-west1,e2standard,preemptible",
  137. },
  138. {
  139. key: &gcpKey{
  140. Labels: map[string]string{
  141. "node.kubernetes.io/instance-type": "a2-highgpu-1g",
  142. "cloud.google.com/gke-gpu": "true",
  143. "cloud.google.com/gke-accelerator": "nvidia-tesla-a100",
  144. "topology.kubernetes.io/region": "us-central1",
  145. },
  146. },
  147. exp: "us-central1,a2highgpu,ondemand,gpu",
  148. },
  149. {
  150. key: &gcpKey{
  151. Labels: map[string]string{
  152. "node.kubernetes.io/instance-type": "t2d-standard-1",
  153. "topology.kubernetes.io/region": "asia-southeast1",
  154. },
  155. },
  156. exp: "asia-southeast1,t2dstandard,ondemand",
  157. },
  158. }
  159. for _, tc := range testCases {
  160. t.Run(tc.exp, func(t *testing.T) {
  161. act := tc.key.Features()
  162. if act != tc.exp {
  163. t.Errorf("expected '%s'; got '%s'", tc.exp, act)
  164. }
  165. })
  166. }
  167. }
  168. // tests basic parsing of GCP pricing API responses
  169. // Load a reader object on a portion of a GCP api response
  170. // Confirm that the resting *GCP object contains the correctly parsed pricing info
  171. func TestParsePage(t *testing.T) {
  172. // NOTE: SKUs here are copied directly from GCP Billing API. Some of them
  173. // are in currency IDR, which relates directly to ticket GTM-52, for which
  174. // some of this work was done. So if the prices look huge... don't panic.
  175. // The only thing we're testing here is that, given these instance types
  176. // and regions and prices, those same prices get set appropriately into
  177. // the returned pricing map.
  178. skuFilePath := "./test/skus.json"
  179. fileBytes, err := os.ReadFile(skuFilePath)
  180. if err != nil {
  181. t.Fatalf("failed to open file '%s': %s", skuFilePath, err)
  182. }
  183. reader := bytes.NewReader(fileBytes)
  184. testGcp := &GCP{}
  185. inputKeys := map[string]models.Key{
  186. "us-central1,a2highgpu,ondemand,gpu": &gcpKey{
  187. Labels: map[string]string{
  188. "node.kubernetes.io/instance-type": "a2-highgpu-1g",
  189. "cloud.google.com/gke-gpu": "true",
  190. "cloud.google.com/gke-accelerator": "nvidia-tesla-a100",
  191. "topology.kubernetes.io/region": "us-central1",
  192. },
  193. },
  194. "us-central1,e2medium,ondemand": &gcpKey{
  195. Labels: map[string]string{
  196. "node.kubernetes.io/instance-type": "e2-medium",
  197. "topology.kubernetes.io/region": "us-central1",
  198. },
  199. },
  200. "us-central1,e2standard,ondemand": &gcpKey{
  201. Labels: map[string]string{
  202. "node.kubernetes.io/instance-type": "e2-standard",
  203. "topology.kubernetes.io/region": "us-central1",
  204. },
  205. },
  206. "asia-southeast1,t2dstandard,ondemand": &gcpKey{
  207. Labels: map[string]string{
  208. "node.kubernetes.io/instance-type": "t2d-standard-1",
  209. "topology.kubernetes.io/region": "asia-southeast1",
  210. },
  211. },
  212. }
  213. pvKeys := map[string]models.PVKey{}
  214. actualPrices, token, err := testGcp.parsePage(reader, inputKeys, pvKeys)
  215. if err != nil {
  216. t.Fatalf("got error parsing page: %v", err)
  217. }
  218. const expectedToken = "APKCS1HVa0YpwgyTFbqbJ1eGwzKZmsPwLqzMZPTSNia5ck1Hc54Tx_Kz3oBxwSnRIdGVxXoSPdf-XlDpyNBf4QuxKcIEgtrQ1LDLWAgZowI0ns7HjrGta2s="
  219. if token != expectedToken {
  220. t.Fatalf("error parsing GCP next page token, parsed %s but expected %s", token, expectedToken)
  221. }
  222. expectedActualPrices := map[string]*GCPPricing{
  223. "us-central1,a2highgpu,ondemand,gpu": {
  224. Name: "services/6F81-5844-456A/skus/039F-D0DA-4055",
  225. SKUID: "039F-D0DA-4055",
  226. Description: "Nvidia Tesla A100 GPU running in Americas",
  227. Category: &GCPResourceInfo{
  228. ServiceDisplayName: "Compute Engine",
  229. ResourceFamily: "Compute",
  230. ResourceGroup: "GPU",
  231. UsageType: "OnDemand",
  232. },
  233. ServiceRegions: []string{"us-central1", "us-east1", "us-west1"},
  234. PricingInfo: []*PricingInfo{
  235. {
  236. Summary: "",
  237. PricingExpression: &PricingExpression{
  238. UsageUnit: "h",
  239. UsageUnitDescription: "hour",
  240. BaseUnit: "s",
  241. BaseUnitConversionFactor: 0,
  242. DisplayQuantity: 1,
  243. TieredRates: []*TieredRates{
  244. {
  245. StartUsageAmount: 0,
  246. UnitPrice: &UnitPriceInfo{
  247. CurrencyCode: "USD",
  248. Units: "2",
  249. Nanos: 933908000,
  250. },
  251. },
  252. },
  253. },
  254. CurrencyConversionRate: 1,
  255. EffectiveTime: "2023-03-24T10:52:50.681Z",
  256. },
  257. },
  258. ServiceProviderName: "Google",
  259. Node: &models.Node{
  260. VCPUCost: "0.031611",
  261. RAMCost: "0.004237",
  262. UsesBaseCPUPrice: false,
  263. GPU: "1",
  264. GPUName: "nvidia-tesla-a100",
  265. GPUCost: "2.933908",
  266. },
  267. },
  268. "us-central1,a2highgpu,ondemand": {
  269. Node: &models.Node{
  270. VCPUCost: "0.031611",
  271. RAMCost: "0.004237",
  272. UsesBaseCPUPrice: false,
  273. UsageType: "ondemand",
  274. },
  275. },
  276. "us-central1,e2medium,ondemand": {
  277. Node: &models.Node{
  278. VCPU: "1.000000",
  279. VCPUCost: "327.173848364",
  280. RAMCost: "43.85294978",
  281. UsesBaseCPUPrice: false,
  282. UsageType: "ondemand",
  283. },
  284. },
  285. "us-central1,e2medium,ondemand,gpu": {
  286. Node: &models.Node{
  287. VCPU: "1.000000",
  288. VCPUCost: "327.173848364",
  289. RAMCost: "43.85294978",
  290. UsesBaseCPUPrice: false,
  291. UsageType: "ondemand",
  292. },
  293. },
  294. "us-central1,e2standard,ondemand": {
  295. Node: &models.Node{
  296. VCPUCost: "327.173848364",
  297. RAMCost: "43.85294978",
  298. UsesBaseCPUPrice: false,
  299. UsageType: "ondemand",
  300. },
  301. },
  302. "us-central1,e2standard,ondemand,gpu": {
  303. Node: &models.Node{
  304. VCPUCost: "327.173848364",
  305. RAMCost: "43.85294978",
  306. UsesBaseCPUPrice: false,
  307. UsageType: "ondemand",
  308. },
  309. },
  310. "asia-southeast1,t2dstandard,ondemand": {
  311. Node: &models.Node{
  312. VCPUCost: "508.934997455",
  313. RAMCost: "68.204999658",
  314. UsesBaseCPUPrice: false,
  315. UsageType: "ondemand",
  316. },
  317. },
  318. "asia-southeast1,t2dstandard,ondemand,gpu": {
  319. Node: &models.Node{
  320. VCPUCost: "508.934997455",
  321. RAMCost: "68.204999658",
  322. UsesBaseCPUPrice: false,
  323. UsageType: "ondemand",
  324. },
  325. },
  326. }
  327. if !reflect.DeepEqual(actualPrices, expectedActualPrices) {
  328. act, _ := json.Marshal(actualPrices)
  329. exp, _ := json.Marshal(expectedActualPrices)
  330. t.Errorf("error parsing GCP prices: parsed \n%s\n expected \n%s\n", string(act), string(exp))
  331. }
  332. }