athenaintegration.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. package aws
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/aws/aws-sdk-go-v2/service/athena/types"
  9. "github.com/opencost/opencost/pkg/cloud"
  10. "github.com/opencost/opencost/pkg/kubecost"
  11. "github.com/opencost/opencost/pkg/log"
  12. "github.com/opencost/opencost/pkg/util/timeutil"
  13. )
  14. const LabelColumnPrefix = "resource_tags_user_"
  15. // athenaDateLayout is the default AWS date format
  16. const AthenaDateLayout = "2006-01-02 15:04:05.000"
  17. // Cost Columns
  18. const AthenaPricingColumn = "line_item_unblended_cost"
  19. // Amortized Cost Columns
  20. const AthenaRIPricingColumn = "reservation_effective_cost"
  21. const AthenaSPPricingColumn = "savings_plan_savings_plan_effective_cost"
  22. // Net Cost Columns
  23. const AthenaNetPricingColumn = "line_item_net_unblended_cost"
  24. // Amortized Net Cost Columns
  25. const AthenaNetRIPricingColumn = "reservation_net_effective_cost"
  26. const AthenaNetSPPricingColumn = "savings_plan_net_savings_plan_effective_cost"
  27. // Category Columns
  28. const AthenaIsNode = "SUBSTRING(line_item_resource_id,1,2) = 'i-'"
  29. const AthenaIsVol = "SUBSTRING(line_item_resource_id, 1, 4) = 'vol-'"
  30. const AthenaIsNetwork = "line_item_usage_type LIKE '%Bytes'"
  31. // athenaDateTruncColumn Aggregates line items from the hourly level to daily. "line_item_usage_start_date" is used because at
  32. // all time values 00:00-23:00 it will truncate to the correct date.
  33. const AthenaDateColumn = "line_item_usage_start_date"
  34. const AthenaDateTruncColumn = "DATE_TRUNC('day'," + AthenaDateColumn + ") as usage_date"
  35. const AthenaWhereDateFmt = `line_item_usage_start_date >= date '%s' AND line_item_usage_start_date < date '%s'`
  36. const AthenaWhereUsage = "(line_item_line_item_type = 'Usage' OR line_item_line_item_type = 'DiscountedUsage' OR line_item_line_item_type = 'SavingsPlanCoveredUsage' OR line_item_line_item_type = 'EdpDiscount' OR line_item_line_item_type = 'PrivateRateDiscount')"
  37. // AthenaQueryIndexes is a struct for holding the context of a query
  38. type AthenaQueryIndexes struct {
  39. Query string
  40. ColumnIndexes map[string]int
  41. TagColumns []string
  42. ListCostColumn string
  43. ListK8sCostColumn string
  44. NetCostColumn string
  45. NetK8sCostColumn string
  46. AmortizedNetCostColumn string
  47. AmortizedNetK8sCostColumn string
  48. AmortizedCostColumn string
  49. AmortizedK8sCostColumn string
  50. InvoicedCostColumn string
  51. InvoicedK8sCostColumn string
  52. }
  53. type AthenaIntegration struct {
  54. AthenaQuerier
  55. }
  56. // Query Athena for CUR data and build a new CloudCostSetRange containing the info
  57. func (ai *AthenaIntegration) GetCloudCost(start, end time.Time) (*kubecost.CloudCostSetRange, error) {
  58. log.Infof("AthenaIntegration[%s]: GetCloudCost: %s", ai.Key(), kubecost.NewWindow(&start, &end).String())
  59. // Query for all column names
  60. allColumns, err := ai.GetColumns()
  61. if err != nil {
  62. return nil, fmt.Errorf("GetCloudCost: error getting Athena columns: %w", err)
  63. }
  64. // List known, hard-coded columns to query
  65. groupByColumns := []string{
  66. AthenaDateTruncColumn,
  67. "line_item_resource_id",
  68. "bill_payer_account_id",
  69. "line_item_usage_account_id",
  70. "line_item_product_code",
  71. "line_item_usage_type",
  72. AthenaIsNode,
  73. AthenaIsVol,
  74. AthenaIsNetwork,
  75. }
  76. // Create query indices
  77. aqi := AthenaQueryIndexes{}
  78. // Determine which columns are user-defined tags and add those to the list
  79. // of columns to query.
  80. for column := range allColumns {
  81. if strings.HasPrefix(column, LabelColumnPrefix) {
  82. groupByColumns = append(groupByColumns, column)
  83. aqi.TagColumns = append(aqi.TagColumns, column)
  84. }
  85. }
  86. var selectColumns []string
  87. // Duplicate GroupBy Columns into select columns
  88. selectColumns = append(selectColumns, groupByColumns...)
  89. // Clean Up group by columns
  90. ai.RemoveColumnAliases(groupByColumns)
  91. // Build list cost column and add it to the select columns
  92. listCostColumn := fmt.Sprintf("SUM(%s) as list_cost", ai.GetListCostColumn())
  93. selectColumns = append(selectColumns, listCostColumn)
  94. aqi.ListCostColumn = listCostColumn
  95. listK8sCostColumn := fmt.Sprintf(
  96. "SUM(%s) as list_kubernetes_cost",
  97. ai.GetKubernetesCostColumn(allColumns, ai.GetListCostColumn()),
  98. )
  99. selectColumns = append(selectColumns, listK8sCostColumn)
  100. aqi.ListK8sCostColumn = listK8sCostColumn
  101. // Build net cost column and add it to select columns
  102. netCostColumn := fmt.Sprintf("SUM(%s) as net_cost", ai.GetNetCostColumn(allColumns))
  103. selectColumns = append(selectColumns, netCostColumn)
  104. aqi.NetCostColumn = netCostColumn
  105. netK8sCostColumn := fmt.Sprintf(
  106. "SUM(%s) as net_kubernetes_cost",
  107. ai.GetKubernetesCostColumn(allColumns, ai.GetNetCostColumn(allColumns)),
  108. )
  109. selectColumns = append(selectColumns, netK8sCostColumn)
  110. aqi.NetK8sCostColumn = netK8sCostColumn
  111. // Build amortized net cost column and add it to select columns
  112. amortizedNetCostColumn := fmt.Sprintf("SUM(%s) as amortized_net_cost", ai.GetAmortizedNetCostColumn(allColumns))
  113. selectColumns = append(selectColumns, amortizedNetCostColumn)
  114. aqi.AmortizedNetCostColumn = amortizedNetCostColumn
  115. amortizedNetK8sCostColumn := fmt.Sprintf(
  116. "SUM(%s) as amortized_net_kubernetes_cost",
  117. ai.GetKubernetesCostColumn(allColumns, ai.GetNetCostColumn(allColumns)),
  118. )
  119. selectColumns = append(selectColumns, amortizedNetK8sCostColumn)
  120. aqi.AmortizedNetK8sCostColumn = amortizedNetK8sCostColumn
  121. // Build Amortized cost column and add it to select columns
  122. amortizedCostColumn := fmt.Sprintf("SUM(%s) as amortized_cost", ai.GetAmortizedCostCase(allColumns))
  123. selectColumns = append(selectColumns, amortizedCostColumn)
  124. aqi.AmortizedCostColumn = amortizedCostColumn
  125. amortizedK8sCostColumn := fmt.Sprintf(
  126. "SUM(%s) as amortized_kubernetes_cost",
  127. ai.GetKubernetesCostColumn(allColumns, ai.GetAmortizedCostCase(allColumns)),
  128. )
  129. selectColumns = append(selectColumns, amortizedK8sCostColumn)
  130. aqi.AmortizedK8sCostColumn = amortizedK8sCostColumn
  131. // We are using Net Cost for Invoiced Cost for now as it is the closest approximation
  132. invoicedCostColumn := netCostColumn
  133. selectColumns = append(selectColumns, invoicedCostColumn)
  134. aqi.InvoicedCostColumn = invoicedCostColumn
  135. invoicedK8sCostColumn := netK8sCostColumn
  136. selectColumns = append(selectColumns, invoicedK8sCostColumn)
  137. aqi.InvoicedK8sCostColumn = invoicedK8sCostColumn
  138. // Build map of query columns to use for parsing query
  139. aqi.ColumnIndexes = map[string]int{}
  140. for i, column := range selectColumns {
  141. aqi.ColumnIndexes[column] = i
  142. }
  143. athenaWhereDate := fmt.Sprintf(AthenaWhereDateFmt, start.Format("2006-01-02"), end.Format("2006-01-02"))
  144. // Query for all line items with a resource_id or from AWS Marketplace, which did not end before
  145. // the range or start after it. This captures all costs with any amount of
  146. // overlap with the range, for which we will only extract the relevant costs
  147. whereConjuncts := []string{
  148. athenaWhereDate,
  149. AthenaWhereUsage,
  150. }
  151. columnStr := strings.Join(selectColumns, ", ")
  152. whereClause := strings.Join(whereConjuncts, " AND ")
  153. groupByStr := strings.Join(groupByColumns, ", ")
  154. queryStr := `
  155. SELECT %s
  156. FROM %s
  157. WHERE %s
  158. GROUP BY %s
  159. `
  160. aqi.Query = fmt.Sprintf(queryStr, columnStr, ai.Table, whereClause, groupByStr)
  161. ccsr, err := kubecost.NewCloudCostSetRange(start, end, timeutil.Day, ai.Key())
  162. if err != nil {
  163. return nil, err
  164. }
  165. // Generate row handling function.
  166. rowHandler := func(row types.Row) {
  167. err2 := ai.RowToCloudCost(row, aqi, ccsr)
  168. if err2 != nil {
  169. log.Errorf("AthenaIntegration: GetCloudCost: error while parsing row: %s", err2.Error())
  170. }
  171. }
  172. log.Debugf("AthenaIntegration[%s]: GetCloudCost: querying: %s", ai.Key(), aqi.Query)
  173. // Query CUR data and fill out CCSR
  174. err = ai.Query(context.TODO(), aqi.Query, GetAthenaQueryFunc(rowHandler))
  175. if err != nil {
  176. return nil, err
  177. }
  178. for _, ccs := range ccsr.CloudCostSets {
  179. log.Debugf("AthenaIntegration[%s]: GetCloudCost: writing compute items for window %s: %d", ai.Key(), ccs.Window, len(ccs.CloudCosts))
  180. ai.ConnectionStatus = ai.GetConnectionStatusFromResult(ccs, ai.ConnectionStatus)
  181. }
  182. return ccsr, nil
  183. }
  184. func (ai *AthenaIntegration) GetListCostColumn() string {
  185. var listCostBuilder strings.Builder
  186. listCostBuilder.WriteString("CASE line_item_line_item_type")
  187. listCostBuilder.WriteString(" WHEN 'EdpDiscount' THEN 0")
  188. listCostBuilder.WriteString(" WHEN 'PrivateRateDiscount' THEN 0")
  189. listCostBuilder.WriteString(" ELSE ")
  190. listCostBuilder.WriteString(AthenaPricingColumn)
  191. listCostBuilder.WriteString(" END")
  192. return listCostBuilder.String()
  193. }
  194. func (ai *AthenaIntegration) GetNetCostColumn(allColumns map[string]bool) string {
  195. netCostColumn := ""
  196. if allColumns[AthenaNetPricingColumn] { // if Net pricing exists
  197. netCostColumn = AthenaNetPricingColumn
  198. } else { // Non-net for if there's no net pricing.
  199. netCostColumn = AthenaPricingColumn
  200. }
  201. return netCostColumn
  202. }
  203. func (ai *AthenaIntegration) GetAmortizedNetCostColumn(allColumns map[string]bool) string {
  204. amortizedNetCostCase := ""
  205. if allColumns[AthenaNetPricingColumn] { // if Net pricing exists
  206. amortizedNetCostCase = ai.GetAmortizedNetCostCase(allColumns)
  207. } else { // Non-net for if there's no net pricing.
  208. amortizedNetCostCase = ai.GetAmortizedCostCase(allColumns)
  209. }
  210. return amortizedNetCostCase
  211. }
  212. // getIsKubernetesColumn generates a boolean column which determines whether a line item is from kubernetes
  213. func (ai *AthenaIntegration) GetIsKubernetesColumn(allColumns map[string]bool) string {
  214. return ai.GetIsKubernetesCase(allColumns)
  215. }
  216. // getKubernetesCostColumn generates a double column which determines the cost of k8s items in an aggregate
  217. func (ai *AthenaIntegration) GetKubernetesCostColumn(allColumns map[string]bool, pricingCase string) string {
  218. k8sCase := ai.GetIsKubernetesCase(allColumns)
  219. return fmt.Sprintf("CAST((%s) as double) * (%s)", k8sCase, pricingCase)
  220. }
  221. func (ai *AthenaIntegration) RemoveColumnAliases(columns []string) {
  222. for i, column := range columns {
  223. if strings.Contains(column, " as ") {
  224. columnValues := strings.Split(column, " as ")
  225. columns[i] = columnValues[0]
  226. }
  227. }
  228. }
  229. func (ai *AthenaIntegration) ConvertLabelToAWSTag(label string) string {
  230. // if the label already has the column prefix assume that it is in the correct format
  231. if strings.HasPrefix(label, LabelColumnPrefix) {
  232. return label
  233. }
  234. // replace characters with underscore
  235. tag := label
  236. tag = strings.ReplaceAll(tag, ".", "_")
  237. tag = strings.ReplaceAll(tag, "/", "_")
  238. tag = strings.ReplaceAll(tag, ":", "_")
  239. tag = strings.ReplaceAll(tag, "-", "_")
  240. // add prefix and return
  241. return LabelColumnPrefix + tag
  242. }
  243. func (ai *AthenaIntegration) GetAmortizedCostCase(allColumns map[string]bool) string {
  244. // Use unblended costs if Reserved Instances/Savings Plans aren't in use
  245. if !allColumns[AthenaRIPricingColumn] && !allColumns[AthenaSPPricingColumn] {
  246. return AthenaPricingColumn
  247. }
  248. var costBuilder strings.Builder
  249. costBuilder.WriteString("CASE line_item_line_item_type")
  250. if allColumns[AthenaRIPricingColumn] {
  251. costBuilder.WriteString(" WHEN 'DiscountedUsage' THEN ")
  252. costBuilder.WriteString(AthenaRIPricingColumn)
  253. }
  254. if allColumns[AthenaSPPricingColumn] {
  255. costBuilder.WriteString(" WHEN 'SavingsPlanCoveredUsage' THEN ")
  256. costBuilder.WriteString(AthenaSPPricingColumn)
  257. }
  258. costBuilder.WriteString(" ELSE ")
  259. costBuilder.WriteString(AthenaPricingColumn)
  260. costBuilder.WriteString(" END")
  261. return costBuilder.String()
  262. }
  263. func (ai *AthenaIntegration) GetAmortizedNetCostCase(allColumns map[string]bool) string {
  264. // Use net unblended costs if Reserved Instances/Savings Plans aren't in use
  265. if !allColumns[AthenaNetRIPricingColumn] && !allColumns[AthenaNetSPPricingColumn] {
  266. return AthenaNetPricingColumn
  267. }
  268. var costBuilder strings.Builder
  269. costBuilder.WriteString("CASE line_item_line_item_type")
  270. if allColumns[AthenaNetRIPricingColumn] {
  271. costBuilder.WriteString(" WHEN 'DiscountedUsage' THEN ")
  272. costBuilder.WriteString(AthenaNetRIPricingColumn)
  273. }
  274. if allColumns[AthenaNetSPPricingColumn] {
  275. costBuilder.WriteString(" WHEN 'SavingsPlanCoveredUsage' THEN ")
  276. costBuilder.WriteString(AthenaNetSPPricingColumn)
  277. }
  278. costBuilder.WriteString(" ELSE ")
  279. costBuilder.WriteString(AthenaNetPricingColumn)
  280. costBuilder.WriteString(" END")
  281. return costBuilder.String()
  282. }
  283. // GetIsKubernetesCase builds a "CASE" clause which attempts to determine if a line item is kubernetes based on labels
  284. // that may be available in the CUR
  285. func (ai *AthenaIntegration) GetIsKubernetesCase(allColumns map[string]bool) string {
  286. // k8sColumns is a list of columns where the presence of a value indicates that a resource is part of a kubernetes cluster
  287. k8sColumns := []string{
  288. "resource_tags_aws_eks_cluster_name",
  289. "resource_tags_user_eks_cluster_name",
  290. "resource_tags_user_alpha_eksctl_io_cluster_name",
  291. "resource_tags_user_kubernetes_io_service_name",
  292. "resource_tags_user_kubernetes_io_created_for_pvc_name",
  293. "resource_tags_user_kubernetes_io_created_for_pv_name",
  294. }
  295. var k8sBuilder strings.Builder
  296. k8sBuilder.WriteString("CASE ")
  297. // EKS is always kubernetes
  298. k8sBuilder.WriteString("WHEN line_item_product_code = 'AmazonEKS' THEN TRUE ")
  299. for _, k8sColumn := range k8sColumns {
  300. if _, ok := allColumns[k8sColumn]; ok {
  301. k8sBuilder.WriteString("WHEN ")
  302. k8sBuilder.WriteString(k8sColumn)
  303. k8sBuilder.WriteString(" <> '' THEN TRUE ")
  304. }
  305. }
  306. k8sBuilder.WriteString("ELSE FALSE END")
  307. return k8sBuilder.String()
  308. }
  309. func (ai *AthenaIntegration) RowToCloudCost(row types.Row, aqi AthenaQueryIndexes, ccsr *kubecost.CloudCostSetRange) error {
  310. if len(row.Data) < len(aqi.ColumnIndexes) {
  311. return fmt.Errorf("rowToCloudCost: row with fewer than %d columns (has only %d)", len(aqi.ColumnIndexes), len(row.Data))
  312. }
  313. // Iterate through the slice of tag columns, assigning
  314. // values to the column names, minus the tag prefix.
  315. labels := kubecost.CloudCostLabels{}
  316. labelValues := []string{}
  317. for _, tagColumnName := range aqi.TagColumns {
  318. labelName := strings.TrimPrefix(tagColumnName, LabelColumnPrefix)
  319. value := GetAthenaRowValue(row, aqi.ColumnIndexes, tagColumnName)
  320. if value != "" {
  321. labels[labelName] = value
  322. labelValues = append(labelValues, value)
  323. }
  324. }
  325. invoiceEntityID := GetAthenaRowValue(row, aqi.ColumnIndexes, "bill_payer_account_id")
  326. accountID := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_usage_account_id")
  327. startStr := GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaDateTruncColumn)
  328. providerID := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_resource_id")
  329. productCode := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_product_code")
  330. usageType := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_usage_type")
  331. isNode, _ := strconv.ParseBool(GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaIsNode))
  332. isVol, _ := strconv.ParseBool(GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaIsVol))
  333. isNetwork, _ := strconv.ParseBool(GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaIsNetwork))
  334. listCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.ListCostColumn)
  335. if err != nil {
  336. return err
  337. }
  338. listK8sCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.ListK8sCostColumn)
  339. if err != nil {
  340. return err
  341. }
  342. netCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.NetCostColumn)
  343. if err != nil {
  344. return err
  345. }
  346. netK8sCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.NetK8sCostColumn)
  347. if err != nil {
  348. return err
  349. }
  350. amortizedNetCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.AmortizedNetCostColumn)
  351. if err != nil {
  352. return err
  353. }
  354. amortizedNetK8sCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.AmortizedNetK8sCostColumn)
  355. if err != nil {
  356. return err
  357. }
  358. amortizedCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.AmortizedCostColumn)
  359. if err != nil {
  360. return err
  361. }
  362. amortizedK8sCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.AmortizedK8sCostColumn)
  363. if err != nil {
  364. return err
  365. }
  366. invoicedCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.InvoicedCostColumn)
  367. if err != nil {
  368. return err
  369. }
  370. invoicedK8sCost, err := GetAthenaRowValueFloat(row, aqi.ColumnIndexes, aqi.InvoicedK8sCostColumn)
  371. if err != nil {
  372. return err
  373. }
  374. // Identify resource category in the CUR
  375. category := SelectAWSCategory(isNode, isVol, isNetwork, providerID, productCode)
  376. // Retrieve final stanza of product code for ProviderID
  377. if productCode == "AWSELB" || productCode == "AmazonFSx" {
  378. providerID = ParseARN(providerID)
  379. }
  380. if productCode == "AmazonEKS" && category == kubecost.ComputeCategory {
  381. if strings.Contains(usageType, "CPU") {
  382. providerID = fmt.Sprintf("%s/CPU", providerID)
  383. } else if strings.Contains(usageType, "GB") {
  384. providerID = fmt.Sprintf("%s/RAM", providerID)
  385. }
  386. }
  387. properties := kubecost.CloudCostProperties{
  388. ProviderID: providerID,
  389. Provider: kubecost.AWSProvider,
  390. AccountID: accountID,
  391. InvoiceEntityID: invoiceEntityID,
  392. Service: productCode,
  393. Category: category,
  394. Labels: labels,
  395. }
  396. start, err := time.Parse(AthenaDateLayout, startStr)
  397. if err != nil {
  398. return fmt.Errorf("unable to parse %s: '%s'", AthenaDateTruncColumn, err.Error())
  399. }
  400. end := start.AddDate(0, 0, 1)
  401. cc := &kubecost.CloudCost{
  402. Properties: &properties,
  403. Window: kubecost.NewWindow(&start, &end),
  404. ListCost: kubecost.CostMetric{
  405. Cost: listCost,
  406. KubernetesPercent: ai.CalculateK8sPercent(listCost, listK8sCost),
  407. },
  408. NetCost: kubecost.CostMetric{
  409. Cost: netCost,
  410. KubernetesPercent: ai.CalculateK8sPercent(netCost, netK8sCost),
  411. },
  412. AmortizedNetCost: kubecost.CostMetric{
  413. Cost: amortizedNetCost,
  414. KubernetesPercent: ai.CalculateK8sPercent(amortizedNetCost, amortizedNetK8sCost),
  415. },
  416. AmortizedCost: kubecost.CostMetric{
  417. Cost: amortizedCost,
  418. KubernetesPercent: ai.CalculateK8sPercent(amortizedCost, amortizedK8sCost),
  419. },
  420. InvoicedCost: kubecost.CostMetric{
  421. Cost: invoicedCost,
  422. KubernetesPercent: ai.CalculateK8sPercent(invoicedCost, invoicedK8sCost),
  423. },
  424. }
  425. ccsr.LoadCloudCost(cc)
  426. return nil
  427. }
  428. func (ai *AthenaIntegration) CalculateK8sPercent(cost, k8sCost float64) float64 {
  429. // Calculate percent of cost that is k8s with the k8sCost
  430. k8sPercent := 0.0
  431. if k8sCost != 0.0 && cost != 0.0 {
  432. k8sPercent = k8sCost / cost
  433. }
  434. return k8sPercent
  435. }
  436. func (ai *AthenaIntegration) GetConnectionStatusFromResult(result cloud.EmptyChecker, currentStatus cloud.ConnectionStatus) cloud.ConnectionStatus {
  437. if result.IsEmpty() && currentStatus != cloud.SuccessfulConnection {
  438. return cloud.MissingData
  439. }
  440. return cloud.SuccessfulConnection
  441. }
  442. func (ai *AthenaIntegration) GetConnectionStatus() string {
  443. // initialize status if it has not done so; this can happen if the integration is inactive
  444. if ai.ConnectionStatus.String() == "" {
  445. ai.ConnectionStatus = cloud.InitialStatus
  446. }
  447. return ai.ConnectionStatus.String()
  448. }