provider_test.go 10.0 KB

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