provider_test.go 23 KB

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