s3selectintegration_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. package aws
  2. import (
  3. "os"
  4. "reflect"
  5. "testing"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/opencost"
  8. "github.com/opencost/opencost/core/pkg/util/json"
  9. "github.com/opencost/opencost/core/pkg/util/timeutil"
  10. )
  11. func TestS3Integration_GetCloudCost(t *testing.T) {
  12. s3ConfigPath := os.Getenv("S3_CONFIGURATION")
  13. if s3ConfigPath == "" {
  14. t.Skip("skipping integration test, set environment variable S3_CONFIGURATION")
  15. }
  16. s3ConfigBin, err := os.ReadFile(s3ConfigPath)
  17. if err != nil {
  18. t.Fatalf("failed to read config file: %s", err.Error())
  19. }
  20. var s3Config S3Configuration
  21. err = json.Unmarshal(s3ConfigBin, &s3Config)
  22. if err != nil {
  23. t.Fatalf("failed to unmarshal config from JSON: %s", err.Error())
  24. }
  25. testCases := map[string]struct {
  26. integration *S3SelectIntegration
  27. start time.Time
  28. end time.Time
  29. expected bool
  30. }{
  31. // No CUR data is expected within 2 days of now
  32. "too_recent_window": {
  33. integration: &S3SelectIntegration{
  34. S3SelectQuerier: S3SelectQuerier{
  35. S3Connection: S3Connection{
  36. S3Configuration: s3Config,
  37. },
  38. },
  39. },
  40. end: time.Now(),
  41. start: time.Now().Add(-timeutil.Day),
  42. expected: true,
  43. },
  44. // CUR data should be available
  45. "last week window": {
  46. integration: &S3SelectIntegration{
  47. S3SelectQuerier: S3SelectQuerier{
  48. S3Connection: S3Connection{
  49. S3Configuration: s3Config,
  50. },
  51. },
  52. },
  53. end: time.Now().Add(-7 * timeutil.Day),
  54. start: time.Now().Add(-8 * timeutil.Day),
  55. expected: false,
  56. },
  57. }
  58. for name, testCase := range testCases {
  59. t.Run(name, func(t *testing.T) {
  60. actual, err := testCase.integration.GetCloudCost(testCase.start, testCase.end)
  61. if err != nil {
  62. t.Errorf("Other error during testing %s", err)
  63. } else if actual.IsEmpty() != testCase.expected {
  64. t.Errorf("Incorrect result, actual emptiness: %t, expected: %t", actual.IsEmpty(), testCase.expected)
  65. }
  66. })
  67. }
  68. }
  69. func Test_s3RowToCloudCost(t *testing.T) {
  70. columnIndexes := map[string]int{
  71. S3SelectListCost: 0,
  72. S3SelectNetCost: 1,
  73. S3SelectRICost: 2,
  74. S3SelectNetRICost: 3,
  75. S3SelectSPCost: 4,
  76. S3SelectNetSPCost: 5,
  77. S3SelectStartDate: 6,
  78. S3SelectBillPayerAccountID: 7,
  79. S3SelectAccountID: 8,
  80. S3SelectResourceID: 9,
  81. S3SelectItemType: 10,
  82. S3SelectProductCode: 11,
  83. S3SelectUsageType: 12,
  84. S3SelectRegionCode: 13,
  85. S3SelectAvailabilityZone: 14,
  86. `s."resourceTags/user:test"`: 15,
  87. `s."resourceTags/aws:test"`: 16,
  88. `s."resourceTags/user:eks:cluster-name"`: 17,
  89. }
  90. userTagColumns := []string{`s."resourceTags/user:test"`, `s."resourceTags/user:eks:cluster-name"`}
  91. awsTagColumns := []string{`s."resourceTags/aws:test"`}
  92. tests := []struct {
  93. name string
  94. row []string
  95. columnIndexes map[string]int
  96. userTagColumns []string
  97. awsTagColumns []string
  98. want *opencost.CloudCost
  99. wantErr bool
  100. }{
  101. {
  102. name: "invalid list cost",
  103. row: []string{"invalid", "2", "3", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", "itemType", "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  104. columnIndexes: columnIndexes,
  105. userTagColumns: userTagColumns,
  106. awsTagColumns: awsTagColumns,
  107. want: nil,
  108. wantErr: true,
  109. },
  110. {
  111. name: "invalid net cost",
  112. row: []string{"1", "invalid", "3", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", "itemType", "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  113. columnIndexes: columnIndexes,
  114. userTagColumns: userTagColumns,
  115. awsTagColumns: awsTagColumns,
  116. want: nil,
  117. wantErr: true,
  118. },
  119. {
  120. name: "invalid RI cost",
  121. row: []string{"1", "2", "invalid", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", TypeDiscountedUsage, "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  122. columnIndexes: columnIndexes,
  123. userTagColumns: userTagColumns,
  124. awsTagColumns: awsTagColumns,
  125. want: nil,
  126. wantErr: true,
  127. },
  128. {
  129. name: "invalid net RI cost",
  130. row: []string{"1", "2", "3", "invalid", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", TypeDiscountedUsage, "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  131. columnIndexes: columnIndexes,
  132. userTagColumns: userTagColumns,
  133. awsTagColumns: awsTagColumns,
  134. want: nil,
  135. wantErr: true,
  136. },
  137. {
  138. name: "invalid SP cost",
  139. row: []string{"1", "2", "3", "4", "invalid", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", TypeSavingsPlanCoveredUsage, "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  140. columnIndexes: columnIndexes,
  141. userTagColumns: userTagColumns,
  142. awsTagColumns: awsTagColumns,
  143. want: nil,
  144. wantErr: true,
  145. },
  146. {
  147. name: "invalid net SP cost",
  148. row: []string{"1", "2", "3", "4", "5", "invalid", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", TypeSavingsPlanCoveredUsage, "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  149. columnIndexes: columnIndexes,
  150. userTagColumns: userTagColumns,
  151. awsTagColumns: awsTagColumns,
  152. want: nil,
  153. wantErr: true,
  154. },
  155. {
  156. name: "invalid date",
  157. row: []string{"1", "2", "3", "4", "5", "6", "invalid", "payerAccountID", "usageAccountID", "resourceID", "itemType", "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  158. columnIndexes: columnIndexes,
  159. userTagColumns: userTagColumns,
  160. awsTagColumns: awsTagColumns,
  161. want: nil,
  162. wantErr: true,
  163. },
  164. {
  165. name: "valid empty labels",
  166. row: []string{"1", "2", "3", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", "itemType", "productCode", "usageType", "regionCode", "availabilityZone", "", "", ""},
  167. columnIndexes: columnIndexes,
  168. userTagColumns: userTagColumns,
  169. awsTagColumns: awsTagColumns,
  170. want: &opencost.CloudCost{
  171. Properties: &opencost.CloudCostProperties{
  172. ProviderID: "resourceID",
  173. Provider: "AWS",
  174. AccountID: "usageAccountID",
  175. AccountName: "usageAccountID",
  176. InvoiceEntityID: "payerAccountID",
  177. InvoiceEntityName: "payerAccountID",
  178. RegionID: "regionCode",
  179. AvailabilityZone: "availabilityZone",
  180. Service: "productCode",
  181. Category: opencost.OtherCategory,
  182. Labels: opencost.CloudCostLabels{},
  183. },
  184. Window: opencost.NewClosedWindow(
  185. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  186. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  187. ),
  188. ListCost: opencost.CostMetric{
  189. Cost: 1,
  190. KubernetesPercent: 0,
  191. },
  192. NetCost: opencost.CostMetric{
  193. Cost: 2,
  194. KubernetesPercent: 0,
  195. },
  196. AmortizedNetCost: opencost.CostMetric{
  197. Cost: 1,
  198. KubernetesPercent: 0,
  199. },
  200. InvoicedCost: opencost.CostMetric{
  201. Cost: 2,
  202. KubernetesPercent: 0,
  203. },
  204. AmortizedCost: opencost.CostMetric{
  205. Cost: 1,
  206. KubernetesPercent: 0,
  207. },
  208. },
  209. wantErr: false,
  210. },
  211. {
  212. name: "valid Kubernetes RI with labels",
  213. row: []string{"1", "2", "3", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", TypeDiscountedUsage, "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue", "clusterName"},
  214. columnIndexes: columnIndexes,
  215. userTagColumns: userTagColumns,
  216. awsTagColumns: awsTagColumns,
  217. want: &opencost.CloudCost{
  218. Properties: &opencost.CloudCostProperties{
  219. ProviderID: "resourceID",
  220. Provider: "AWS",
  221. AccountID: "usageAccountID",
  222. AccountName: "usageAccountID",
  223. InvoiceEntityID: "payerAccountID",
  224. InvoiceEntityName: "payerAccountID",
  225. RegionID: "regionCode",
  226. AvailabilityZone: "availabilityZone",
  227. Service: "productCode",
  228. Category: opencost.OtherCategory,
  229. Labels: opencost.CloudCostLabels{
  230. "test": "userTagTestValue",
  231. "eks:cluster-name": "clusterName",
  232. "aws:test": "awsTagTestValue",
  233. },
  234. },
  235. Window: opencost.NewClosedWindow(
  236. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  237. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  238. ),
  239. ListCost: opencost.CostMetric{
  240. Cost: 1,
  241. KubernetesPercent: 1,
  242. },
  243. NetCost: opencost.CostMetric{
  244. Cost: 2,
  245. KubernetesPercent: 1,
  246. },
  247. AmortizedNetCost: opencost.CostMetric{
  248. Cost: 4,
  249. KubernetesPercent: 1,
  250. },
  251. InvoicedCost: opencost.CostMetric{
  252. Cost: 2,
  253. KubernetesPercent: 1,
  254. },
  255. AmortizedCost: opencost.CostMetric{
  256. Cost: 3,
  257. KubernetesPercent: 1,
  258. },
  259. },
  260. wantErr: false,
  261. },
  262. {
  263. name: "valid Kubernetes SP no labels",
  264. row: []string{"1", "2", "3", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID", TypeSavingsPlanCoveredUsage, "AmazonEKS", "usageType", "regionCode", "availabilityZone", "", "", ""},
  265. columnIndexes: columnIndexes,
  266. userTagColumns: userTagColumns,
  267. awsTagColumns: awsTagColumns,
  268. want: &opencost.CloudCost{
  269. Properties: &opencost.CloudCostProperties{
  270. ProviderID: "resourceID",
  271. Provider: "AWS",
  272. AccountID: "usageAccountID",
  273. AccountName: "usageAccountID",
  274. InvoiceEntityID: "payerAccountID",
  275. InvoiceEntityName: "payerAccountID",
  276. RegionID: "regionCode",
  277. AvailabilityZone: "availabilityZone",
  278. Service: "AmazonEKS",
  279. Category: opencost.ManagementCategory,
  280. Labels: opencost.CloudCostLabels{},
  281. },
  282. Window: opencost.NewClosedWindow(
  283. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  284. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  285. ),
  286. ListCost: opencost.CostMetric{
  287. Cost: 1,
  288. KubernetesPercent: 1,
  289. },
  290. NetCost: opencost.CostMetric{
  291. Cost: 2,
  292. KubernetesPercent: 1,
  293. },
  294. AmortizedNetCost: opencost.CostMetric{
  295. Cost: 6,
  296. KubernetesPercent: 1,
  297. },
  298. InvoicedCost: opencost.CostMetric{
  299. Cost: 2,
  300. KubernetesPercent: 1,
  301. },
  302. AmortizedCost: opencost.CostMetric{
  303. Cost: 5,
  304. KubernetesPercent: 1,
  305. },
  306. },
  307. wantErr: false,
  308. },
  309. {
  310. name: "valid Kubernetes load balancer product code",
  311. row: []string{"1", "2", "3", "4", "5", "6", "2024-09-01T00:00:00Z", "payerAccountID", "usageAccountID", "resourceID/lbID", TypeSavingsPlanCoveredUsage, "AWSELB", "usageType", "regionCode", "availabilityZone", "", "", ""},
  312. columnIndexes: columnIndexes,
  313. userTagColumns: userTagColumns,
  314. awsTagColumns: awsTagColumns,
  315. want: &opencost.CloudCost{
  316. Properties: &opencost.CloudCostProperties{
  317. ProviderID: "lbID",
  318. Provider: "AWS",
  319. AccountID: "usageAccountID",
  320. AccountName: "usageAccountID",
  321. InvoiceEntityID: "payerAccountID",
  322. InvoiceEntityName: "payerAccountID",
  323. RegionID: "regionCode",
  324. AvailabilityZone: "availabilityZone",
  325. Service: "AWSELB",
  326. Category: opencost.NetworkCategory,
  327. Labels: opencost.CloudCostLabels{},
  328. },
  329. Window: opencost.NewClosedWindow(
  330. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  331. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  332. ),
  333. ListCost: opencost.CostMetric{
  334. Cost: 1,
  335. KubernetesPercent: 0,
  336. },
  337. NetCost: opencost.CostMetric{
  338. Cost: 2,
  339. KubernetesPercent: 0,
  340. },
  341. AmortizedNetCost: opencost.CostMetric{
  342. Cost: 6,
  343. KubernetesPercent: 0,
  344. },
  345. InvoicedCost: opencost.CostMetric{
  346. Cost: 2,
  347. KubernetesPercent: 0,
  348. },
  349. AmortizedCost: opencost.CostMetric{
  350. Cost: 5,
  351. KubernetesPercent: 0,
  352. },
  353. },
  354. wantErr: false,
  355. },
  356. }
  357. for _, tt := range tests {
  358. t.Run(tt.name, func(t *testing.T) {
  359. got, err := s3RowToCloudCost(tt.row, tt.columnIndexes, tt.userTagColumns, tt.awsTagColumns)
  360. if (err != nil) != tt.wantErr {
  361. t.Errorf("s3RowToCloudCost() error = %v, wantErr %v", err, tt.wantErr)
  362. return
  363. }
  364. if !reflect.DeepEqual(got, tt.want) {
  365. t.Errorf("s3RowToCloudCost() got = %v, want %v", got, tt.want)
  366. }
  367. })
  368. }
  369. }
  370. func Test_hasK8sLabel(t *testing.T) {
  371. tests := []struct {
  372. name string
  373. labels opencost.CloudCostLabels
  374. want bool
  375. }{
  376. {
  377. name: "empty",
  378. labels: opencost.CloudCostLabels{},
  379. want: false,
  380. },
  381. {
  382. name: "no k8s label",
  383. labels: opencost.CloudCostLabels{
  384. "key": "value",
  385. },
  386. want: false,
  387. },
  388. {
  389. name: "aws eks cluster name",
  390. labels: opencost.CloudCostLabels{
  391. TagAWSEKSClusterName: "value",
  392. },
  393. want: true,
  394. },
  395. {
  396. name: "eks cluster name",
  397. labels: opencost.CloudCostLabels{
  398. TagEKSClusterName: "value",
  399. },
  400. want: true,
  401. },
  402. {
  403. name: "eks ctl cluster name",
  404. labels: opencost.CloudCostLabels{
  405. TagEKSCtlClusterName: "value",
  406. },
  407. want: true,
  408. },
  409. {
  410. name: "kubernetes service name",
  411. labels: opencost.CloudCostLabels{
  412. TagKubernetesServiceName: "value",
  413. },
  414. want: true,
  415. },
  416. {
  417. name: "kubernetes pvc name",
  418. labels: opencost.CloudCostLabels{
  419. TagKubernetesPVCName: "value",
  420. },
  421. want: true,
  422. },
  423. {
  424. name: "kubernetes pv name",
  425. labels: opencost.CloudCostLabels{
  426. TagKubernetesPVName: "value",
  427. },
  428. want: true,
  429. },
  430. }
  431. for _, tt := range tests {
  432. t.Run(tt.name, func(t *testing.T) {
  433. if got := hasK8sLabel(tt.labels); got != tt.want {
  434. t.Errorf("hasK8sLabel() = %v, want %v", got, tt.want)
  435. }
  436. })
  437. }
  438. }