athenaintegration_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. package aws
  2. import (
  3. "os"
  4. "reflect"
  5. "testing"
  6. "time"
  7. "github.com/aws/aws-sdk-go-v2/service/athena/types"
  8. "github.com/opencost/opencost/core/pkg/opencost"
  9. "github.com/opencost/opencost/core/pkg/util/json"
  10. "github.com/opencost/opencost/core/pkg/util/timeutil"
  11. )
  12. func TestAthenaIntegration_GetCloudCost(t *testing.T) {
  13. athenaConfigPath := os.Getenv("ATHENA_CONFIGURATION")
  14. if athenaConfigPath == "" {
  15. t.Skip("skipping integration test, set environment variable ATHENA_CONFIGURATION")
  16. }
  17. athenaConfigBin, err := os.ReadFile(athenaConfigPath)
  18. if err != nil {
  19. t.Fatalf("failed to read config file: %s", err.Error())
  20. }
  21. var athenaConfig AthenaConfiguration
  22. err = json.Unmarshal(athenaConfigBin, &athenaConfig)
  23. if err != nil {
  24. t.Fatalf("failed to unmarshal config from JSON: %s", err.Error())
  25. }
  26. testCases := map[string]struct {
  27. integration *AthenaIntegration
  28. start time.Time
  29. end time.Time
  30. expected bool
  31. }{
  32. // No CUR data is expected within 2 days of now
  33. "too_recent_window": {
  34. integration: &AthenaIntegration{
  35. AthenaQuerier: AthenaQuerier{
  36. AthenaConfiguration: athenaConfig,
  37. },
  38. },
  39. end: time.Now(),
  40. start: time.Now().Add(-timeutil.Day),
  41. expected: true,
  42. },
  43. // CUR data should be available
  44. "last week window": {
  45. integration: &AthenaIntegration{
  46. AthenaQuerier: AthenaQuerier{
  47. AthenaConfiguration: athenaConfig,
  48. },
  49. },
  50. end: time.Now().Add(-7 * timeutil.Day),
  51. start: time.Now().Add(-8 * timeutil.Day),
  52. expected: false,
  53. },
  54. }
  55. for name, testCase := range testCases {
  56. t.Run(name, func(t *testing.T) {
  57. actual, err := testCase.integration.GetCloudCost(testCase.start, testCase.end)
  58. if err != nil {
  59. t.Errorf("Other error during testing %s", err)
  60. } else if actual.IsEmpty() != testCase.expected {
  61. t.Errorf("Incorrect result, actual emptiness: %t, expected: %t", actual.IsEmpty(), testCase.expected)
  62. }
  63. })
  64. }
  65. }
  66. func Test_athenaRowToCloudCost(t *testing.T) {
  67. aqi := AthenaQueryIndexes{
  68. ColumnIndexes: map[string]int{
  69. "ListCostColumn": 0,
  70. "NetCostColumn": 1,
  71. "AmortizedNetCostColumn": 2,
  72. "AmortizedCostColumn": 3,
  73. "IsK8sColumn": 4,
  74. AthenaDateTruncColumn: 5,
  75. "line_item_resource_id": 6,
  76. "bill_payer_account_id": 7,
  77. "line_item_usage_account_id": 8,
  78. "line_item_product_code": 9,
  79. "line_item_usage_type": 10,
  80. "product_region_code": 11,
  81. "line_item_availability_zone": 12,
  82. "resource_tags_user_test": 13,
  83. "resource_tags_aws_test": 14,
  84. },
  85. TagColumns: []string{"resource_tags_user_test"},
  86. AWSTagColumns: []string{"resource_tags_aws_test"},
  87. ListCostColumn: "ListCostColumn",
  88. NetCostColumn: "NetCostColumn",
  89. AmortizedNetCostColumn: "AmortizedNetCostColumn",
  90. AmortizedCostColumn: "AmortizedCostColumn",
  91. IsK8sColumn: "IsK8sColumn",
  92. }
  93. tests := []struct {
  94. name string
  95. row []string
  96. aqi AthenaQueryIndexes
  97. want *opencost.CloudCost
  98. wantErr bool
  99. }{
  100. {
  101. name: "incorrect row length",
  102. row: []string{"not enough elements"},
  103. aqi: aqi,
  104. want: nil,
  105. wantErr: true,
  106. },
  107. {
  108. name: "invalid list cost",
  109. row: []string{"invalid", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  110. aqi: aqi,
  111. want: nil,
  112. wantErr: true,
  113. },
  114. {
  115. name: "invalid net cost",
  116. row: []string{"1", "invalid", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  117. aqi: aqi,
  118. want: nil,
  119. wantErr: true,
  120. },
  121. {
  122. name: "invalid amortized net cost",
  123. row: []string{"1", "2", "invalid", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  124. aqi: aqi,
  125. want: nil,
  126. wantErr: true,
  127. },
  128. {
  129. name: "invalid amortized cost",
  130. row: []string{"1", "2", "3", "invalid", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  131. aqi: aqi,
  132. want: nil,
  133. wantErr: true,
  134. },
  135. {
  136. name: "invalid date",
  137. row: []string{"1", "2", "3", "4", "true", "invalid", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  138. aqi: aqi,
  139. want: nil,
  140. wantErr: true,
  141. },
  142. {
  143. name: "valid kubernetes with labels",
  144. row: []string{"1", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  145. aqi: aqi,
  146. want: &opencost.CloudCost{
  147. Properties: &opencost.CloudCostProperties{
  148. ProviderID: "resourceID",
  149. Provider: "AWS",
  150. AccountID: "usageAccountID",
  151. AccountName: "usageAccountID",
  152. InvoiceEntityID: "payerAccountID",
  153. InvoiceEntityName: "payerAccountID",
  154. RegionID: "regionCode",
  155. AvailabilityZone: "availabilityZone",
  156. Service: "productCode",
  157. Category: opencost.OtherCategory,
  158. Labels: opencost.CloudCostLabels{
  159. "test": "userTagTestValue",
  160. "aws_test": "awsTagTestValue",
  161. },
  162. },
  163. Window: opencost.NewClosedWindow(
  164. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  165. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  166. ),
  167. ListCost: opencost.CostMetric{
  168. Cost: 1,
  169. KubernetesPercent: 1,
  170. },
  171. NetCost: opencost.CostMetric{
  172. Cost: 2,
  173. KubernetesPercent: 1,
  174. },
  175. AmortizedNetCost: opencost.CostMetric{
  176. Cost: 3,
  177. KubernetesPercent: 1,
  178. },
  179. InvoicedCost: opencost.CostMetric{
  180. Cost: 2,
  181. KubernetesPercent: 1,
  182. },
  183. AmortizedCost: opencost.CostMetric{
  184. Cost: 4,
  185. KubernetesPercent: 1,
  186. },
  187. },
  188. wantErr: false,
  189. },
  190. {
  191. name: "valid non-kubernetes, no labels",
  192. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "", ""},
  193. aqi: aqi,
  194. want: &opencost.CloudCost{
  195. Properties: &opencost.CloudCostProperties{
  196. ProviderID: "resourceID",
  197. Provider: "AWS",
  198. AccountID: "usageAccountID",
  199. AccountName: "usageAccountID",
  200. InvoiceEntityID: "payerAccountID",
  201. InvoiceEntityName: "payerAccountID",
  202. RegionID: "regionCode",
  203. AvailabilityZone: "availabilityZone",
  204. Service: "productCode",
  205. Category: opencost.OtherCategory,
  206. Labels: opencost.CloudCostLabels{},
  207. },
  208. Window: opencost.NewClosedWindow(
  209. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  210. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  211. ),
  212. ListCost: opencost.CostMetric{
  213. Cost: 1,
  214. KubernetesPercent: 0,
  215. },
  216. NetCost: opencost.CostMetric{
  217. Cost: 2,
  218. KubernetesPercent: 0,
  219. },
  220. AmortizedNetCost: opencost.CostMetric{
  221. Cost: 3,
  222. KubernetesPercent: 0,
  223. },
  224. InvoicedCost: opencost.CostMetric{
  225. Cost: 2,
  226. KubernetesPercent: 0,
  227. },
  228. AmortizedCost: opencost.CostMetric{
  229. Cost: 4,
  230. KubernetesPercent: 0,
  231. },
  232. },
  233. wantErr: false,
  234. },
  235. {
  236. name: "valid load balancer product code",
  237. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID/lbID", "payerAccountID", "usageAccountID", "AWSELB", "usageType", "regionCode", "availabilityZone", "", ""},
  238. aqi: aqi,
  239. want: &opencost.CloudCost{
  240. Properties: &opencost.CloudCostProperties{
  241. ProviderID: "lbID",
  242. Provider: "AWS",
  243. AccountID: "usageAccountID",
  244. AccountName: "usageAccountID",
  245. InvoiceEntityID: "payerAccountID",
  246. InvoiceEntityName: "payerAccountID",
  247. RegionID: "regionCode",
  248. AvailabilityZone: "availabilityZone",
  249. Service: "AWSELB",
  250. Category: opencost.NetworkCategory,
  251. Labels: opencost.CloudCostLabels{},
  252. },
  253. Window: opencost.NewClosedWindow(
  254. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  255. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  256. ),
  257. ListCost: opencost.CostMetric{
  258. Cost: 1,
  259. KubernetesPercent: 0,
  260. },
  261. NetCost: opencost.CostMetric{
  262. Cost: 2,
  263. KubernetesPercent: 0,
  264. },
  265. AmortizedNetCost: opencost.CostMetric{
  266. Cost: 3,
  267. KubernetesPercent: 0,
  268. },
  269. InvoicedCost: opencost.CostMetric{
  270. Cost: 2,
  271. KubernetesPercent: 0,
  272. },
  273. AmortizedCost: opencost.CostMetric{
  274. Cost: 4,
  275. KubernetesPercent: 0,
  276. },
  277. },
  278. wantErr: false,
  279. },
  280. {
  281. name: "valid non-kubernetes, Fargate CPU",
  282. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "CPU", "regionCode", "availabilityZone", "", ""},
  283. aqi: aqi,
  284. want: &opencost.CloudCost{
  285. Properties: &opencost.CloudCostProperties{
  286. ProviderID: "123:pod/resource/CPU",
  287. Provider: "AWS",
  288. AccountID: "usageAccountID",
  289. AccountName: "usageAccountID",
  290. InvoiceEntityID: "payerAccountID",
  291. InvoiceEntityName: "payerAccountID",
  292. RegionID: "regionCode",
  293. AvailabilityZone: "availabilityZone",
  294. Service: "AmazonEKS",
  295. Category: opencost.ComputeCategory,
  296. Labels: opencost.CloudCostLabels{},
  297. },
  298. Window: opencost.NewClosedWindow(
  299. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  300. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  301. ),
  302. ListCost: opencost.CostMetric{
  303. Cost: 1,
  304. KubernetesPercent: 0,
  305. },
  306. NetCost: opencost.CostMetric{
  307. Cost: 2,
  308. KubernetesPercent: 0,
  309. },
  310. AmortizedNetCost: opencost.CostMetric{
  311. Cost: 3,
  312. KubernetesPercent: 0,
  313. },
  314. InvoicedCost: opencost.CostMetric{
  315. Cost: 2,
  316. KubernetesPercent: 0,
  317. },
  318. AmortizedCost: opencost.CostMetric{
  319. Cost: 4,
  320. KubernetesPercent: 0,
  321. },
  322. },
  323. wantErr: false,
  324. },
  325. {
  326. name: "valid non-kubernetes, Fargate RAM",
  327. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "GB", "regionCode", "availabilityZone", "", ""},
  328. aqi: aqi,
  329. want: &opencost.CloudCost{
  330. Properties: &opencost.CloudCostProperties{
  331. ProviderID: "123:pod/resource/RAM",
  332. Provider: "AWS",
  333. AccountID: "usageAccountID",
  334. AccountName: "usageAccountID",
  335. InvoiceEntityID: "payerAccountID",
  336. InvoiceEntityName: "payerAccountID",
  337. RegionID: "regionCode",
  338. AvailabilityZone: "availabilityZone",
  339. Service: "AmazonEKS",
  340. Category: opencost.ComputeCategory,
  341. Labels: opencost.CloudCostLabels{},
  342. },
  343. Window: opencost.NewClosedWindow(
  344. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  345. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  346. ),
  347. ListCost: opencost.CostMetric{
  348. Cost: 1,
  349. KubernetesPercent: 0,
  350. },
  351. NetCost: opencost.CostMetric{
  352. Cost: 2,
  353. KubernetesPercent: 0,
  354. },
  355. AmortizedNetCost: opencost.CostMetric{
  356. Cost: 3,
  357. KubernetesPercent: 0,
  358. },
  359. InvoicedCost: opencost.CostMetric{
  360. Cost: 2,
  361. KubernetesPercent: 0,
  362. },
  363. AmortizedCost: opencost.CostMetric{
  364. Cost: 4,
  365. KubernetesPercent: 0,
  366. },
  367. },
  368. wantErr: false,
  369. },
  370. }
  371. for _, tt := range tests {
  372. t.Run(tt.name, func(t *testing.T) {
  373. row := stringsToRow(tt.row)
  374. got, err := athenaRowToCloudCost(row, tt.aqi)
  375. if (err != nil) != tt.wantErr {
  376. t.Errorf("RowToCloudCost() error = %v, wantErr %v", err, tt.wantErr)
  377. return
  378. }
  379. if !reflect.DeepEqual(got, tt.want) {
  380. t.Errorf("RowToCloudCost() got = %v, want %v", got, tt.want)
  381. }
  382. })
  383. }
  384. }
  385. func stringsToRow(strings []string) types.Row {
  386. var data []types.Datum
  387. for _, str := range strings {
  388. varChar := str
  389. data = append(data, types.Datum{VarCharValue: &varChar})
  390. }
  391. return types.Row{Data: data}
  392. }