provider_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. package aws
  2. import (
  3. "bytes"
  4. "io"
  5. "net/http"
  6. "net/url"
  7. "reflect"
  8. "testing"
  9. "github.com/opencost/opencost/pkg/cloud/models"
  10. v1 "k8s.io/api/core/v1"
  11. )
  12. func Test_awsKey_getUsageType(t *testing.T) {
  13. type fields struct {
  14. Labels map[string]string
  15. ProviderID string
  16. }
  17. type args struct {
  18. labels map[string]string
  19. }
  20. tests := []struct {
  21. name string
  22. fields fields
  23. args args
  24. want string
  25. }{
  26. {
  27. // test with no labels should return false
  28. name: "Label does not have the capacityType label associated with it",
  29. args: args{
  30. labels: map[string]string{},
  31. },
  32. want: "",
  33. },
  34. {
  35. name: "EKS label with a capacityType set to empty string should return empty string",
  36. args: args{
  37. labels: map[string]string{
  38. EKSCapacityTypeLabel: "",
  39. },
  40. },
  41. want: "",
  42. },
  43. {
  44. name: "EKS label with capacityType set to a random value should return empty string",
  45. args: args{
  46. labels: map[string]string{
  47. EKSCapacityTypeLabel: "TEST_ME",
  48. },
  49. },
  50. want: "",
  51. },
  52. {
  53. name: "EKS label with capacityType set to spot should return spot",
  54. args: args{
  55. labels: map[string]string{
  56. EKSCapacityTypeLabel: EKSCapacitySpotTypeValue,
  57. },
  58. },
  59. want: PreemptibleType,
  60. },
  61. {
  62. name: "Karpenter label with a capacityType set to empty string should return empty string",
  63. args: args{
  64. labels: map[string]string{
  65. models.KarpenterCapacityTypeLabel: "",
  66. },
  67. },
  68. want: "",
  69. },
  70. {
  71. name: "Karpenter label with capacityType set to a random value should return empty string",
  72. args: args{
  73. labels: map[string]string{
  74. models.KarpenterCapacityTypeLabel: "TEST_ME",
  75. },
  76. },
  77. want: "",
  78. },
  79. {
  80. name: "Karpenter label with capacityType set to spot should return spot",
  81. args: args{
  82. labels: map[string]string{
  83. models.KarpenterCapacityTypeLabel: models.KarpenterCapacitySpotTypeValue,
  84. },
  85. },
  86. want: PreemptibleType,
  87. },
  88. }
  89. for _, tt := range tests {
  90. t.Run(tt.name, func(t *testing.T) {
  91. k := &awsKey{
  92. Labels: tt.fields.Labels,
  93. ProviderID: tt.fields.ProviderID,
  94. }
  95. if got := k.getUsageType(tt.args.labels); got != tt.want {
  96. t.Errorf("getUsageType() = %v, want %v", got, tt.want)
  97. }
  98. })
  99. }
  100. }
  101. // Test_populate_pricing
  102. //
  103. // Objective: To test core pricing population logic for AWS
  104. //
  105. // Case 0: US endpoints
  106. // Take a portion of json returned from ondemand terms in us endpoints
  107. // load the request into the http response and give it to the function
  108. // inspect the resulting aws object after the function returns and validate fields
  109. // Case 1: Chinese endpoints
  110. // Same as above US test case, except using CN PV offer codes
  111. // Validate populated fields in AWS object
  112. func Test_populate_pricing(t *testing.T) {
  113. awsTest := AWS{
  114. ValidPricingKeys: map[string]bool{},
  115. }
  116. inputkeys := map[string]bool{
  117. "us-east-2,m5.large,linux": true,
  118. }
  119. // Case 0
  120. awsUSEastString := `
  121. {
  122. "formatVersion" : "v1.0",
  123. "disclaimer" : "This pricing list is for informational purposes only. All prices are subject to the additional terms included in the pricing pages on http://aws.amazon.com. All Free Tier prices are also subject to the terms included at https://aws.amazon.com/free/",
  124. "offerCode" : "AmazonEC2",
  125. "version" : "20230322145651",
  126. "publicationDate" : "2023-03-22T14:56:51Z",
  127. "products" : {
  128. "8D49XP354UEYTHGM" : {
  129. "sku" : "8D49XP354UEYTHGM",
  130. "productFamily" : "Compute Instance",
  131. "attributes" : {
  132. "servicecode" : "AmazonEC2",
  133. "location" : "US East (Ohio)",
  134. "locationType" : "AWS Region",
  135. "instanceType" : "m5.large",
  136. "currentGeneration" : "Yes",
  137. "instanceFamily" : "General purpose",
  138. "vcpu" : "2",
  139. "physicalProcessor" : "Intel Xeon Platinum 8175",
  140. "clockSpeed" : "3.1 GHz",
  141. "memory" : "8 GiB",
  142. "storage" : "EBS only",
  143. "networkPerformance" : "Up to 10 Gigabit",
  144. "processorArchitecture" : "64-bit",
  145. "tenancy" : "Shared",
  146. "operatingSystem" : "Linux",
  147. "licenseModel" : "No License required",
  148. "usagetype" : "USE2-BoxUsage:m5.large",
  149. "operation" : "RunInstances",
  150. "availabilityzone" : "NA",
  151. "capacitystatus" : "Used",
  152. "classicnetworkingsupport" : "false",
  153. "dedicatedEbsThroughput" : "Up to 2120 Mbps",
  154. "ecu" : "10",
  155. "enhancedNetworkingSupported" : "Yes",
  156. "gpuMemory" : "NA",
  157. "intelAvxAvailable" : "Yes",
  158. "intelAvx2Available" : "Yes",
  159. "intelTurboAvailable" : "Yes",
  160. "marketoption" : "OnDemand",
  161. "normalizationSizeFactor" : "4",
  162. "preInstalledSw" : "NA",
  163. "processorFeatures" : "Intel AVX; Intel AVX2; Intel AVX512; Intel Turbo",
  164. "regionCode" : "us-east-2",
  165. "servicename" : "Amazon Elastic Compute Cloud",
  166. "vpcnetworkingsupport" : "true"
  167. }
  168. },
  169. "9ZEEN7WWWQKAG292" : {
  170. "sku" : "9ZEEN7WWWQKAG292",
  171. "productFamily" : "Compute Instance",
  172. "attributes" : {
  173. "servicecode" : "AmazonEC2",
  174. "location" : "US East (Ohio)",
  175. "locationType" : "AWS Region",
  176. "instanceType" : "p3.8xlarge",
  177. "currentGeneration" : "Yes",
  178. "instanceFamily" : "GPU instance",
  179. "vcpu" : "32",
  180. "physicalProcessor" : "Intel Xeon E5-2686 v4 (Broadwell)",
  181. "clockSpeed" : "2.3 GHz",
  182. "memory" : "244 GiB",
  183. "storage" : "EBS only",
  184. "networkPerformance" : "10 Gigabit",
  185. "processorArchitecture" : "64-bit",
  186. "tenancy" : "Shared",
  187. "operatingSystem" : "Windows",
  188. "licenseModel" : "Bring your own license",
  189. "usagetype" : "USE2-BoxUsage:p3.8xlarge",
  190. "operation" : "RunInstances:0800",
  191. "availabilityzone" : "NA",
  192. "capacitystatus" : "Used",
  193. "classicnetworkingsupport" : "false",
  194. "dedicatedEbsThroughput" : "7000 Mbps",
  195. "ecu" : "97",
  196. "enhancedNetworkingSupported" : "Yes",
  197. "gpu" : "4",
  198. "gpuMemory" : "NA",
  199. "intelAvxAvailable" : "Yes",
  200. "intelAvx2Available" : "Yes",
  201. "intelTurboAvailable" : "Yes",
  202. "marketoption" : "OnDemand",
  203. "normalizationSizeFactor" : "64",
  204. "preInstalledSw" : "NA",
  205. "processorFeatures" : "Intel AVX; Intel AVX2; Intel Turbo",
  206. "regionCode" : "us-east-2",
  207. "servicename" : "Amazon Elastic Compute Cloud",
  208. "vpcnetworkingsupport" : "true"
  209. }
  210. },
  211. "M6UGCCQ3CDJQAA37" : {
  212. "sku" : "M6UGCCQ3CDJQAA37",
  213. "productFamily" : "Storage",
  214. "attributes" : {
  215. "servicecode" : "AmazonEC2",
  216. "location" : "US East (Ohio)",
  217. "locationType" : "AWS Region",
  218. "storageMedia" : "SSD-backed",
  219. "volumeType" : "General Purpose",
  220. "maxVolumeSize" : "16 TiB",
  221. "maxIopsvolume" : "16000",
  222. "maxThroughputvolume" : "1000 MiB/s",
  223. "usagetype" : "USE2-EBS:VolumeUsage.gp3",
  224. "operation" : "",
  225. "regionCode" : "us-east-2",
  226. "servicename" : "Amazon Elastic Compute Cloud",
  227. "volumeApiName" : "gp3"
  228. }
  229. }
  230. },
  231. "terms" : {
  232. "OnDemand" : {
  233. "M6UGCCQ3CDJQAA37" : {
  234. "M6UGCCQ3CDJQAA37.JRTCKXETXF" : {
  235. "offerTermCode" : "JRTCKXETXF",
  236. "sku" : "M6UGCCQ3CDJQAA37",
  237. "effectiveDate" : "2023-03-01T00:00:00Z",
  238. "priceDimensions" : {
  239. "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7" : {
  240. "rateCode" : "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7",
  241. "description" : "$0.08 per GB-month of General Purpose (gp3) provisioned storage - US East (Ohio)",
  242. "beginRange" : "0",
  243. "endRange" : "Inf",
  244. "unit" : "GB-Mo",
  245. "pricePerUnit" : {
  246. "USD" : "0.0800000000"
  247. },
  248. "appliesTo" : [ ]
  249. }
  250. },
  251. "termAttributes" : { }
  252. }
  253. },
  254. "9ZEEN7WWWQKAG292" : {
  255. "9ZEEN7WWWQKAG292.JRTCKXETXF" : {
  256. "offerTermCode" : "JRTCKXETXF",
  257. "sku" : "9ZEEN7WWWQKAG292",
  258. "effectiveDate" : "2023-03-01T00:00:00Z",
  259. "priceDimensions" : {
  260. "9ZEEN7WWWQKAG292.JRTCKXETXF.6YS6EN2CT7" : {
  261. "rateCode" : "9ZEEN7WWWQKAG292.JRTCKXETXF.6YS6EN2CT7",
  262. "description" : "$12.24 per On Demand Windows BYOL p3.8xlarge Instance Hour",
  263. "beginRange" : "0",
  264. "endRange" : "Inf",
  265. "unit" : "Hrs",
  266. "pricePerUnit" : {
  267. "USD" : "12.2400000000"
  268. },
  269. "appliesTo" : [ ]
  270. }
  271. },
  272. "termAttributes" : { }
  273. }
  274. },
  275. "8D49XP354UEYTHGM" : {
  276. "8D49XP354UEYTHGM.MZU6U2429S" : {
  277. "offerTermCode" : "MZU6U2429S",
  278. "sku" : "8D49XP354UEYTHGM",
  279. "effectiveDate" : "2019-01-01T00:00:00Z",
  280. "priceDimensions" : {
  281. "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U" : {
  282. "rateCode" : "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U",
  283. "description" : "Upfront Fee",
  284. "unit" : "Quantity",
  285. "pricePerUnit" : {
  286. "USD" : "1161"
  287. },
  288. "appliesTo" : [ ]
  289. },
  290. },
  291. "termAttributes" : {
  292. "LeaseContractLength" : "3yr",
  293. "OfferingClass" : "convertible",
  294. "PurchaseOption" : "All Upfront"
  295. }
  296. }
  297. }
  298. }
  299. },
  300. "attributesList" : { }
  301. }
  302. `
  303. testResponse := http.Response{
  304. Body: io.NopCloser(bytes.NewBufferString(awsUSEastString)),
  305. Request: &http.Request{
  306. URL: &url.URL{
  307. Scheme: "https",
  308. Host: "test-aws-http-endpoint:443",
  309. },
  310. },
  311. }
  312. awsTest.populatePricing(&testResponse, inputkeys)
  313. expectedProdTermsDisk := &AWSProductTerms{
  314. Sku: "M6UGCCQ3CDJQAA37",
  315. Memory: "",
  316. Storage: "",
  317. VCpu: "",
  318. GPU: "",
  319. OnDemand: &AWSOfferTerm{
  320. Sku: "M6UGCCQ3CDJQAA37",
  321. OfferTermCode: "JRTCKXETXF",
  322. PriceDimensions: map[string]*AWSRateCode{
  323. "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7": {
  324. Unit: "GB-Mo",
  325. PricePerUnit: AWSCurrencyCode{
  326. USD: "0.0800000000",
  327. CNY: "",
  328. },
  329. },
  330. },
  331. },
  332. PV: &models.PV{
  333. Cost: "0.00010958904109589041",
  334. CostPerIO: "",
  335. Class: "gp3",
  336. Size: "",
  337. Region: "us-east-2",
  338. ProviderID: "",
  339. },
  340. }
  341. expectedProdTermsInstanceOndemand := &AWSProductTerms{
  342. Sku: "8D49XP354UEYTHGM",
  343. Memory: "8 GiB",
  344. Storage: "EBS only",
  345. VCpu: "2",
  346. GPU: "",
  347. OnDemand: &AWSOfferTerm{
  348. Sku: "",
  349. OfferTermCode: "",
  350. PriceDimensions: nil,
  351. },
  352. }
  353. expectedProdTermsInstanceSpot := &AWSProductTerms{
  354. Sku: "8D49XP354UEYTHGM",
  355. Memory: "8 GiB",
  356. Storage: "EBS only",
  357. VCpu: "2",
  358. GPU: "",
  359. OnDemand: &AWSOfferTerm{
  360. Sku: "",
  361. OfferTermCode: "",
  362. PriceDimensions: nil,
  363. },
  364. }
  365. expectedPricing := map[string]*AWSProductTerms{
  366. "us-east-2,EBS:VolumeUsage.gp3": expectedProdTermsDisk,
  367. "us-east-2,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
  368. "us-east-2,m5.large,linux": expectedProdTermsInstanceOndemand,
  369. "us-east-2,m5.large,linux,preemptible": expectedProdTermsInstanceSpot,
  370. }
  371. if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
  372. t.Fatalf("expected parsed pricing did not match actual parsed result (us-east-1)")
  373. }
  374. // Case 1
  375. awsCnString := `
  376. {
  377. "formatVersion" : "v1.0",
  378. "disclaimer" : "This pricing list is for informational purposes only. All prices are subject to the additional terms included in the pricing pages on http://www.amazonaws.cn.",
  379. "offerCode" : "AmazonEC2",
  380. "version" : "20230314154740",
  381. "publicationDate" : "2023-03-14T15:47:40Z",
  382. "products" : {
  383. "R83VXG9NAPDASEGN" : {
  384. "sku" : "R83VXG9NAPDASEGN",
  385. "productFamily" : "Storage",
  386. "attributes" : {
  387. "servicecode" : "AmazonEC2",
  388. "location" : "China (Ningxia)",
  389. "locationType" : "AWS Region",
  390. "storageMedia" : "SSD-backed",
  391. "volumeType" : "General Purpose",
  392. "maxVolumeSize" : "16 TiB",
  393. "maxIopsvolume" : "16000",
  394. "maxThroughputvolume" : "1000 MiB/s",
  395. "usagetype" : "CNW1-EBS:VolumeUsage.gp3",
  396. "operation" : "",
  397. "regionCode" : "cn-northwest-1",
  398. "servicename" : "Amazon Elastic Compute Cloud",
  399. "volumeApiName" : "gp3"
  400. }
  401. }
  402. },
  403. "terms" : {
  404. "OnDemand" : {
  405. "R83VXG9NAPDASEGN" : {
  406. "R83VXG9NAPDASEGN.5Y9WH78GDR" : {
  407. "offerTermCode" : "5Y9WH78GDR",
  408. "sku" : "R83VXG9NAPDASEGN",
  409. "effectiveDate" : "2023-03-01T00:00:00Z",
  410. "priceDimensions" : {
  411. "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6" : {
  412. "rateCode" : "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6",
  413. "description" : "0.5312 CNY per GB-month of General Purpose (gp3) provisioned storage - China (Ningxia)",
  414. "beginRange" : "0",
  415. "endRange" : "Inf",
  416. "unit" : "GB-Mo",
  417. "pricePerUnit" : {
  418. "CNY" : "0.5312000000"
  419. },
  420. "appliesTo" : [ ]
  421. }
  422. },
  423. "termAttributes" : { }
  424. }
  425. }
  426. }
  427. },
  428. "attributesList" : { }
  429. }
  430. `
  431. awsTest = AWS{
  432. ValidPricingKeys: map[string]bool{},
  433. }
  434. testResponse = http.Response{
  435. Body: io.NopCloser(bytes.NewBufferString(awsCnString)),
  436. Request: &http.Request{
  437. URL: &url.URL{
  438. Scheme: "https",
  439. Host: "test-aws-http-endpoint:443",
  440. },
  441. },
  442. }
  443. awsTest.populatePricing(&testResponse, inputkeys)
  444. expectedProdTermsDisk = &AWSProductTerms{
  445. Sku: "R83VXG9NAPDASEGN",
  446. Memory: "",
  447. Storage: "",
  448. VCpu: "",
  449. GPU: "",
  450. OnDemand: &AWSOfferTerm{
  451. Sku: "R83VXG9NAPDASEGN",
  452. OfferTermCode: "5Y9WH78GDR",
  453. PriceDimensions: map[string]*AWSRateCode{
  454. "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6": {
  455. Unit: "GB-Mo",
  456. PricePerUnit: AWSCurrencyCode{
  457. USD: "",
  458. CNY: "0.5312000000",
  459. },
  460. },
  461. },
  462. },
  463. PV: &models.PV{
  464. Cost: "0.0007276712328767123",
  465. CostPerIO: "",
  466. Class: "gp3",
  467. Size: "",
  468. Region: "cn-northwest-1",
  469. ProviderID: "",
  470. },
  471. }
  472. expectedPricing = map[string]*AWSProductTerms{
  473. "cn-northwest-1,EBS:VolumeUsage.gp3": expectedProdTermsDisk,
  474. "cn-northwest-1,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
  475. }
  476. if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
  477. t.Fatalf("expected parsed pricing did not match actual parsed result (cn)")
  478. }
  479. }
  480. func TestFeatures(t *testing.T) {
  481. testCases := map[string]struct {
  482. aws awsKey
  483. expected string
  484. }{
  485. "Spot from custom labels": {
  486. aws: awsKey{
  487. SpotLabelName: "node-type",
  488. SpotLabelValue: "node-spot",
  489. Labels: map[string]string{
  490. "node-type": "node-spot",
  491. v1.LabelOSStable: "linux",
  492. v1.LabelHostname: "my-hostname",
  493. v1.LabelTopologyRegion: "us-west-2",
  494. v1.LabelTopologyZone: "us-west-2b",
  495. v1.LabelInstanceTypeStable: "m5.large",
  496. },
  497. },
  498. expected: "us-west-2,m5.large,linux,preemptible",
  499. },
  500. }
  501. for name, tc := range testCases {
  502. t.Run(name, func(t *testing.T) {
  503. features := tc.aws.Features()
  504. if features != tc.expected {
  505. t.Errorf("expected %s, got %s", tc.expected, features)
  506. }
  507. })
  508. }
  509. }
  510. func Test_getStorageClassTypeFrom(t *testing.T) {
  511. tests := []struct {
  512. name string
  513. provisioner string
  514. want string
  515. }{
  516. {
  517. name: "empty-provisioner",
  518. provisioner: "",
  519. want: "",
  520. },
  521. {
  522. name: "ebs-default-provisioner",
  523. provisioner: "kubernetes.io/aws-ebs",
  524. want: "gp2",
  525. },
  526. {
  527. name: "ebs-csi-provisioner",
  528. provisioner: "ebs.csi.aws.com",
  529. want: "gp3",
  530. },
  531. {
  532. name: "unknown-provisioner",
  533. provisioner: "unknown",
  534. want: "",
  535. },
  536. }
  537. for _, tt := range tests {
  538. t.Run(tt.name, func(t *testing.T) {
  539. if got := getStorageClassTypeFrom(tt.provisioner); got != tt.want {
  540. t.Errorf("getStorageClassTypeFrom() = %v, want %v", got, tt.want)
  541. }
  542. })
  543. }
  544. }