provider_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. package aws
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "reflect"
  10. "testing"
  11. "github.com/opencost/opencost/pkg/cloud/models"
  12. v1 "k8s.io/api/core/v1"
  13. )
  14. func Test_awsKey_getUsageType(t *testing.T) {
  15. type fields struct {
  16. Labels map[string]string
  17. ProviderID string
  18. }
  19. type args struct {
  20. labels map[string]string
  21. }
  22. tests := []struct {
  23. name string
  24. fields fields
  25. args args
  26. want string
  27. }{
  28. {
  29. // test with no labels should return false
  30. name: "Label does not have the capacityType label associated with it",
  31. args: args{
  32. labels: map[string]string{},
  33. },
  34. want: "",
  35. },
  36. {
  37. name: "EKS label with a capacityType set to empty string should return empty string",
  38. args: args{
  39. labels: map[string]string{
  40. EKSCapacityTypeLabel: "",
  41. },
  42. },
  43. want: "",
  44. },
  45. {
  46. name: "EKS label with capacityType set to a random value should return empty string",
  47. args: args{
  48. labels: map[string]string{
  49. EKSCapacityTypeLabel: "TEST_ME",
  50. },
  51. },
  52. want: "",
  53. },
  54. {
  55. name: "EKS label with capacityType set to spot should return spot",
  56. args: args{
  57. labels: map[string]string{
  58. EKSCapacityTypeLabel: EKSCapacitySpotTypeValue,
  59. },
  60. },
  61. want: PreemptibleType,
  62. },
  63. {
  64. name: "Karpenter label with a capacityType set to empty string should return empty string",
  65. args: args{
  66. labels: map[string]string{
  67. models.KarpenterCapacityTypeLabel: "",
  68. },
  69. },
  70. want: "",
  71. },
  72. {
  73. name: "Karpenter label with capacityType set to a random value should return empty string",
  74. args: args{
  75. labels: map[string]string{
  76. models.KarpenterCapacityTypeLabel: "TEST_ME",
  77. },
  78. },
  79. want: "",
  80. },
  81. {
  82. name: "Karpenter label with capacityType set to spot should return spot",
  83. args: args{
  84. labels: map[string]string{
  85. models.KarpenterCapacityTypeLabel: models.KarpenterCapacitySpotTypeValue,
  86. },
  87. },
  88. want: PreemptibleType,
  89. },
  90. }
  91. for _, tt := range tests {
  92. t.Run(tt.name, func(t *testing.T) {
  93. k := &awsKey{
  94. Labels: tt.fields.Labels,
  95. ProviderID: tt.fields.ProviderID,
  96. }
  97. if got := k.getUsageType(tt.args.labels); got != tt.want {
  98. t.Errorf("getUsageType() = %v, want %v", got, tt.want)
  99. }
  100. })
  101. }
  102. }
  103. // Test_PricingData_Regression
  104. //
  105. // Objective: To test the pricing data download and validate the schema is still
  106. // as expected
  107. //
  108. // These tests may take a long time to complete. It is downloading AWS Pricing
  109. // data files (~500MB) for each region.
  110. func Test_PricingData_Regression(t *testing.T) {
  111. if os.Getenv("INTEGRATION") == "" {
  112. t.Skip("skipping integration tests, set environment variable INTEGRATION")
  113. }
  114. awsRegions := []string{"us-east-1", "eu-west-1"}
  115. // Check pricing data produced for each region
  116. for _, region := range awsRegions {
  117. node := v1.Node{}
  118. node.SetLabels(map[string]string{"topology.kubernetes.io/region": region})
  119. awsTest := AWS{}
  120. res, _, err := awsTest.getRegionPricing([]*v1.Node{&node})
  121. if err != nil {
  122. t.Errorf("Failed to download pricing data for region %s: %v", region, err)
  123. }
  124. // Unmarshal pricing data into AWSPricing
  125. var pricingData AWSPricing
  126. body, err := io.ReadAll(res.Body)
  127. if err != nil {
  128. t.Errorf("Failed to read pricing data for region %s: %v", region, err)
  129. }
  130. err = json.Unmarshal(body, &pricingData)
  131. if err != nil {
  132. t.Errorf("Failed to unmarshal pricing data for region %s: %v", region, err)
  133. }
  134. // ASSERTION. We only anticipate "OnDemand" or "CapacityBlock" in the
  135. // pricing data.
  136. //
  137. // Failing this test does not necessarily mean we have regressed. Just
  138. // that we need to revisit this code to ensure OnDemand pricing is still
  139. // functioning as expected.
  140. for _, product := range pricingData.Products {
  141. if product.Attributes.MarketOption != "OnDemand" && product.Attributes.MarketOption != "CapacityBlock" && product.Attributes.MarketOption != "" {
  142. t.Errorf("Invalid marketOption for product %s: %s", product.Sku, product.Attributes.MarketOption)
  143. }
  144. }
  145. }
  146. }
  147. // Test_populate_pricing
  148. //
  149. // Objective: To test core pricing population logic for AWS
  150. //
  151. // Case 0: US endpoints
  152. // Take a portion of json returned from ondemand terms in us endpoints load the
  153. // request into the http response and give it to the function inspect the
  154. // resulting aws object after the function returns and validate fields
  155. //
  156. // Case 1: Ensure marketOption=OnDemand
  157. // AWS introduced the field marketOption. We need to further filter for
  158. // marketOption=OnDemand to ensure we are not getting pricing from a line item
  159. // such as marketOption=CapacityBlock
  160. //
  161. // Case 2: Chinese endpoints
  162. // Same as above US test case, except using CN PV offer codes. Validate
  163. // populated fields in AWS object
  164. func Test_populate_pricing(t *testing.T) {
  165. awsTest := AWS{
  166. ValidPricingKeys: map[string]bool{},
  167. }
  168. inputkeys := map[string]bool{
  169. "us-east-2,m5.large,linux": true,
  170. }
  171. // Case 0
  172. awsUSEastString := `
  173. {
  174. "formatVersion" : "v1.0",
  175. "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/",
  176. "offerCode" : "AmazonEC2",
  177. "version" : "20230322145651",
  178. "publicationDate" : "2023-03-22T14:56:51Z",
  179. "products" : {
  180. "8D49XP354UEYTHGM" : {
  181. "sku" : "8D49XP354UEYTHGM",
  182. "productFamily" : "Compute Instance",
  183. "attributes" : {
  184. "servicecode" : "AmazonEC2",
  185. "location" : "US East (Ohio)",
  186. "locationType" : "AWS Region",
  187. "instanceType" : "m5.large",
  188. "currentGeneration" : "Yes",
  189. "instanceFamily" : "General purpose",
  190. "vcpu" : "2",
  191. "physicalProcessor" : "Intel Xeon Platinum 8175",
  192. "clockSpeed" : "3.1 GHz",
  193. "memory" : "8 GiB",
  194. "storage" : "EBS only",
  195. "networkPerformance" : "Up to 10 Gigabit",
  196. "processorArchitecture" : "64-bit",
  197. "tenancy" : "Shared",
  198. "operatingSystem" : "Linux",
  199. "licenseModel" : "No License required",
  200. "usagetype" : "USE2-BoxUsage:m5.large",
  201. "operation" : "RunInstances",
  202. "availabilityzone" : "NA",
  203. "capacitystatus" : "Used",
  204. "classicnetworkingsupport" : "false",
  205. "dedicatedEbsThroughput" : "Up to 2120 Mbps",
  206. "ecu" : "10",
  207. "enhancedNetworkingSupported" : "Yes",
  208. "gpuMemory" : "NA",
  209. "intelAvxAvailable" : "Yes",
  210. "intelAvx2Available" : "Yes",
  211. "intelTurboAvailable" : "Yes",
  212. "marketoption" : "OnDemand",
  213. "normalizationSizeFactor" : "4",
  214. "preInstalledSw" : "NA",
  215. "processorFeatures" : "Intel AVX; Intel AVX2; Intel AVX512; Intel Turbo",
  216. "regionCode" : "us-east-2",
  217. "servicename" : "Amazon Elastic Compute Cloud",
  218. "vpcnetworkingsupport" : "true"
  219. }
  220. },
  221. "9ZEEN7WWWQKAG292" : {
  222. "sku" : "9ZEEN7WWWQKAG292",
  223. "productFamily" : "Compute Instance",
  224. "attributes" : {
  225. "servicecode" : "AmazonEC2",
  226. "location" : "US East (Ohio)",
  227. "locationType" : "AWS Region",
  228. "instanceType" : "p3.8xlarge",
  229. "currentGeneration" : "Yes",
  230. "instanceFamily" : "GPU instance",
  231. "vcpu" : "32",
  232. "physicalProcessor" : "Intel Xeon E5-2686 v4 (Broadwell)",
  233. "clockSpeed" : "2.3 GHz",
  234. "memory" : "244 GiB",
  235. "storage" : "EBS only",
  236. "networkPerformance" : "10 Gigabit",
  237. "processorArchitecture" : "64-bit",
  238. "tenancy" : "Shared",
  239. "operatingSystem" : "Windows",
  240. "licenseModel" : "Bring your own license",
  241. "usagetype" : "USE2-BoxUsage:p3.8xlarge",
  242. "operation" : "RunInstances:0800",
  243. "availabilityzone" : "NA",
  244. "capacitystatus" : "Used",
  245. "classicnetworkingsupport" : "false",
  246. "dedicatedEbsThroughput" : "7000 Mbps",
  247. "ecu" : "97",
  248. "enhancedNetworkingSupported" : "Yes",
  249. "gpu" : "4",
  250. "gpuMemory" : "NA",
  251. "intelAvxAvailable" : "Yes",
  252. "intelAvx2Available" : "Yes",
  253. "intelTurboAvailable" : "Yes",
  254. "marketoption" : "OnDemand",
  255. "normalizationSizeFactor" : "64",
  256. "preInstalledSw" : "NA",
  257. "processorFeatures" : "Intel AVX; Intel AVX2; Intel Turbo",
  258. "regionCode" : "us-east-2",
  259. "servicename" : "Amazon Elastic Compute Cloud",
  260. "vpcnetworkingsupport" : "true"
  261. }
  262. },
  263. "M6UGCCQ3CDJQAA37" : {
  264. "sku" : "M6UGCCQ3CDJQAA37",
  265. "productFamily" : "Storage",
  266. "attributes" : {
  267. "servicecode" : "AmazonEC2",
  268. "location" : "US East (Ohio)",
  269. "locationType" : "AWS Region",
  270. "storageMedia" : "SSD-backed",
  271. "volumeType" : "General Purpose",
  272. "maxVolumeSize" : "16 TiB",
  273. "maxIopsvolume" : "16000",
  274. "maxThroughputvolume" : "1000 MiB/s",
  275. "usagetype" : "USE2-EBS:VolumeUsage.gp3",
  276. "operation" : "",
  277. "regionCode" : "us-east-2",
  278. "servicename" : "Amazon Elastic Compute Cloud",
  279. "volumeApiName" : "gp3"
  280. }
  281. }
  282. },
  283. "terms" : {
  284. "OnDemand" : {
  285. "M6UGCCQ3CDJQAA37" : {
  286. "M6UGCCQ3CDJQAA37.JRTCKXETXF" : {
  287. "offerTermCode" : "JRTCKXETXF",
  288. "sku" : "M6UGCCQ3CDJQAA37",
  289. "effectiveDate" : "2023-03-01T00:00:00Z",
  290. "priceDimensions" : {
  291. "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7" : {
  292. "rateCode" : "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7",
  293. "description" : "$0.08 per GB-month of General Purpose (gp3) provisioned storage - US East (Ohio)",
  294. "beginRange" : "0",
  295. "endRange" : "Inf",
  296. "unit" : "GB-Mo",
  297. "pricePerUnit" : {
  298. "USD" : "0.0800000000"
  299. },
  300. "appliesTo" : [ ]
  301. }
  302. },
  303. "termAttributes" : { }
  304. }
  305. },
  306. "9ZEEN7WWWQKAG292" : {
  307. "9ZEEN7WWWQKAG292.JRTCKXETXF" : {
  308. "offerTermCode" : "JRTCKXETXF",
  309. "sku" : "9ZEEN7WWWQKAG292",
  310. "effectiveDate" : "2023-03-01T00:00:00Z",
  311. "priceDimensions" : {
  312. "9ZEEN7WWWQKAG292.JRTCKXETXF.6YS6EN2CT7" : {
  313. "rateCode" : "9ZEEN7WWWQKAG292.JRTCKXETXF.6YS6EN2CT7",
  314. "description" : "$12.24 per On Demand Windows BYOL p3.8xlarge Instance Hour",
  315. "beginRange" : "0",
  316. "endRange" : "Inf",
  317. "unit" : "Hrs",
  318. "pricePerUnit" : {
  319. "USD" : "12.2400000000"
  320. },
  321. "appliesTo" : [ ]
  322. }
  323. },
  324. "termAttributes" : { }
  325. }
  326. },
  327. "8D49XP354UEYTHGM" : {
  328. "8D49XP354UEYTHGM.MZU6U2429S" : {
  329. "offerTermCode" : "MZU6U2429S",
  330. "sku" : "8D49XP354UEYTHGM",
  331. "effectiveDate" : "2019-01-01T00:00:00Z",
  332. "priceDimensions" : {
  333. "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U" : {
  334. "rateCode" : "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U",
  335. "description" : "Upfront Fee",
  336. "unit" : "Quantity",
  337. "pricePerUnit" : {
  338. "USD" : "1161"
  339. },
  340. "appliesTo" : [ ]
  341. },
  342. },
  343. "termAttributes" : {
  344. "LeaseContractLength" : "3yr",
  345. "OfferingClass" : "convertible",
  346. "PurchaseOption" : "All Upfront"
  347. }
  348. }
  349. }
  350. }
  351. },
  352. "attributesList" : { }
  353. }
  354. `
  355. testResponse := http.Response{
  356. Body: io.NopCloser(bytes.NewBufferString(awsUSEastString)),
  357. Request: &http.Request{
  358. URL: &url.URL{
  359. Scheme: "https",
  360. Host: "test-aws-http-endpoint:443",
  361. },
  362. },
  363. }
  364. awsTest.populatePricing(&testResponse, inputkeys)
  365. expectedProdTermsDisk := &AWSProductTerms{
  366. Sku: "M6UGCCQ3CDJQAA37",
  367. Memory: "",
  368. Storage: "",
  369. VCpu: "",
  370. GPU: "",
  371. OnDemand: &AWSOfferTerm{
  372. Sku: "M6UGCCQ3CDJQAA37",
  373. OfferTermCode: "JRTCKXETXF",
  374. PriceDimensions: map[string]*AWSRateCode{
  375. "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7": {
  376. Unit: "GB-Mo",
  377. PricePerUnit: AWSCurrencyCode{
  378. USD: "0.0800000000",
  379. CNY: "",
  380. },
  381. },
  382. },
  383. },
  384. PV: &models.PV{
  385. Cost: "0.00010958904109589041",
  386. CostPerIO: "",
  387. Class: "gp3",
  388. Size: "",
  389. Region: "us-east-2",
  390. ProviderID: "",
  391. },
  392. }
  393. expectedProdTermsInstanceOndemand := &AWSProductTerms{
  394. Sku: "8D49XP354UEYTHGM",
  395. Memory: "8 GiB",
  396. Storage: "EBS only",
  397. VCpu: "2",
  398. GPU: "",
  399. OnDemand: &AWSOfferTerm{
  400. Sku: "",
  401. OfferTermCode: "",
  402. PriceDimensions: nil,
  403. },
  404. }
  405. expectedProdTermsInstanceSpot := &AWSProductTerms{
  406. Sku: "8D49XP354UEYTHGM",
  407. Memory: "8 GiB",
  408. Storage: "EBS only",
  409. VCpu: "2",
  410. GPU: "",
  411. OnDemand: &AWSOfferTerm{
  412. Sku: "",
  413. OfferTermCode: "",
  414. PriceDimensions: nil,
  415. },
  416. }
  417. expectedPricing := map[string]*AWSProductTerms{
  418. "us-east-2,EBS:VolumeUsage.gp3": expectedProdTermsDisk,
  419. "us-east-2,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
  420. "us-east-2,m5.large,linux": expectedProdTermsInstanceOndemand,
  421. "us-east-2,m5.large,linux,preemptible": expectedProdTermsInstanceSpot,
  422. }
  423. if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
  424. t.Fatalf("expected parsed pricing did not match actual parsed result (us-east-1)")
  425. }
  426. // Case 1 - Only accept `"marketoption":"OnDemand"`
  427. inputkeysCase1 := map[string]bool{
  428. "us-east-1,p4d.24xlarge,linux": true,
  429. }
  430. pricingCase1 := `
  431. {
  432. "formatVersion" : "v1.0",
  433. "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/",
  434. "offerCode" : "AmazonEC2",
  435. "version" : "20240528203522",
  436. "publicationDate" : "2024-05-28T20:35:22Z",
  437. "products" : {
  438. "H7NGEAC6UEHNTKSJ" : {
  439. "sku" : "H7NGEAC6UEHNTKSJ",
  440. "productFamily" : "Compute Instance",
  441. "attributes" : {
  442. "servicecode" : "AmazonEC2",
  443. "location" : "US East (N. Virginia)",
  444. "locationType" : "AWS Region",
  445. "instanceType" : "p4d.24xlarge",
  446. "currentGeneration" : "Yes",
  447. "instanceFamily" : "GPU instance",
  448. "vcpu" : "96",
  449. "physicalProcessor" : "Intel Xeon Platinum 8275L",
  450. "clockSpeed" : "3 GHz",
  451. "memory" : "1152 GiB",
  452. "storage" : "8 x 1000 SSD",
  453. "networkPerformance" : "400 Gigabit",
  454. "processorArchitecture" : "64-bit",
  455. "tenancy" : "Shared",
  456. "operatingSystem" : "Linux",
  457. "licenseModel" : "No License required",
  458. "usagetype" : "BoxUsage:p4d.24xlarge",
  459. "operation" : "RunInstances",
  460. "availabilityzone" : "NA",
  461. "capacitystatus" : "Used",
  462. "classicnetworkingsupport" : "false",
  463. "dedicatedEbsThroughput" : "19000 Mbps",
  464. "ecu" : "345",
  465. "enhancedNetworkingSupported" : "No",
  466. "gpu" : "8",
  467. "gpuMemory" : "NA",
  468. "intelAvxAvailable" : "Yes",
  469. "intelAvx2Available" : "Yes",
  470. "intelTurboAvailable" : "Yes",
  471. "marketoption" : "OnDemand",
  472. "normalizationSizeFactor" : "192",
  473. "preInstalledSw" : "NA",
  474. "processorFeatures" : "Intel AVX; Intel AVX2; Intel AVX512; Intel Turbo",
  475. "regionCode" : "us-east-1",
  476. "servicename" : "Amazon Elastic Compute Cloud",
  477. "vpcnetworkingsupport" : "true"
  478. }
  479. },
  480. "YSXJGN78QTXNVGDQ" : {
  481. "sku" : "YSXJGN78QTXNVGDQ",
  482. "productFamily" : "Compute Instance",
  483. "attributes" : {
  484. "servicecode" : "AmazonEC2",
  485. "location" : "US East (N. Virginia)",
  486. "locationType" : "AWS Region",
  487. "instanceType" : "p4d.24xlarge",
  488. "currentGeneration" : "Yes",
  489. "instanceFamily" : "GPU instance",
  490. "vcpu" : "96",
  491. "physicalProcessor" : "Intel Xeon Platinum 8275L",
  492. "clockSpeed" : "3 GHz",
  493. "memory" : "1152 GiB",
  494. "storage" : "8 x 1000 SSD",
  495. "networkPerformance" : "400 Gigabit",
  496. "processorArchitecture" : "64-bit",
  497. "tenancy" : "Shared",
  498. "operatingSystem" : "Linux",
  499. "licenseModel" : "No License required",
  500. "usagetype" : "BoxUsage:p4d.24xlarge",
  501. "operation" : "RunInstances:CB",
  502. "availabilityzone" : "NA",
  503. "capacitystatus" : "Used",
  504. "classicnetworkingsupport" : "false",
  505. "dedicatedEbsThroughput" : "19000 Mbps",
  506. "ecu" : "345",
  507. "enhancedNetworkingSupported" : "No",
  508. "gpu" : "8",
  509. "gpuMemory" : "NA",
  510. "intelAvxAvailable" : "Yes",
  511. "intelAvx2Available" : "Yes",
  512. "intelTurboAvailable" : "Yes",
  513. "marketoption" : "CapacityBlock",
  514. "normalizationSizeFactor" : "192",
  515. "preInstalledSw" : "NA",
  516. "processorFeatures" : "Intel AVX; Intel AVX2; Intel AVX512; Intel Turbo",
  517. "regionCode" : "us-east-1",
  518. "servicename" : "Amazon Elastic Compute Cloud",
  519. "vpcnetworkingsupport" : "true"
  520. }
  521. }
  522. },
  523. "terms" : {
  524. "OnDemand" : {
  525. "H7NGEAC6UEHNTKSJ" : {
  526. "H7NGEAC6UEHNTKSJ.JRTCKXETXF" : {
  527. "offerTermCode" : "JRTCKXETXF",
  528. "sku" : "H7NGEAC6UEHNTKSJ",
  529. "effectiveDate" : "2024-05-01T00:00:00Z",
  530. "priceDimensions" : {
  531. "H7NGEAC6UEHNTKSJ.JRTCKXETXF.6YS6EN2CT7" : {
  532. "rateCode" : "H7NGEAC6UEHNTKSJ.JRTCKXETXF.6YS6EN2CT7",
  533. "description" : "$32.7726 per On Demand Linux p4d.24xlarge Instance Hour",
  534. "beginRange" : "0",
  535. "endRange" : "Inf",
  536. "unit" : "Hrs",
  537. "pricePerUnit" : {
  538. "USD" : "32.7726000000"
  539. },
  540. "appliesTo" : [ ]
  541. }
  542. },
  543. "termAttributes" : { }
  544. }
  545. },
  546. "YSXJGN78QTXNVGDQ" : {
  547. "YSXJGN78QTXNVGDQ.JRTCKXETXF" : {
  548. "offerTermCode" : "JRTCKXETXF",
  549. "sku" : "YSXJGN78QTXNVGDQ",
  550. "effectiveDate" : "2024-05-01T00:00:00Z",
  551. "priceDimensions" : {
  552. "YSXJGN78QTXNVGDQ.JRTCKXETXF.6YS6EN2CT7" : {
  553. "rateCode" : "YSXJGN78QTXNVGDQ.JRTCKXETXF.6YS6EN2CT7",
  554. "description" : "$0.00 per Capacity Block Linux p4d.24xlarge Instance Hour",
  555. "beginRange" : "0",
  556. "endRange" : "Inf",
  557. "unit" : "Hrs",
  558. "pricePerUnit" : {
  559. "USD" : "0.0000000000"
  560. },
  561. "appliesTo" : [ ]
  562. }
  563. },
  564. "termAttributes" : { }
  565. }
  566. },
  567. }
  568. },
  569. "attributesList" : { }
  570. }
  571. `
  572. testResponseCase1 := http.Response{
  573. Body: io.NopCloser(bytes.NewBufferString(pricingCase1)),
  574. Request: &http.Request{
  575. URL: &url.URL{
  576. Scheme: "https",
  577. Host: "test-aws-http-endpoint:443",
  578. },
  579. },
  580. }
  581. awsTest.populatePricing(&testResponseCase1, inputkeysCase1)
  582. expectedProdTermsInstanceOndemandCase1 := &AWSProductTerms{
  583. Sku: "H7NGEAC6UEHNTKSJ",
  584. Memory: "1152 GiB",
  585. Storage: "8 x 1000 SSD",
  586. VCpu: "96",
  587. GPU: "8",
  588. OnDemand: &AWSOfferTerm{
  589. Sku: "H7NGEAC6UEHNTKSJ",
  590. OfferTermCode: "JRTCKXETXF",
  591. PriceDimensions: map[string]*AWSRateCode{
  592. "H7NGEAC6UEHNTKSJ.JRTCKXETXF.6YS6EN2CT7": {
  593. Unit: "Hrs",
  594. PricePerUnit: AWSCurrencyCode{
  595. USD: "32.7726000000",
  596. },
  597. },
  598. },
  599. },
  600. }
  601. expectedPricingCase1 := map[string]*AWSProductTerms{
  602. "us-east-1,p4d.24xlarge,linux": expectedProdTermsInstanceOndemandCase1,
  603. "us-east-1,p4d.24xlarge,linux,preemptible": expectedProdTermsInstanceOndemandCase1,
  604. }
  605. if !reflect.DeepEqual(expectedPricingCase1, awsTest.Pricing) {
  606. expectedJsonString, _ := json.MarshalIndent(expectedPricingCase1, "", " ")
  607. resultJsonString, _ := json.MarshalIndent(awsTest.Pricing, "", " ")
  608. t.Logf("Expected: %s", string(expectedJsonString))
  609. t.Logf("Result: %s", string(resultJsonString))
  610. t.Fatalf("expected parsed pricing did not match actual parsed result (us-east-1)")
  611. }
  612. // Case 2
  613. awsCnString := `
  614. {
  615. "formatVersion" : "v1.0",
  616. "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.",
  617. "offerCode" : "AmazonEC2",
  618. "version" : "20230314154740",
  619. "publicationDate" : "2023-03-14T15:47:40Z",
  620. "products" : {
  621. "R83VXG9NAPDASEGN" : {
  622. "sku" : "R83VXG9NAPDASEGN",
  623. "productFamily" : "Storage",
  624. "attributes" : {
  625. "servicecode" : "AmazonEC2",
  626. "location" : "China (Ningxia)",
  627. "locationType" : "AWS Region",
  628. "storageMedia" : "SSD-backed",
  629. "volumeType" : "General Purpose",
  630. "maxVolumeSize" : "16 TiB",
  631. "maxIopsvolume" : "16000",
  632. "maxThroughputvolume" : "1000 MiB/s",
  633. "usagetype" : "CNW1-EBS:VolumeUsage.gp3",
  634. "operation" : "",
  635. "regionCode" : "cn-northwest-1",
  636. "servicename" : "Amazon Elastic Compute Cloud",
  637. "volumeApiName" : "gp3"
  638. }
  639. }
  640. },
  641. "terms" : {
  642. "OnDemand" : {
  643. "R83VXG9NAPDASEGN" : {
  644. "R83VXG9NAPDASEGN.5Y9WH78GDR" : {
  645. "offerTermCode" : "5Y9WH78GDR",
  646. "sku" : "R83VXG9NAPDASEGN",
  647. "effectiveDate" : "2023-03-01T00:00:00Z",
  648. "priceDimensions" : {
  649. "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6" : {
  650. "rateCode" : "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6",
  651. "description" : "0.5312 CNY per GB-month of General Purpose (gp3) provisioned storage - China (Ningxia)",
  652. "beginRange" : "0",
  653. "endRange" : "Inf",
  654. "unit" : "GB-Mo",
  655. "pricePerUnit" : {
  656. "CNY" : "0.5312000000"
  657. },
  658. "appliesTo" : [ ]
  659. }
  660. },
  661. "termAttributes" : { }
  662. }
  663. }
  664. }
  665. },
  666. "attributesList" : { }
  667. }
  668. `
  669. awsTest = AWS{
  670. ValidPricingKeys: map[string]bool{},
  671. }
  672. testResponse = http.Response{
  673. Body: io.NopCloser(bytes.NewBufferString(awsCnString)),
  674. Request: &http.Request{
  675. URL: &url.URL{
  676. Scheme: "https",
  677. Host: "test-aws-http-endpoint:443",
  678. },
  679. },
  680. }
  681. awsTest.populatePricing(&testResponse, inputkeys)
  682. expectedProdTermsDisk = &AWSProductTerms{
  683. Sku: "R83VXG9NAPDASEGN",
  684. Memory: "",
  685. Storage: "",
  686. VCpu: "",
  687. GPU: "",
  688. OnDemand: &AWSOfferTerm{
  689. Sku: "R83VXG9NAPDASEGN",
  690. OfferTermCode: "5Y9WH78GDR",
  691. PriceDimensions: map[string]*AWSRateCode{
  692. "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6": {
  693. Unit: "GB-Mo",
  694. PricePerUnit: AWSCurrencyCode{
  695. USD: "",
  696. CNY: "0.5312000000",
  697. },
  698. },
  699. },
  700. },
  701. PV: &models.PV{
  702. Cost: "0.0007276712328767123",
  703. CostPerIO: "",
  704. Class: "gp3",
  705. Size: "",
  706. Region: "cn-northwest-1",
  707. ProviderID: "",
  708. },
  709. }
  710. expectedPricing = map[string]*AWSProductTerms{
  711. "cn-northwest-1,EBS:VolumeUsage.gp3": expectedProdTermsDisk,
  712. "cn-northwest-1,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
  713. }
  714. if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
  715. t.Fatalf("expected parsed pricing did not match actual parsed result (cn)")
  716. }
  717. }
  718. func TestFeatures(t *testing.T) {
  719. testCases := map[string]struct {
  720. aws awsKey
  721. expected string
  722. }{
  723. "Spot from custom labels": {
  724. aws: awsKey{
  725. SpotLabelName: "node-type",
  726. SpotLabelValue: "node-spot",
  727. Labels: map[string]string{
  728. "node-type": "node-spot",
  729. v1.LabelOSStable: "linux",
  730. v1.LabelHostname: "my-hostname",
  731. v1.LabelTopologyRegion: "us-west-2",
  732. v1.LabelTopologyZone: "us-west-2b",
  733. v1.LabelInstanceTypeStable: "m5.large",
  734. },
  735. },
  736. expected: "us-west-2,m5.large,linux,preemptible",
  737. },
  738. }
  739. for name, tc := range testCases {
  740. t.Run(name, func(t *testing.T) {
  741. features := tc.aws.Features()
  742. if features != tc.expected {
  743. t.Errorf("expected %s, got %s", tc.expected, features)
  744. }
  745. })
  746. }
  747. }
  748. func Test_getStorageClassTypeFrom(t *testing.T) {
  749. tests := []struct {
  750. name string
  751. provisioner string
  752. want string
  753. }{
  754. {
  755. name: "empty-provisioner",
  756. provisioner: "",
  757. want: "",
  758. },
  759. {
  760. name: "ebs-default-provisioner",
  761. provisioner: "kubernetes.io/aws-ebs",
  762. want: "gp2",
  763. },
  764. {
  765. name: "ebs-csi-provisioner",
  766. provisioner: "ebs.csi.aws.com",
  767. want: "gp3",
  768. },
  769. {
  770. name: "unknown-provisioner",
  771. provisioner: "unknown",
  772. want: "",
  773. },
  774. }
  775. for _, tt := range tests {
  776. t.Run(tt.name, func(t *testing.T) {
  777. if got := getStorageClassTypeFrom(tt.provisioner); got != tt.want {
  778. t.Errorf("getStorageClassTypeFrom() = %v, want %v", got, tt.want)
  779. }
  780. })
  781. }
  782. }