provider_test.go 9.8 KB

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