provider_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. package aws
  2. import (
  3. "encoding/json"
  4. "io"
  5. "net/http"
  6. "net/url"
  7. "os"
  8. "reflect"
  9. "testing"
  10. "github.com/opencost/opencost/core/pkg/clustercache"
  11. "github.com/opencost/opencost/pkg/cloud/models"
  12. "github.com/opencost/opencost/pkg/config"
  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. ClusterRegion: "us-east-2",
  170. }
  171. inputkeys := map[string]bool{
  172. "us-east-2,m5.large,linux": true,
  173. }
  174. fixture, err := os.Open("testdata/pricing-us-east-2.json")
  175. if err != nil {
  176. t.Fatalf("failed to load pricing fixture: %s", err)
  177. }
  178. testResponse := http.Response{
  179. Body: io.NopCloser(fixture),
  180. Request: &http.Request{
  181. URL: &url.URL{
  182. Scheme: "https",
  183. Host: "test-aws-http-endpoint:443",
  184. },
  185. },
  186. }
  187. awsTest.populatePricing(&testResponse, inputkeys)
  188. expectedProdTermsDisk := &AWSProductTerms{
  189. Sku: "M6UGCCQ3CDJQAA37",
  190. Memory: "",
  191. Storage: "",
  192. VCpu: "",
  193. GPU: "",
  194. OnDemand: &AWSOfferTerm{
  195. Sku: "M6UGCCQ3CDJQAA37",
  196. OfferTermCode: "JRTCKXETXF",
  197. PriceDimensions: map[string]*AWSRateCode{
  198. "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7": {
  199. Unit: "GB-Mo",
  200. PricePerUnit: AWSCurrencyCode{
  201. USD: "0.0800000000",
  202. CNY: "",
  203. },
  204. },
  205. },
  206. },
  207. PV: &models.PV{
  208. Cost: "0.00010958904109589041",
  209. CostPerIO: "",
  210. Class: "gp3",
  211. Size: "",
  212. Region: "us-east-2",
  213. ProviderID: "",
  214. },
  215. }
  216. expectedProdTermsInstanceOndemand := &AWSProductTerms{
  217. Sku: "8D49XP354UEYTHGM",
  218. Memory: "8 GiB",
  219. Storage: "EBS only",
  220. VCpu: "2",
  221. GPU: "",
  222. OnDemand: &AWSOfferTerm{
  223. Sku: "8D49XP354UEYTHGM",
  224. OfferTermCode: "MZU6U2429S",
  225. PriceDimensions: map[string]*AWSRateCode{
  226. "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U": {
  227. Unit: "Quantity",
  228. PricePerUnit: AWSCurrencyCode{
  229. USD: "1161",
  230. CNY: "",
  231. },
  232. },
  233. },
  234. },
  235. }
  236. expectedProdTermsInstanceSpot := &AWSProductTerms{
  237. Sku: "8D49XP354UEYTHGM",
  238. Memory: "8 GiB",
  239. Storage: "EBS only",
  240. VCpu: "2",
  241. GPU: "",
  242. OnDemand: &AWSOfferTerm{
  243. Sku: "8D49XP354UEYTHGM",
  244. OfferTermCode: "MZU6U2429S",
  245. PriceDimensions: map[string]*AWSRateCode{
  246. "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U": {
  247. Unit: "Quantity",
  248. PricePerUnit: AWSCurrencyCode{
  249. USD: "1161",
  250. CNY: "",
  251. },
  252. },
  253. },
  254. },
  255. }
  256. expectedProdTermsLoadbalancer := &AWSProductTerms{
  257. Sku: "Y9RYMSE644KDSV4S",
  258. OnDemand: &AWSOfferTerm{
  259. Sku: "Y9RYMSE644KDSV4S",
  260. OfferTermCode: "JRTCKXETXF",
  261. PriceDimensions: map[string]*AWSRateCode{
  262. "Y9RYMSE644KDSV4S.JRTCKXETXF.6YS6EN2CT7": {
  263. Unit: "Hrs",
  264. PricePerUnit: AWSCurrencyCode{
  265. USD: "0.0225000000",
  266. CNY: "",
  267. },
  268. },
  269. },
  270. },
  271. LoadBalancer: &models.LoadBalancer{
  272. Cost: 0.0225,
  273. },
  274. }
  275. expectedPricing := map[string]*AWSProductTerms{
  276. "us-east-2,EBS:VolumeUsage.gp3": expectedProdTermsDisk,
  277. "us-east-2,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
  278. "us-east-2,m5.large,linux": expectedProdTermsInstanceOndemand,
  279. "us-east-2,m5.large,linux,preemptible": expectedProdTermsInstanceSpot,
  280. "us-east-2,LoadBalancerUsage": expectedProdTermsLoadbalancer,
  281. }
  282. if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
  283. t.Fatalf("expected parsed pricing did not match actual parsed result (us-east-2)")
  284. }
  285. lbPricing, _ := awsTest.LoadBalancerPricing()
  286. if lbPricing.Cost != 0.0225 {
  287. t.Fatalf("expected loadbalancer pricing of 0.0225 but got %f (us-east-2)", lbPricing.Cost)
  288. }
  289. // Case 1 - Only accept `"marketoption":"OnDemand"`
  290. inputkeysCase1 := map[string]bool{
  291. "us-east-1,p4d.24xlarge,linux": true,
  292. }
  293. fixture, err = os.Open("testdata/pricing-us-east-1.json")
  294. if err != nil {
  295. t.Fatalf("failed to load pricing fixture: %s", err)
  296. }
  297. testResponseCase1 := http.Response{
  298. Body: io.NopCloser(fixture),
  299. Request: &http.Request{
  300. URL: &url.URL{
  301. Scheme: "https",
  302. Host: "test-aws-http-endpoint:443",
  303. },
  304. },
  305. }
  306. awsTest.populatePricing(&testResponseCase1, inputkeysCase1)
  307. expectedProdTermsInstanceOndemandCase1 := &AWSProductTerms{
  308. Sku: "H7NGEAC6UEHNTKSJ",
  309. Memory: "1152 GiB",
  310. Storage: "8 x 1000 SSD",
  311. VCpu: "96",
  312. GPU: "8",
  313. OnDemand: &AWSOfferTerm{
  314. Sku: "H7NGEAC6UEHNTKSJ",
  315. OfferTermCode: "JRTCKXETXF",
  316. PriceDimensions: map[string]*AWSRateCode{
  317. "H7NGEAC6UEHNTKSJ.JRTCKXETXF.6YS6EN2CT7": {
  318. Unit: "Hrs",
  319. PricePerUnit: AWSCurrencyCode{
  320. USD: "32.7726000000",
  321. },
  322. },
  323. },
  324. },
  325. }
  326. expectedPricingCase1 := map[string]*AWSProductTerms{
  327. "us-east-1,p4d.24xlarge,linux": expectedProdTermsInstanceOndemandCase1,
  328. "us-east-1,p4d.24xlarge,linux,preemptible": expectedProdTermsInstanceOndemandCase1,
  329. }
  330. if !reflect.DeepEqual(expectedPricingCase1, awsTest.Pricing) {
  331. expectedJsonString, _ := json.MarshalIndent(expectedPricingCase1, "", " ")
  332. resultJsonString, _ := json.MarshalIndent(awsTest.Pricing, "", " ")
  333. t.Logf("Expected: %s", string(expectedJsonString))
  334. t.Logf("Result: %s", string(resultJsonString))
  335. t.Fatalf("expected parsed pricing did not match actual parsed result (us-east-1)")
  336. }
  337. // Case 2
  338. awsTest = AWS{
  339. ValidPricingKeys: map[string]bool{},
  340. }
  341. fixture, err = os.Open("testdata/pricing-cn-northwest-1.json")
  342. if err != nil {
  343. t.Fatalf("failed to load pricing fixture: %s", err)
  344. }
  345. testResponse = http.Response{
  346. Body: io.NopCloser(fixture),
  347. Request: &http.Request{
  348. URL: &url.URL{
  349. Scheme: "https",
  350. Host: "test-aws-http-endpoint:443",
  351. },
  352. },
  353. }
  354. awsTest.populatePricing(&testResponse, inputkeys)
  355. expectedProdTermsDisk = &AWSProductTerms{
  356. Sku: "R83VXG9NAPDASEGN",
  357. Memory: "",
  358. Storage: "",
  359. VCpu: "",
  360. GPU: "",
  361. OnDemand: &AWSOfferTerm{
  362. Sku: "R83VXG9NAPDASEGN",
  363. OfferTermCode: "5Y9WH78GDR",
  364. PriceDimensions: map[string]*AWSRateCode{
  365. "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6": {
  366. Unit: "GB-Mo",
  367. PricePerUnit: AWSCurrencyCode{
  368. USD: "",
  369. CNY: "0.5312000000",
  370. },
  371. },
  372. },
  373. },
  374. PV: &models.PV{
  375. Cost: "0.0007276712328767123",
  376. CostPerIO: "",
  377. Class: "gp3",
  378. Size: "",
  379. Region: "cn-northwest-1",
  380. ProviderID: "",
  381. },
  382. }
  383. expectedPricing = map[string]*AWSProductTerms{
  384. "cn-northwest-1,EBS:VolumeUsage.gp3": expectedProdTermsDisk,
  385. "cn-northwest-1,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
  386. }
  387. if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
  388. t.Fatalf("expected parsed pricing did not match actual parsed result (cn)")
  389. }
  390. }
  391. func TestFeatures(t *testing.T) {
  392. testCases := map[string]struct {
  393. aws awsKey
  394. expected string
  395. }{
  396. "Spot from custom labels": {
  397. aws: awsKey{
  398. SpotLabelName: "node-type",
  399. SpotLabelValue: "node-spot",
  400. Labels: map[string]string{
  401. "node-type": "node-spot",
  402. v1.LabelOSStable: "linux",
  403. v1.LabelHostname: "my-hostname",
  404. v1.LabelTopologyRegion: "us-west-2",
  405. v1.LabelTopologyZone: "us-west-2b",
  406. v1.LabelInstanceTypeStable: "m5.large",
  407. },
  408. },
  409. expected: "us-west-2,m5.large,linux,preemptible",
  410. },
  411. }
  412. for name, tc := range testCases {
  413. t.Run(name, func(t *testing.T) {
  414. features := tc.aws.Features()
  415. if features != tc.expected {
  416. t.Errorf("expected %s, got %s", tc.expected, features)
  417. }
  418. })
  419. }
  420. }
  421. func Test_getStorageClassTypeFrom(t *testing.T) {
  422. tests := []struct {
  423. name string
  424. provisioner string
  425. want string
  426. }{
  427. {
  428. name: "empty-provisioner",
  429. provisioner: "",
  430. want: "",
  431. },
  432. {
  433. name: "ebs-default-provisioner",
  434. provisioner: "kubernetes.io/aws-ebs",
  435. want: "gp2",
  436. },
  437. {
  438. name: "ebs-csi-provisioner",
  439. provisioner: "ebs.csi.aws.com",
  440. want: "gp3",
  441. },
  442. {
  443. name: "unknown-provisioner",
  444. provisioner: "unknown",
  445. want: "",
  446. },
  447. }
  448. for _, tt := range tests {
  449. t.Run(tt.name, func(t *testing.T) {
  450. if got := getStorageClassTypeFrom(tt.provisioner); got != tt.want {
  451. t.Errorf("getStorageClassTypeFrom() = %v, want %v", got, tt.want)
  452. }
  453. })
  454. }
  455. }
  456. func Test_awsKey_isFargateNode(t *testing.T) {
  457. tests := []struct {
  458. name string
  459. labels map[string]string
  460. want bool
  461. }{
  462. {
  463. name: "fargate node with correct label",
  464. labels: map[string]string{
  465. eksComputeTypeLabel: "fargate",
  466. },
  467. want: true,
  468. },
  469. {
  470. name: "ec2 node with different compute type",
  471. labels: map[string]string{
  472. eksComputeTypeLabel: "ec2",
  473. },
  474. want: false,
  475. },
  476. {
  477. name: "node without compute type label",
  478. labels: map[string]string{
  479. "some.other.label": "value",
  480. },
  481. want: false,
  482. },
  483. {
  484. name: "node with empty labels",
  485. labels: map[string]string{},
  486. want: false,
  487. },
  488. {
  489. name: "node with nil labels",
  490. labels: nil,
  491. want: false,
  492. },
  493. }
  494. for _, tt := range tests {
  495. t.Run(tt.name, func(t *testing.T) {
  496. k := &awsKey{
  497. Labels: tt.labels,
  498. }
  499. if got := k.isFargateNode(); got != tt.want {
  500. t.Errorf("awsKey.isFargateNode() = %v, want %v", got, tt.want)
  501. }
  502. })
  503. }
  504. }
  505. func TestGetPricingListURL(t *testing.T) {
  506. tests := []struct {
  507. name string
  508. serviceCode string
  509. nodeList []*clustercache.Node
  510. expected string
  511. }{
  512. {
  513. name: "AmazonEC2 service with us-east-1 region",
  514. serviceCode: "AmazonEC2",
  515. nodeList: []*clustercache.Node{
  516. {
  517. Name: "test-node",
  518. Labels: map[string]string{
  519. "topology.kubernetes.io/region": "us-east-1",
  520. },
  521. },
  522. },
  523. expected: "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/us-east-1/index.json",
  524. },
  525. {
  526. name: "AmazonECS service with us-west-2 region",
  527. serviceCode: "AmazonECS",
  528. nodeList: []*clustercache.Node{
  529. {
  530. Name: "test-node",
  531. Labels: map[string]string{
  532. "topology.kubernetes.io/region": "us-west-2",
  533. },
  534. },
  535. },
  536. expected: "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonECS/current/us-west-2/index.json",
  537. },
  538. {
  539. name: "Chinese region cn-north-1",
  540. serviceCode: "AmazonEC2",
  541. nodeList: []*clustercache.Node{
  542. {
  543. Name: "test-node",
  544. Labels: map[string]string{
  545. "topology.kubernetes.io/region": "cn-north-1",
  546. },
  547. },
  548. },
  549. expected: "https://pricing.cn-north-1.amazonaws.com.cn/offers/v1.0/cn/AmazonEC2/current/cn-north-1/index.json",
  550. },
  551. {
  552. name: "Chinese region cn-northwest-1",
  553. serviceCode: "AmazonECS",
  554. nodeList: []*clustercache.Node{
  555. {
  556. Name: "test-node",
  557. Labels: map[string]string{
  558. "topology.kubernetes.io/region": "cn-northwest-1",
  559. },
  560. },
  561. },
  562. expected: "https://pricing.cn-north-1.amazonaws.com.cn/offers/v1.0/cn/AmazonECS/current/cn-northwest-1/index.json",
  563. },
  564. {
  565. name: "empty node list - multiregion",
  566. serviceCode: "AmazonEC2",
  567. nodeList: []*clustercache.Node{},
  568. expected: "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.json",
  569. },
  570. {
  571. name: "multiple regions - multiregion",
  572. serviceCode: "AmazonECS",
  573. nodeList: []*clustercache.Node{
  574. {
  575. Name: "test-node-1",
  576. Labels: map[string]string{
  577. "topology.kubernetes.io/region": "us-east-1",
  578. },
  579. },
  580. {
  581. Name: "test-node-2",
  582. Labels: map[string]string{
  583. "topology.kubernetes.io/region": "us-west-2",
  584. },
  585. },
  586. },
  587. expected: "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonECS/current/index.json",
  588. },
  589. {
  590. name: "node without region label",
  591. serviceCode: "AmazonEC2",
  592. nodeList: []*clustercache.Node{
  593. {
  594. Name: "test-node",
  595. Labels: map[string]string{
  596. "some.other.label": "value",
  597. },
  598. },
  599. },
  600. expected: "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.json",
  601. },
  602. }
  603. for _, tt := range tests {
  604. t.Run(tt.name, func(t *testing.T) {
  605. result := getPricingListURL(tt.serviceCode, tt.nodeList)
  606. if result != tt.expected {
  607. t.Errorf("getPricingListURL() = %v, expected %v", result, tt.expected)
  608. }
  609. })
  610. }
  611. }
  612. func Test_configUpdaterWithReaderAndType_forSpotValues(t *testing.T) {
  613. fixture, err := os.Open("testdata/aws-config.json")
  614. if err != nil {
  615. t.Fatalf("failed to load aws config fixture: %s", err)
  616. }
  617. defer fixture.Close()
  618. c := &models.CustomPricing{}
  619. callback := configUpdaterWithReaderAndType(fixture, "otherupdatetype")
  620. err = callback(c)
  621. if err != nil {
  622. t.Fatalf("failed to load aws config: %s", err)
  623. }
  624. if c.AwsSpotDataBucket != "mybucket" {
  625. t.Fatalf("Expected %s but got %s", "mybucket", c.AwsSpotDataBucket)
  626. }
  627. if c.AwsSpotDataPrefix != "myprefix" {
  628. t.Fatalf("Expected %s but got %s", "myprefix", c.AwsSpotDataPrefix)
  629. }
  630. if c.AwsSpotDataRegion != "us-east-1" {
  631. t.Fatalf("Expected %s but got %s", "us-east-1", c.AwsSpotDataRegion)
  632. }
  633. fixture2, err := os.Open("testdata/aws-config-empty.json")
  634. if err != nil {
  635. t.Fatalf("failed to load aws config fixture: %s", err)
  636. }
  637. defer fixture2.Close()
  638. c = &models.CustomPricing{}
  639. callback = configUpdaterWithReaderAndType(fixture2, "otherupdatetype")
  640. err = callback(c)
  641. if err != nil {
  642. t.Fatalf("failed to load aws config: %s", err)
  643. }
  644. if c.AwsSpotDataBucket != "" {
  645. t.Fatalf("Expected empty string but got %s", c.AwsSpotDataBucket)
  646. }
  647. if c.AwsSpotDataPrefix != "" {
  648. t.Fatalf("Expected empty string but got %s", c.AwsSpotDataPrefix)
  649. }
  650. if c.AwsSpotDataRegion != "" {
  651. t.Fatalf("Expected empty string but got %s", c.AwsSpotDataRegion)
  652. }
  653. }
  654. // Mock cluster cache for testing
  655. type mockClusterCache struct {
  656. pods []*clustercache.Pod
  657. }
  658. func (m *mockClusterCache) Run() {}
  659. func (m *mockClusterCache) Stop() {}
  660. func (m *mockClusterCache) GetAllPods() []*clustercache.Pod {
  661. return m.pods
  662. }
  663. func (m *mockClusterCache) GetAllNodes() []*clustercache.Node {
  664. return nil
  665. }
  666. func (m *mockClusterCache) GetAllPersistentVolumes() []*clustercache.PersistentVolume {
  667. return nil
  668. }
  669. func (m *mockClusterCache) GetAllPersistentVolumeClaims() []*clustercache.PersistentVolumeClaim {
  670. return nil
  671. }
  672. func (m *mockClusterCache) GetAllStorageClasses() []*clustercache.StorageClass {
  673. return nil
  674. }
  675. func (m *mockClusterCache) GetAllServices() []*clustercache.Service {
  676. return nil
  677. }
  678. func (m *mockClusterCache) GetAllDeployments() []*clustercache.Deployment {
  679. return nil
  680. }
  681. func (m *mockClusterCache) GetAllDaemonSets() []*clustercache.DaemonSet {
  682. return nil
  683. }
  684. func (m *mockClusterCache) GetAllStatefulSets() []*clustercache.StatefulSet {
  685. return nil
  686. }
  687. func (m *mockClusterCache) GetAllReplicaSets() []*clustercache.ReplicaSet {
  688. return nil
  689. }
  690. func (m *mockClusterCache) GetAllJobs() []*clustercache.Job {
  691. return nil
  692. }
  693. func (m *mockClusterCache) GetAllNamespaces() []*clustercache.Namespace {
  694. return nil
  695. }
  696. func (m *mockClusterCache) GetAllPodDisruptionBudgets() []*clustercache.PodDisruptionBudget {
  697. return nil
  698. }
  699. func (m *mockClusterCache) GetAllReplicationControllers() []*clustercache.ReplicationController {
  700. return nil
  701. }
  702. func (m *mockClusterCache) GetAllResourceQuotas() []*clustercache.ResourceQuota {
  703. return nil
  704. }
  705. func TestAWS_getFargatePod(t *testing.T) {
  706. tests := []struct {
  707. name string
  708. pods []*clustercache.Pod
  709. awsKey *awsKey
  710. wantPod *clustercache.Pod
  711. wantBool bool
  712. }{
  713. {
  714. name: "pod found for node",
  715. pods: []*clustercache.Pod{
  716. {
  717. Name: "test-pod",
  718. Spec: clustercache.PodSpec{
  719. NodeName: "fargate-node-1",
  720. },
  721. },
  722. },
  723. awsKey: &awsKey{
  724. Name: "fargate-node-1",
  725. },
  726. wantPod: &clustercache.Pod{
  727. Name: "test-pod",
  728. Spec: clustercache.PodSpec{
  729. NodeName: "fargate-node-1",
  730. },
  731. },
  732. wantBool: true,
  733. },
  734. {
  735. name: "pod not found for node",
  736. pods: []*clustercache.Pod{
  737. {
  738. Name: "test-pod",
  739. Spec: clustercache.PodSpec{
  740. NodeName: "different-node",
  741. },
  742. },
  743. },
  744. awsKey: &awsKey{
  745. Name: "fargate-node-1",
  746. },
  747. wantPod: nil,
  748. wantBool: false,
  749. },
  750. {
  751. name: "no pods in cluster",
  752. pods: []*clustercache.Pod{},
  753. awsKey: &awsKey{
  754. Name: "fargate-node-1",
  755. },
  756. wantPod: nil,
  757. wantBool: false,
  758. },
  759. }
  760. for _, tt := range tests {
  761. t.Run(tt.name, func(t *testing.T) {
  762. aws := &AWS{
  763. Clientset: &mockClusterCache{pods: tt.pods},
  764. }
  765. gotPod, gotBool := aws.getFargatePod(tt.awsKey)
  766. if gotBool != tt.wantBool {
  767. t.Errorf("AWS.getFargatePod() gotBool = %v, want %v", gotBool, tt.wantBool)
  768. }
  769. if tt.wantPod == nil && gotPod != nil {
  770. t.Errorf("AWS.getFargatePod() gotPod = %v, want nil", gotPod)
  771. } else if tt.wantPod != nil && gotPod == nil {
  772. t.Errorf("AWS.getFargatePod() gotPod = nil, want %v", tt.wantPod)
  773. } else if tt.wantPod != nil && gotPod != nil {
  774. if gotPod.Name != tt.wantPod.Name || gotPod.Spec.NodeName != tt.wantPod.Spec.NodeName {
  775. t.Errorf("AWS.getFargatePod() gotPod = %v, want %v", gotPod, tt.wantPod)
  776. }
  777. }
  778. })
  779. }
  780. }
  781. // fakeProviderConfig implements models.ProviderConfig for testing
  782. type fakeProviderConfig struct {
  783. customPricing *models.CustomPricing
  784. }
  785. func (f *fakeProviderConfig) GetCustomPricingData() (*models.CustomPricing, error) {
  786. if f.customPricing != nil {
  787. return f.customPricing, nil
  788. }
  789. return &models.CustomPricing{}, nil
  790. }
  791. func (f *fakeProviderConfig) Update(func(*models.CustomPricing) error) (*models.CustomPricing, error) {
  792. return f.customPricing, nil
  793. }
  794. func (f *fakeProviderConfig) UpdateFromMap(map[string]string) (*models.CustomPricing, error) {
  795. return f.customPricing, nil
  796. }
  797. func (f *fakeProviderConfig) ConfigFileManager() *config.ConfigFileManager {
  798. return nil
  799. }
  800. func TestAWS_SpotRefreshEnabled(t *testing.T) {
  801. tests := []struct {
  802. name string
  803. spotDataBucket string
  804. spotDataRegion string
  805. projectID string
  806. spotDataFeedEnabled string
  807. want bool
  808. }{
  809. {
  810. name: "disabled via config - with bucket",
  811. spotDataBucket: "my-bucket",
  812. spotDataRegion: "us-east-1",
  813. projectID: "123456789",
  814. spotDataFeedEnabled: "false",
  815. want: false,
  816. },
  817. {
  818. name: "disabled via config - with projectID only",
  819. projectID: "123456789",
  820. spotDataFeedEnabled: "false",
  821. want: false,
  822. },
  823. {
  824. name: "enabled by default - with bucket",
  825. spotDataBucket: "my-bucket",
  826. spotDataRegion: "us-east-1",
  827. projectID: "123456789",
  828. spotDataFeedEnabled: "",
  829. want: true,
  830. },
  831. {
  832. name: "enabled explicitly - with bucket",
  833. spotDataBucket: "my-bucket",
  834. spotDataRegion: "us-east-1",
  835. projectID: "123456789",
  836. spotDataFeedEnabled: "true",
  837. want: true,
  838. },
  839. {
  840. name: "no spot config - disabled",
  841. spotDataBucket: "",
  842. spotDataRegion: "",
  843. projectID: "",
  844. spotDataFeedEnabled: "",
  845. want: false,
  846. },
  847. {
  848. name: "no spot config - but explicitly enabled",
  849. spotDataBucket: "",
  850. spotDataRegion: "",
  851. projectID: "",
  852. spotDataFeedEnabled: "true",
  853. want: false,
  854. },
  855. {
  856. name: "only projectID set - enabled by default",
  857. projectID: "123456789",
  858. spotDataFeedEnabled: "",
  859. want: true,
  860. },
  861. {
  862. name: "only bucket set - enabled by default",
  863. spotDataBucket: "my-bucket",
  864. spotDataFeedEnabled: "",
  865. want: true,
  866. },
  867. {
  868. name: "only region set - enabled by default",
  869. spotDataRegion: "us-east-1",
  870. spotDataFeedEnabled: "",
  871. want: true,
  872. },
  873. }
  874. for _, tt := range tests {
  875. t.Run(tt.name, func(t *testing.T) {
  876. aws := &AWS{
  877. SpotDataBucket: tt.spotDataBucket,
  878. SpotDataRegion: tt.spotDataRegion,
  879. ProjectID: tt.projectID,
  880. Config: &fakeProviderConfig{
  881. customPricing: &models.CustomPricing{
  882. SpotDataFeedEnabled: tt.spotDataFeedEnabled,
  883. },
  884. },
  885. }
  886. got := aws.SpotRefreshEnabled()
  887. if got != tt.want {
  888. t.Errorf("AWS.SpotRefreshEnabled() = %v, want %v", got, tt.want)
  889. }
  890. })
  891. }
  892. // Test nil Config scenario to ensure no panic
  893. t.Run("nil config - falls back to field check", func(t *testing.T) {
  894. aws := &AWS{
  895. SpotDataBucket: "my-bucket",
  896. SpotDataRegion: "us-east-1",
  897. ProjectID: "123456789",
  898. Config: nil, // nil Config should not cause panic
  899. }
  900. got := aws.SpotRefreshEnabled()
  901. want := true // Should fall back to field-based check
  902. if got != want {
  903. t.Errorf("AWS.SpotRefreshEnabled() with nil Config = %v, want %v", got, want)
  904. }
  905. })
  906. t.Run("nil config - no spot fields", func(t *testing.T) {
  907. aws := &AWS{
  908. SpotDataBucket: "",
  909. SpotDataRegion: "",
  910. ProjectID: "",
  911. Config: nil, // nil Config should not cause panic
  912. }
  913. got := aws.SpotRefreshEnabled()
  914. want := false // No fields set, should return false
  915. if got != want {
  916. t.Errorf("AWS.SpotRefreshEnabled() with nil Config and no fields = %v, want %v", got, want)
  917. }
  918. })
  919. }