Ver código fonte

Merge branch 'develop' into jj/cloud-cost-status

Matt Ray 2 anos atrás
pai
commit
2ea81e7918

+ 10 - 1
.github/workflows/pr.yaml → .github/workflows/build-test.yaml

@@ -1,6 +1,10 @@
-name: Develop PR - build test
+name: Build/Test
 
 on:
+  push:
+    branches:
+      - develop
+
   pull_request:
     branches:
       - develop
@@ -41,6 +45,11 @@ jobs:
         name: Build
         run: |
           just build-local
+      - name: Upload code coverage
+        uses: actions/upload-artifact@v3
+        with:
+          name: oc-code-coverage
+          path: coverage.out
 
   frontend:
     runs-on: ubuntu-latest

+ 51 - 0
.github/workflows/sonar.yaml

@@ -0,0 +1,51 @@
+name: Sonar Code Coverage Upload
+on:
+  workflow_run:
+    workflows: ["Build/Test"]
+    types: [completed]
+jobs:
+  sonar:
+    name: Sonar
+    runs-on: ubuntu-latest
+    if: github.event.workflow_run.conclusion == 'success'
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          repository: ${{ github.event.workflow_run.head_repository.full_name }}
+          ref: ${{ github.event.workflow_run.head_branch }}
+          fetch-depth: 0
+      - name: 'Download code coverage'
+        uses: actions/github-script@v6
+        with:
+          script: |
+            let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
+               owner: context.repo.owner,
+               repo: context.repo.repo,
+               run_id: context.payload.workflow_run.id,
+            });
+            let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
+              return artifact.name == "oc-code-coverage"
+            })[0];
+            let download = await github.rest.actions.downloadArtifact({
+               owner: context.repo.owner,
+               repo: context.repo.repo,
+               artifact_id: matchArtifact.id,
+               archive_format: 'zip',
+            });
+            let fs = require('fs');
+            fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/oc-code-coverage.zip`, Buffer.from(download.data));
+      - name: 'Unzip code coverage'
+        run: unzip oc-code-coverage.zip -d coverage
+      - name: SonarCloud Scan
+        uses: sonarsource/sonarcloud-github-action@master
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+        with:
+          args: >
+            -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
+            -Dsonar.pullrequest.key=${{ github.event.workflow_run.pull_requests[0].number }}
+            -Dsonar.pullrequest.branch=${{ github.event.workflow_run.pull_requests[0].head.ref }}
+            -Dsonar.pullrequest.base=${{ github.event.workflow_run.pull_requests[0].base.ref }}
+            -Dsonar.projectKey=opencost_opencost
+            -Dsonar.organization=opencost

+ 1 - 0
ADOPTERS.MD

@@ -14,3 +14,4 @@ If you would like to be included in this table, please submit a PR to this file
 | Grafana Labs                               | *                                 | end user               | [How Grafana Labs uses and contributes to OpenCost](https://grafana.com/blog/2023/02/02/how-grafana-labs-uses-and-contributes-to-opencost-the-open-source-project-for-real-time-cost-monitoring-in-kubernetes/) |
 | Microsoft                                  | *                                 | Service Provider       | [Leverage OpenCost on Azure Kubernetes Service](http://aka.ms/aks/OpenCost-AKS) |
 | mindcurv group                             | *                                 | Consultancy            | [mindcurv group](https://mindcurv.com/en/) |
+| Zendesk                                    | *                                 | end user               | [Zendesk](https://www.zendesk.com/) |

+ 1 - 1
NOTICE

@@ -1,5 +1,5 @@
 OpenCost
-Copyright 2022 Cloud Native Computing Foundation
+Copyright 2022 - 2023 Cloud Native Computing Foundation
 
 This product includes software developed at
 The Cloud Native Computing Foundation (http://www.cncf.io).

+ 13 - 10
README.md

@@ -1,39 +1,42 @@
 <img src="./opencost-header.png"/>
 
-# OpenCost — your favorite open source cost monitoring tool for Kubernetes
+# OpenCost — your favorite open source cost monitoring tool for Kubernetes and cloud spend
 
-OpenCost models give teams visibility into current and historical Kubernetes spend and resource allocation. These models provide cost transparency in Kubernetes environments that support multiple applications, teams, departments, etc.
+OpenCost give teams visibility into current and historical Kubernetes and cloud spend and resource allocation.
+These models provide cost transparency in Kubernetes environments that support multiple applications, teams, departments, etc.
+It also provides visibility into the cloud costs across multiple providers.
 
 OpenCost was originally developed and open sourced by [Kubecost](https://kubecost.com). This project combines a [specification](/spec/) as well as a Golang implementation of these detailed requirements.
 
-![OpenCost allocation UI](./ui/src/opencost-ui.png)
+[![OpenCost UI Walkthrough](./ui/src/thumbnail.png)](https://youtu.be/lCP4Ci9Kcdg)
+*OpenCost UI Walkthrough*
 
 To see the full functionality of OpenCost you can view [OpenCost features](https://opencost.io). Here is a summary of features enabled:
 
 - Real-time cost allocation by Kubernetes cluster, node, namespace, controller kind, controller, service, or pod
-- Dynamic on-demand asset pricing enabled by integrations with AWS, Azure, and GCP billing APIs
+- Multi-cloud cost monitoring for all cloud services on AWS, Azure, GCP
+- Dynamic on-demand k8s asset pricing enabled by integrations with AWS, Azure, and GCP billing APIs
 - Supports on-prem k8s clusters with custom CSV pricing
-- Allocation for in-cluster resources like CPU, GPU, memory, and persistent volumes.
+- Allocation for in-cluster K8s resources like CPU, GPU, memory, and persistent volumes
 - Easily export pricing data to Prometheus with /metrics endpoint ([learn more](https://www.opencost.io/docs/installation/prometheus))
 - Free and open source distribution ([Apache2 license](LICENSE))
 
 ## Getting Started
 
-You can deploy OpenCost on any Kubernetes 1.8+ cluster in a matter of minutes, if not seconds!
+You can deploy OpenCost on any Kubernetes 1.20+ cluster in a matter of minutes, if not seconds!
 
-Visit the full documentation for [recommended install options](https://www.opencost.io/docs/installation/install).
+Visit the full documentation for [recommended installation options](https://www.opencost.io/docs/installation/install).
 
 ## Usage
 
 - [Cost APIs](https://www.opencost.io/docs/integrations/api)
 - [CLI / kubectl cost](https://www.opencost.io/docs/integrations/kubectl-cost)
 - [Prometheus Metrics](https://www.opencost.io/docs/integrations/prometheus)
-- Reference [User Interface](https://github.com/opencost/opencost/tree/develop/ui)
+- [User Interface](https://www.opencost.io/docs/installation/ui)
 
 ## Contributing
 
-We :heart: pull requests! See [`CONTRIBUTING.md`](CONTRIBUTING.md) for information on building the project from source
-and contributing changes.
+We :heart: pull requests! See [`CONTRIBUTING.md`](CONTRIBUTING.md) for information on building the project from source and contributing changes.
 
 ## Community
 

+ 10 - 10
go.mod

@@ -3,9 +3,9 @@ module github.com/opencost/opencost
 replace github.com/golang/lint => golang.org/x/lint v0.0.0-20180702182130-06c8688daad7
 
 require (
-	cloud.google.com/go/bigquery v1.48.0
+	cloud.google.com/go/bigquery v1.50.0
 	cloud.google.com/go/compute/metadata v0.2.3
-	cloud.google.com/go/storage v1.28.1
+	cloud.google.com/go/storage v1.29.0
 	github.com/Azure/azure-pipeline-go v0.2.3
 	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0
@@ -53,11 +53,11 @@ require (
 	go.etcd.io/bbolt v1.3.5
 	go.opentelemetry.io/otel v1.19.0
 	golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
-	golang.org/x/oauth2 v0.6.0
+	golang.org/x/oauth2 v0.7.0
 	golang.org/x/sync v0.1.0
 	golang.org/x/text v0.13.0
 	google.golang.org/api v0.114.0
-	google.golang.org/protobuf v1.29.1
+	google.golang.org/protobuf v1.30.0
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/api v0.25.3
 	k8s.io/apimachinery v0.25.3
@@ -67,8 +67,8 @@ require (
 
 require (
 	cloud.google.com/go v0.110.0 // indirect
-	cloud.google.com/go/compute v1.18.0 // indirect
-	cloud.google.com/go/iam v0.12.0 // indirect
+	cloud.google.com/go/compute v1.19.1 // indirect
+	cloud.google.com/go/iam v0.13.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
@@ -81,7 +81,7 @@ require (
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/andybalholm/brotli v1.0.4 // indirect
-	github.com/apache/arrow/go/v10 v10.0.1 // indirect
+	github.com/apache/arrow/go/v11 v11.0.0 // indirect
 	github.com/apache/thrift v0.16.0 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
@@ -110,7 +110,7 @@ require (
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/flatbuffers v2.0.8+incompatible // indirect
 	github.com/google/gnostic v0.5.7-v3refs // indirect
@@ -169,8 +169,8 @@ require (
 	golang.org/x/tools v0.6.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230320184635-7606e756e683 // indirect
-	google.golang.org/grpc v1.53.0 // indirect
+	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+	google.golang.org/grpc v1.56.3 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 21 - 20
go.sum

@@ -26,18 +26,18 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/bigquery v1.48.0 h1:u+fhS1jJOkPO9vdM84M8HO5VznTfVUicBeoXNKD26ho=
-cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac=
-cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
-cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/bigquery v1.50.0 h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ=
+cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
+cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
+cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/datacatalog v1.12.0 h1:3uaYULZRLByPdbuUvacGeqneudztEM4xqKQsBcxbDnY=
+cloud.google.com/go/datacatalog v1.13.0 h1:4H5IJiyUE0X6ShQBqgFFZvGGcrwGVndTwUSLP4c52gw=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/iam v0.12.0 h1:DRtTY29b75ciH6Ov1PHb4/iat2CLCvrOm40Q0a6DFpE=
-cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY=
+cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
+cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
 cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
@@ -48,8 +48,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
-cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
+cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
@@ -117,8 +117,8 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.62.3/go.mod h1:Api2AkmMgGaSUAhmk76oaFO
 github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI=
-github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
+github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM=
+github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
 github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY=
 github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -322,8 +322,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
@@ -852,8 +853,8 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
-golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
+golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1101,8 +1102,8 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20230320184635-7606e756e683 h1:khxVcsk/FhnzxMKOyD+TDGwjbEOpcPuIpmafPGFmhMA=
-google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1123,8 +1124,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1137,8 +1138,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
-google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 1 - 1
justfile

@@ -8,7 +8,7 @@ default:
 
 # Run unit tests
 test:
-    {{commonenv}} go test ./...
+    {{commonenv}} go test ./... -coverprofile=coverage.out
 
 # Compile a local binary
 build-local:

+ 2 - 2
kubernetes/opencost.yaml

@@ -142,7 +142,7 @@ spec:
       restartPolicy: Always
       serviceAccountName: opencost
       containers:
-        - image: gcr.io/kubecost1/opencost
+        - image: quay.io/kubecost1/kubecost-cost-model:latest
           name: opencost
           resources:
             requests:
@@ -167,7 +167,7 @@ spec:
             privileged: false
             readOnlyRootFilesystem: true
             runAsUser: 1001
-        - image: gcr.io/kubecost1/opencost-ui
+        - image: quay.io/kubecost1/opencost-ui:latest
           name: opencost-ui
           resources:
             requests:

+ 1 - 1
pkg/cloud/aws/athenaintegration.go

@@ -147,7 +147,7 @@ func (ai *AthenaIntegration) GetCloudCost(start, end time.Time) (*kubecost.Cloud
 	groupByStr := strings.Join(groupByColumns, ", ")
 	queryStr := `
 		SELECT %s
-		FROM %s
+		FROM "%s"
 		WHERE %s
 		GROUP BY %s
 	`

+ 2 - 5
pkg/cloud/azure/azurestorageintegration.go

@@ -67,11 +67,8 @@ func (asi *AzureStorageIntegration) GetCloudCost(start, end time.Time) (*kubecos
 			},
 		}
 
-		// Check if Item
-		if abv.IsCompute(cc.Properties.Category) {
-			// TODO: Will need to split VMSS for other features
-			ccsr.LoadCloudCost(cc)
-		}
+		ccsr.LoadCloudCost(cc)
+
 		return nil
 	})
 	if err != nil {

+ 0 - 1
pkg/cloud/gcp/provider.go

@@ -985,7 +985,6 @@ func (gcp *GCP) parsePages(inputKeys map[string]models.Key, pvKeys map[string]mo
 
 	url := gcp.getBillingAPIURL(gcp.APIKey, c.CurrencyCode)
 
-	log.Infof("Fetch GCP Billing Data from URL: %s", url)
 	var parsePagesHelper func(string) error
 	parsePagesHelper = func(pageToken string) error {
 		if pageToken == "done" {

+ 358 - 0
pkg/cloudcost/memoryrepository_test.go

@@ -0,0 +1,358 @@
+package cloudcost
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/pkg/kubecost"
+	"github.com/opencost/opencost/pkg/util/timeutil"
+)
+
+func TestMemoryRepository_Get(t *testing.T) {
+	defaultStart := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := defaultStart.Add(timeutil.Day)
+	defaultData := map[string]map[time.Time]*kubecost.CloudCostSet{
+		"key-1": {
+			defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+		},
+	}
+	tests := map[string]struct {
+		data      map[string]map[time.Time]*kubecost.CloudCostSet
+		startTime time.Time
+		key       string
+		want      *kubecost.CloudCostSet
+		wantErr   bool
+	}{
+		"No Data": {
+			data:      map[string]map[time.Time]*kubecost.CloudCostSet{},
+			startTime: defaultStart,
+			key:       "key-1",
+			want:      nil,
+			wantErr:   false,
+		},
+		"has data": {
+			data:      defaultData,
+			startTime: defaultStart,
+			key:       "key-1",
+			want:      DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+			wantErr:   false,
+		},
+		"wrong key": {
+			data:      defaultData,
+			startTime: defaultStart,
+			key:       "key-2",
+			want:      nil,
+			wantErr:   false,
+		},
+		"wrong time": {
+			data:      defaultData,
+			startTime: defaultEnd,
+			key:       "key-1",
+			want:      nil,
+			wantErr:   false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			m := &MemoryRepository{
+				data: tt.data,
+			}
+			got, err := m.Get(tt.startTime, tt.key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Get() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestMemoryRepository_Has(t *testing.T) {
+	defaultStart := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := defaultStart.Add(timeutil.Day)
+	defaultData := map[string]map[time.Time]*kubecost.CloudCostSet{
+		"key-1": {
+			defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+		},
+	}
+	tests := map[string]struct {
+		data      map[string]map[time.Time]*kubecost.CloudCostSet
+		startTime time.Time
+		key       string
+		want      bool
+		wantErr   bool
+	}{
+		"No Data": {
+			data:      map[string]map[time.Time]*kubecost.CloudCostSet{},
+			startTime: defaultStart,
+			key:       "key-1",
+			want:      false,
+			wantErr:   false,
+		},
+		"has data": {
+			data:      defaultData,
+			startTime: defaultStart,
+			key:       "key-1",
+			want:      true,
+			wantErr:   false,
+		},
+		"wrong key": {
+			data:      defaultData,
+			startTime: defaultStart,
+			key:       "key-2",
+			want:      false,
+			wantErr:   false,
+		},
+		"wrong time": {
+			data:      defaultData,
+			startTime: defaultEnd,
+			key:       "key-1",
+			want:      false,
+			wantErr:   false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			m := &MemoryRepository{
+				data: tt.data,
+			}
+			got, err := m.Has(tt.startTime, tt.key)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Has() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("Has() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestMemoryRepository_Keys(t *testing.T) {
+
+	tests := map[string]struct {
+		data    map[string]map[time.Time]*kubecost.CloudCostSet
+		want    []string
+		wantErr bool
+	}{
+		"empty": {
+			data:    map[string]map[time.Time]*kubecost.CloudCostSet{},
+			want:    []string{},
+			wantErr: false,
+		},
+		"one-key": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": nil,
+			},
+			want:    []string{"key-1"},
+			wantErr: false,
+		},
+		"two-key": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": nil,
+				"key-2": {
+					time.Now():        nil,
+					time.Now().Add(1): nil,
+				},
+			},
+			want:    []string{"key-1", "key-2"},
+			wantErr: false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			m := &MemoryRepository{
+				data: tt.data,
+			}
+			got, err := m.Keys()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Keys() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Keys() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestMemoryRepository_Put(t *testing.T) {
+	defaultStart := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := defaultStart.Add(timeutil.Day)
+
+	tests := map[string]struct {
+		data    map[string]map[time.Time]*kubecost.CloudCostSet
+		input   *kubecost.CloudCostSet
+		want    map[string]map[time.Time]*kubecost.CloudCostSet
+		wantErr bool
+	}{
+
+		"nil set": {
+			data:    map[string]map[time.Time]*kubecost.CloudCostSet{},
+			input:   nil,
+			want:    map[string]map[time.Time]*kubecost.CloudCostSet{},
+			wantErr: true,
+		},
+		"invalid window": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{},
+			input: &kubecost.CloudCostSet{
+				CloudCosts:  nil,
+				Window:      kubecost.Window{},
+				Integration: "key-1",
+			},
+			want:    map[string]map[time.Time]*kubecost.CloudCostSet{},
+			wantErr: true,
+		},
+		"invalid key": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{},
+			input: &kubecost.CloudCostSet{
+				CloudCosts:  nil,
+				Window:      kubecost.NewClosedWindow(defaultStart, defaultEnd),
+				Integration: "",
+			},
+			want:    map[string]map[time.Time]*kubecost.CloudCostSet{},
+			wantErr: true,
+		},
+		"valid input": {
+			data:  map[string]map[time.Time]*kubecost.CloudCostSet{},
+			input: DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+			want: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+				},
+			},
+			wantErr: false,
+		},
+		"overwrite": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "gcp", "key-1"),
+				},
+			},
+			input: DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+			want: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "aws", "key-1"),
+				},
+			},
+			wantErr: false,
+		},
+		"invalid overwrite": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "gcp", "key-1"),
+				},
+			},
+			input: &kubecost.CloudCostSet{
+				Window:      kubecost.NewWindow(&defaultStart, nil),
+				Integration: "key-1",
+			},
+			want: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					defaultStart: DefaultMockCloudCostSet(defaultStart, defaultEnd, "gcp", "key-1"),
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			m := &MemoryRepository{data: tt.data}
+
+			if err := m.Put(tt.input); (err != nil) != tt.wantErr {
+				t.Errorf("Put() error = %v, wantErr %v", err, tt.wantErr)
+			}
+
+			if !reflect.DeepEqual(m.data, tt.want) {
+				t.Errorf("Put() got = %v, want %v", m.data, tt.want)
+			}
+		})
+	}
+}
+
+func TestMemoryRepository_Expire(t *testing.T) {
+	dayOne := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+	dayTwo := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
+	dayThree := time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC)
+	tests := map[string]struct {
+		data    map[string]map[time.Time]*kubecost.CloudCostSet
+		limit   time.Time
+		want    map[string]map[time.Time]*kubecost.CloudCostSet
+		wantErr bool
+	}{
+		"no expire": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayTwo: nil,
+				},
+			},
+			limit: dayOne,
+			want: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayTwo: nil,
+				},
+			},
+			wantErr: false,
+		},
+		"limit match": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayTwo: nil,
+				},
+			},
+			limit: dayTwo,
+			want: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayTwo: nil,
+				},
+			},
+			wantErr: false,
+		},
+		"single expire": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayTwo: nil,
+				},
+			},
+			limit:   dayThree,
+			want:    map[string]map[time.Time]*kubecost.CloudCostSet{},
+			wantErr: false,
+		},
+		"one key expire": {
+			data: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayOne: nil,
+					dayTwo: nil,
+				},
+				"key-2": {
+					dayOne: nil,
+				},
+			},
+			limit: dayTwo,
+			want: map[string]map[time.Time]*kubecost.CloudCostSet{
+				"key-1": {
+					dayTwo: nil,
+				},
+			},
+			wantErr: false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			m := &MemoryRepository{
+				data: tt.data,
+			}
+			if err := m.Expire(tt.limit); (err != nil) != tt.wantErr {
+				t.Errorf("Expire() error = %v, wantErr %v", err, tt.wantErr)
+			}
+
+			if !reflect.DeepEqual(m.data, tt.want) {
+				t.Errorf("Expire() got = %v, want %v", m.data, tt.want)
+			}
+
+		})
+	}
+}

+ 90 - 0
pkg/cloudcost/mock.go

@@ -0,0 +1,90 @@
+package cloudcost
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/pkg/kubecost"
+)
+
+func DefaultMockCloudCostSet(start, end time.Time, provider, integration string) *kubecost.CloudCostSet {
+	ccs := kubecost.NewCloudCostSet(start, end)
+
+	ccs.Integration = integration
+
+	ccs.Insert(&kubecost.CloudCost{
+		Window: ccs.Window,
+		Properties: &kubecost.CloudCostProperties{
+			Provider:        provider,
+			AccountID:       "account1",
+			InvoiceEntityID: "invoiceEntity1",
+			Service:         provider + "-storage",
+			Category:        kubecost.StorageCategory,
+			Labels: kubecost.CloudCostLabels{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
+			},
+			ProviderID: "id1",
+		},
+		ListCost: kubecost.CostMetric{
+			Cost:              100,
+			KubernetesPercent: 0,
+		},
+		NetCost: kubecost.CostMetric{
+			Cost:              100,
+			KubernetesPercent: 0,
+		},
+	})
+
+	ccs.Insert(&kubecost.CloudCost{
+		Window: ccs.Window,
+		Properties: &kubecost.CloudCostProperties{
+			Provider:        provider,
+			AccountID:       "account1",
+			InvoiceEntityID: "invoiceEntity1",
+			Service:         provider + "-compute",
+			Category:        kubecost.ComputeCategory,
+			Labels: kubecost.CloudCostLabels{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
+			},
+			ProviderID: "id2",
+		},
+		ListCost: kubecost.CostMetric{
+			Cost:              2000,
+			KubernetesPercent: 1,
+		},
+		NetCost: kubecost.CostMetric{
+			Cost:              1800,
+			KubernetesPercent: 1,
+		},
+	})
+
+	ccs.Insert(&kubecost.CloudCost{
+		Window: ccs.Window,
+		Properties: &kubecost.CloudCostProperties{
+			Provider:        provider,
+			AccountID:       "account2",
+			InvoiceEntityID: "invoiceEntity2",
+			Service:         provider + "-compute",
+			Category:        kubecost.ComputeCategory,
+			Labels: kubecost.CloudCostLabels{
+				"label1": "value1",
+				"label2": "value2",
+				"label3": "value3",
+			},
+			ProviderID: "id3",
+		},
+		ListCost: kubecost.CostMetric{
+			Cost:              8000,
+			KubernetesPercent: 1,
+		},
+		NetCost: kubecost.CostMetric{
+			Cost:              8000,
+			KubernetesPercent: 1,
+		},
+	})
+
+	return ccs
+}

+ 118 - 0
pkg/cloudcost/querier_test.go

@@ -0,0 +1,118 @@
+package cloudcost
+
+import (
+	"testing"
+)
+
+func TestParseSortDirection(t *testing.T) {
+	tests := map[string]struct {
+		input   string
+		want    SortDirection
+		wantErr bool
+	}{
+		"Empty String": {
+			input:   "",
+			want:    SortDirectionNone,
+			wantErr: true,
+		},
+		"invalid input": {
+			input:   "invalid",
+			want:    SortDirectionNone,
+			wantErr: true,
+		},
+		"upper case ascending": {
+			input:   "ASC",
+			want:    SortDirectionAscending,
+			wantErr: false,
+		},
+		"lower case ascending": {
+			input:   "asc",
+			want:    SortDirectionAscending,
+			wantErr: false,
+		},
+		"upper case descending": {
+			input:   "DESC",
+			want:    SortDirectionDescending,
+			wantErr: false,
+		},
+		"lower case descending": {
+			input:   "desc",
+			want:    SortDirectionDescending,
+			wantErr: false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			got, err := ParseSortDirection(tt.input)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseSortDirection() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("ParseSortDirection() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestParseSortField(t *testing.T) {
+
+	tests := map[string]struct {
+		input   string
+		want    SortField
+		wantErr bool
+	}{
+		"Empty String": {
+			input:   "",
+			want:    SortFieldNone,
+			wantErr: true,
+		},
+		"invalid input": {
+			input:   "invalid",
+			want:    SortFieldNone,
+			wantErr: true,
+		},
+		"upper case cost": {
+			input:   "Cost",
+			want:    SortFieldCost,
+			wantErr: false,
+		},
+		"lower case cost": {
+			input:   "cost",
+			want:    SortFieldCost,
+			wantErr: false,
+		},
+		"upper case k8s %": {
+			input:   "KubernetesPercent",
+			want:    SortFieldKubernetesPercent,
+			wantErr: false,
+		},
+		"lower case k8s %": {
+			input:   "kubernetesPercent",
+			want:    SortFieldKubernetesPercent,
+			wantErr: false,
+		},
+		"upper case name": {
+			input:   "Name",
+			want:    SortFieldName,
+			wantErr: false,
+		},
+		"lower case Name": {
+			input:   "name",
+			want:    SortFieldName,
+			wantErr: false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			got, err := ParseSortField(tt.input)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseSortField() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("ParseSortField() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 8 - 171
pkg/cloudcost/queryservice.go

@@ -1,16 +1,12 @@
 package cloudcost
 
 import (
-	"encoding/csv"
 	"fmt"
 	"net/http"
 	"strings"
 
 	"github.com/julienschmidt/httprouter"
-	filter21 "github.com/opencost/opencost/pkg/filter21"
-	"github.com/opencost/opencost/pkg/filter21/cloudcost"
 	"github.com/opencost/opencost/pkg/kubecost"
-	"github.com/opencost/opencost/pkg/prom"
 	"github.com/opencost/opencost/pkg/util/httputil"
 	"go.opentelemetry.io/otel"
 )
@@ -52,7 +48,8 @@ func (s *QueryService) GetCloudCostHandler() func(w http.ResponseWriter, r *http
 			return
 		}
 
-		request, err := parseCloudCostRequest(r)
+		qp := httputil.NewQueryParams(r.URL.Query())
+		request, err := ParseCloudCostRequest(qp)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			return
@@ -89,7 +86,8 @@ func (s *QueryService) GetCloudCostViewGraphHandler() func(w http.ResponseWriter
 			return
 		}
 
-		request, err := parseCloudCostViewRequest(r)
+		qp := httputil.NewQueryParams(r.URL.Query())
+		request, err := parseCloudCostViewRequest(qp)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			return
@@ -131,7 +129,8 @@ func (s *QueryService) GetCloudCostViewTotalsHandler() func(w http.ResponseWrite
 			return
 		}
 
-		request, err := parseCloudCostViewRequest(r)
+		qp := httputil.NewQueryParams(r.URL.Query())
+		request, err := parseCloudCostViewRequest(qp)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			return
@@ -173,13 +172,13 @@ func (s *QueryService) GetCloudCostViewTableHandler() func(w http.ResponseWriter
 			return
 		}
 
-		request, err := parseCloudCostViewRequest(r)
+		qp := httputil.NewQueryParams(r.URL.Query())
+		request, err := parseCloudCostViewRequest(qp)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			return
 		}
 
-		qp := httputil.NewQueryParams(r.URL.Query())
 		format := qp.Get("format", "json")
 		if strings.HasPrefix(format, csvFormat) {
 			w.Header().Set("Content-Type", "text/csv")
@@ -206,165 +205,3 @@ func (s *QueryService) GetCloudCostViewTableHandler() func(w http.ResponseWriter
 		protocol.WriteData(w, resp)
 	}
 }
-
-func parseCloudCostRequest(r *http.Request) (*QueryRequest, error) {
-	qp := httputil.NewQueryParams(r.URL.Query())
-
-	windowStr := qp.Get("window", "")
-	if windowStr == "" {
-		return nil, fmt.Errorf("missing require window param")
-	}
-
-	window, err := kubecost.ParseWindowUTC(windowStr)
-	if err != nil {
-		return nil, fmt.Errorf("invalid window parameter: %w", err)
-	}
-	if window.IsOpen() {
-		return nil, fmt.Errorf("invalid window parameter: %s", window.String())
-	}
-
-	aggregateByRaw := qp.GetList("aggregate", ",")
-	aggregateBy := []string{}
-	for _, aggBy := range aggregateByRaw {
-		prop, err := ParseCloudCostProperty(aggBy)
-		if err != nil {
-			return nil, fmt.Errorf("error parsing aggregate by %v", err)
-		}
-		aggregateBy = append(aggregateBy, prop)
-	}
-	if len(aggregateBy) == 0 {
-		aggregateBy = []string{
-			kubecost.CloudCostInvoiceEntityIDProp,
-			kubecost.CloudCostAccountIDProp,
-			kubecost.CloudCostProviderProp,
-			kubecost.CloudCostProviderIDProp,
-			kubecost.CloudCostCategoryProp,
-			kubecost.CloudCostServiceProp,
-		}
-	}
-
-	accumulate := kubecost.ParseAccumulate(qp.Get("accumulate", ""))
-
-	var filter filter21.Filter
-	filterString := qp.Get("filter", "")
-	if filterString != "" {
-		parser := cloudcost.NewCloudCostFilterParser()
-		filter, err = parser.Parse(filterString)
-		if err != nil {
-			return nil, fmt.Errorf("Parsing 'filter' parameter: %s", err)
-		}
-	}
-
-	opts := &QueryRequest{
-		Start:       *window.Start(),
-		End:         *window.End(),
-		AggregateBy: aggregateBy,
-		Accumulate:  accumulate,
-		Filter:      filter,
-	}
-
-	return opts, nil
-}
-
-func ParseCloudCostProperty(text string) (string, error) {
-	switch strings.TrimSpace(strings.ToLower(text)) {
-	case strings.ToLower(kubecost.CloudCostInvoiceEntityIDProp):
-		return kubecost.CloudCostInvoiceEntityIDProp, nil
-	case strings.ToLower(kubecost.CloudCostAccountIDProp):
-		return kubecost.CloudCostAccountIDProp, nil
-	case strings.ToLower(kubecost.CloudCostProviderProp):
-		return kubecost.CloudCostProviderProp, nil
-	case strings.ToLower(kubecost.CloudCostProviderIDProp):
-		return kubecost.CloudCostProviderIDProp, nil
-	case strings.ToLower(kubecost.CloudCostCategoryProp):
-		return kubecost.CloudCostCategoryProp, nil
-	case strings.ToLower(kubecost.CloudCostServiceProp):
-		return kubecost.CloudCostServiceProp, nil
-	}
-
-	if strings.HasPrefix(text, "label:") {
-		label := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "label:")))
-		return fmt.Sprintf("label:%s", label), nil
-	}
-
-	return "", fmt.Errorf("invalid cloud cost property: %s", text)
-}
-
-func parseCloudCostViewRequest(r *http.Request) (*ViewQueryRequest, error) {
-	qr, err := parseCloudCostRequest(r)
-	if err != nil {
-		return nil, err
-	}
-	qp := httputil.NewQueryParams(r.URL.Query())
-
-	// parse cost metric
-	costMetricName, err := kubecost.ParseCostMetricName(qp.Get("costMetric", string(kubecost.CostMetricAmortizedNetCost)))
-	if err != nil {
-		return nil, fmt.Errorf("error parsing 'costMetric': %w", err)
-	}
-
-	limit := qp.GetInt("limit", 0)
-	offset := qp.GetInt("offset", 0)
-
-	// parse order
-	order, err := ParseSortDirection(qp.Get("sortByOrder", "desc"))
-	if err != nil {
-		return nil, fmt.Errorf("error parsing 'sortByOrder: %w", err)
-	}
-
-	sortColumn, err := ParseSortField(qp.Get("sortBy", "cost"))
-	if err != nil {
-		return nil, fmt.Errorf("error parsing 'sortBy': %w", err)
-	}
-
-	return &ViewQueryRequest{
-		QueryRequest:     *qr,
-		CostMetricName:   costMetricName,
-		ChartItemsLength: DefaultChartItemsLength,
-		Limit:            limit,
-		Offset:           offset,
-		SortDirection:    order,
-		SortColumn:       sortColumn,
-	}, nil
-}
-
-// CloudCostViewTableRowsToCSV takes the csv writer and writes the ViewTableRows into the writer.
-func CloudCostViewTableRowsToCSV(writer *csv.Writer, ctr ViewTableRows, window string) error {
-	defer writer.Flush()
-	// Write the column headers
-	headers := []string{
-		"Name",
-		"K8s Utilization",
-		"Total",
-		"Window",
-	}
-	err := writer.Write(headers)
-	if err != nil {
-		return fmt.Errorf("CloudCostViewTableRowsToCSV: failed to convert ViewTableRows to csv with error: %w", err)
-	}
-
-	// Write one row per entry in the ViewTableRows
-	for _, row := range ctr {
-		err = writer.Write([]string{
-			row.Name,
-			fmt.Sprintf("%.3f", row.KubernetesPercent),
-			fmt.Sprintf("%.3f", row.Cost),
-			window,
-		})
-		if err != nil {
-			return fmt.Errorf("CloudCostViewTableRowsToCSV: failed to convert ViewTableRows to csv with error: %w", err)
-		}
-	}
-
-	return nil
-}
-
-func writeCloudCostViewTableRowsAsCSV(w http.ResponseWriter, ctr ViewTableRows, window string) {
-	writer := csv.NewWriter(w)
-
-	err := CloudCostViewTableRowsToCSV(writer, ctr, window)
-	if err != nil {
-		protocol.WriteError(w, protocol.InternalServerError(err.Error()))
-		return
-	}
-}

+ 170 - 0
pkg/cloudcost/queryservice_helper.go

@@ -0,0 +1,170 @@
+package cloudcost
+
+import (
+	"encoding/csv"
+	"fmt"
+	"net/http"
+	"strings"
+
+	filter21 "github.com/opencost/opencost/pkg/filter21"
+	"github.com/opencost/opencost/pkg/filter21/cloudcost"
+	"github.com/opencost/opencost/pkg/kubecost"
+	"github.com/opencost/opencost/pkg/prom"
+	"github.com/opencost/opencost/pkg/util/httputil"
+)
+
+func ParseCloudCostRequest(qp httputil.QueryParams) (*QueryRequest, error) {
+
+	windowStr := qp.Get("window", "")
+	if windowStr == "" {
+		return nil, fmt.Errorf("missing require window param")
+	}
+
+	window, err := kubecost.ParseWindowUTC(windowStr)
+	if err != nil {
+		return nil, fmt.Errorf("invalid window parameter: %w", err)
+	}
+	if window.IsOpen() {
+		return nil, fmt.Errorf("invalid window parameter: %s", window.String())
+	}
+
+	aggregateByRaw := qp.GetList("aggregate", ",")
+	var aggregateBy []string
+	for _, aggBy := range aggregateByRaw {
+		prop, err := ParseCloudCostProperty(aggBy)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing aggregate by %v", err)
+		}
+		aggregateBy = append(aggregateBy, prop)
+	}
+
+	accumulate := kubecost.ParseAccumulate(qp.Get("accumulate", ""))
+
+	var filter filter21.Filter
+	filterString := qp.Get("filter", "")
+	if filterString != "" {
+		parser := cloudcost.NewCloudCostFilterParser()
+		filter, err = parser.Parse(filterString)
+		if err != nil {
+			return nil, fmt.Errorf("Parsing 'filter' parameter: %s", err)
+		}
+	}
+
+	opts := &QueryRequest{
+		Start:       *window.Start(),
+		End:         *window.End(),
+		AggregateBy: aggregateBy,
+		Accumulate:  accumulate,
+		Filter:      filter,
+	}
+
+	return opts, nil
+}
+
+func ParseCloudCostProperty(text string) (string, error) {
+	switch strings.TrimSpace(strings.ToLower(text)) {
+	case strings.ToLower(kubecost.CloudCostInvoiceEntityIDProp):
+		return kubecost.CloudCostInvoiceEntityIDProp, nil
+	case strings.ToLower(kubecost.CloudCostAccountIDProp):
+		return kubecost.CloudCostAccountIDProp, nil
+	case strings.ToLower(kubecost.CloudCostProviderProp):
+		return kubecost.CloudCostProviderProp, nil
+	case strings.ToLower(kubecost.CloudCostProviderIDProp):
+		return kubecost.CloudCostProviderIDProp, nil
+	case strings.ToLower(kubecost.CloudCostCategoryProp):
+		return kubecost.CloudCostCategoryProp, nil
+	case strings.ToLower(kubecost.CloudCostServiceProp):
+		return kubecost.CloudCostServiceProp, nil
+	}
+
+	if strings.HasPrefix(text, "label:") {
+		label := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "label:")))
+		return fmt.Sprintf("label:%s", label), nil
+	}
+
+	return "", fmt.Errorf("invalid cloud cost property: %s", text)
+}
+
+func parseCloudCostViewRequest(qp httputil.QueryParams) (*ViewQueryRequest, error) {
+	qr, err := ParseCloudCostRequest(qp)
+	if err != nil {
+		return nil, err
+	}
+
+	// parse cost metric
+	costMetricName, err := kubecost.ParseCostMetricName(qp.Get("costMetric", string(kubecost.CostMetricAmortizedNetCost)))
+	if err != nil {
+		return nil, fmt.Errorf("error parsing 'costMetric': %w", err)
+	}
+
+	limit := qp.GetInt("limit", 0)
+	if limit < 0 {
+		return nil, fmt.Errorf("invalid value for limit %d", limit)
+	}
+	offset := qp.GetInt("offset", 0)
+	if offset < 0 {
+		return nil, fmt.Errorf("invalid value for offset %d", offset)
+	}
+
+	// parse order
+	order, err := ParseSortDirection(qp.Get("sortByOrder", "desc"))
+	if err != nil {
+		return nil, fmt.Errorf("error parsing 'sortByOrder: %w", err)
+	}
+
+	sortColumn, err := ParseSortField(qp.Get("sortBy", "cost"))
+	if err != nil {
+		return nil, fmt.Errorf("error parsing 'sortBy': %w", err)
+	}
+
+	return &ViewQueryRequest{
+		QueryRequest:     *qr,
+		CostMetricName:   costMetricName,
+		ChartItemsLength: DefaultChartItemsLength,
+		Limit:            limit,
+		Offset:           offset,
+		SortDirection:    order,
+		SortColumn:       sortColumn,
+	}, nil
+}
+
+// CloudCostViewTableRowsToCSV takes the csv writer and writes the ViewTableRows into the writer.
+func CloudCostViewTableRowsToCSV(writer *csv.Writer, ctr ViewTableRows, window string) error {
+	defer writer.Flush()
+	// Write the column headers
+	headers := []string{
+		"Name",
+		"K8s Utilization",
+		"Total",
+		"Window",
+	}
+	err := writer.Write(headers)
+	if err != nil {
+		return fmt.Errorf("CloudCostViewTableRowsToCSV: failed to convert ViewTableRows to csv with error: %w", err)
+	}
+
+	// Write one row per entry in the ViewTableRows
+	for _, row := range ctr {
+		err = writer.Write([]string{
+			row.Name,
+			fmt.Sprintf("%.3f", row.KubernetesPercent),
+			fmt.Sprintf("%.3f", row.Cost),
+			window,
+		})
+		if err != nil {
+			return fmt.Errorf("CloudCostViewTableRowsToCSV: failed to convert ViewTableRows to csv with error: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func writeCloudCostViewTableRowsAsCSV(w http.ResponseWriter, ctr ViewTableRows, window string) {
+	writer := csv.NewWriter(w)
+
+	err := CloudCostViewTableRowsToCSV(writer, ctr, window)
+	if err != nil {
+		protocol.WriteError(w, protocol.InternalServerError(err.Error()))
+		return
+	}
+}

+ 136 - 0
pkg/cloudcost/queryservice_helper_test.go

@@ -0,0 +1,136 @@
+package cloudcost
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/pkg/filter21/cloudcost"
+	"github.com/opencost/opencost/pkg/kubecost"
+	"github.com/opencost/opencost/pkg/util/httputil"
+)
+
+func TestParseCloudCostRequest(t *testing.T) {
+	windowStr := "2023-01-01T00:00:00Z,2023-01-02T00:00:00Z"
+	start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+	end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
+	validFilterStr := `service:"AmazonEC2"`
+	parser := cloudcost.NewCloudCostFilterParser()
+	validFilter, _ := parser.Parse(validFilterStr)
+	tests := map[string]struct {
+		values  map[string][]string
+		want    *QueryRequest
+		wantErr bool
+	}{
+		"missing window": {
+			values:  map[string][]string{},
+			want:    nil,
+			wantErr: true,
+		},
+		"invalid window": {
+			values: map[string][]string{
+				"window": {"invalid"},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		"valid window": {
+			values: map[string][]string{
+				"window": {windowStr},
+			},
+			want: &QueryRequest{
+				Start:       start,
+				End:         end,
+				AggregateBy: nil,
+				Accumulate:  "",
+				Filter:      nil,
+			},
+			wantErr: false,
+		},
+		"valid aggregate": {
+			values: map[string][]string{
+				"window":    {windowStr},
+				"aggregate": {"invoiceEntityID,accountID,label:app"},
+			},
+			want: &QueryRequest{
+				Start:       start,
+				End:         end,
+				AggregateBy: []string{kubecost.CloudCostInvoiceEntityIDProp, kubecost.CloudCostAccountIDProp, "label:app"},
+				Accumulate:  "",
+				Filter:      nil,
+			},
+			wantErr: false,
+		},
+		"invalid aggregate": {
+			values: map[string][]string{
+				"window":    {windowStr},
+				"aggregate": {"invalid"},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		"valid accumulate": {
+			values: map[string][]string{
+				"window":     {windowStr},
+				"accumulate": {"week"},
+			},
+			want: &QueryRequest{
+				Start:       start,
+				End:         end,
+				AggregateBy: nil,
+				Accumulate:  kubecost.AccumulateOptionWeek,
+				Filter:      nil,
+			},
+			wantErr: false,
+		},
+		"invalid accumulate": {
+			values: map[string][]string{
+				"window":     {windowStr},
+				"accumulate": {"invalid"},
+			},
+			want: &QueryRequest{
+				Start:       start,
+				End:         end,
+				AggregateBy: nil,
+				Accumulate:  kubecost.AccumulateOptionNone,
+				Filter:      nil,
+			},
+			wantErr: false,
+		},
+		"valid filter": {
+			values: map[string][]string{
+				"window": {windowStr},
+				"filter": {validFilterStr},
+			},
+			want: &QueryRequest{
+				Start:       start,
+				End:         end,
+				AggregateBy: nil,
+				Accumulate:  kubecost.AccumulateOptionNone,
+				Filter:      validFilter,
+			},
+			wantErr: false,
+		},
+		"invalid filter": {
+			values: map[string][]string{
+				"window": {windowStr},
+				"filter": {"invalid"},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			qp := httputil.NewQueryParams(tt.values)
+			got, err := ParseCloudCostRequest(qp)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseCloudCostRequest() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ParseCloudCostRequest() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 10 - 3
pkg/cloudcost/repositoryquerier.go

@@ -220,10 +220,17 @@ func (rq *RepositoryQuerier) QueryViewTable(request ViewQueryRequest, ctx contex
 		return make([]*ViewTableRow, 0), nil
 	}
 
-	limit := request.Offset + request.Limit
-	if limit > len(rows) {
+	if request.Limit > 0 {
+		limit := request.Offset + request.Limit
+		if limit > len(rows) {
+			return rows[request.Offset:], nil
+		}
+		return rows[request.Offset:limit], nil
+	}
+
+	if request.Offset > 0 {
 		return rows[request.Offset:], nil
 	}
 
-	return rows[request.Offset:limit], nil
+	return rows, nil
 }

+ 15 - 31
pkg/costmodel/costmodel.go

@@ -144,39 +144,25 @@ const (
 		label_replace(
 			label_replace(
 				avg(
-					count_over_time(kube_pod_container_resource_requests{resource="memory", unit="byte", container!="",container!="POD", node!="", %s}[%s] %s)
-					*
-					avg_over_time(kube_pod_container_resource_requests{resource="memory", unit="byte", container!="",container!="POD", node!="", %s}[%s] %s)
+					sum_over_time(kube_pod_container_resource_requests{resource="memory", unit="byte", container!="",container!="POD", node!="", %s}[%s] %s)
 				) by (namespace,container,pod,node,%s) , "container_name","$1","container","(.+)"
 			), "pod_name","$1","pod","(.+)"
 		)
 	) by (namespace,container_name,pod_name,node,%s)`
-	queryRAMUsageStr = `sort_desc(
-		avg(
-			label_replace(
-				label_replace(
-					label_replace(
-						count_over_time(container_memory_working_set_bytes{container!="", container!="POD", instance!="", %s}[%s] %s), "node", "$1", "instance", "(.+)"
-					), "container_name", "$1", "container", "(.+)"
-				), "pod_name", "$1", "pod", "(.+)"
-			)
-			*
+	queryRAMUsageStr = `avg(
+		label_replace(
 			label_replace(
 				label_replace(
-					label_replace(
-						avg_over_time(container_memory_working_set_bytes{container!="", container!="POD", instance!="", %s}[%s] %s), "node", "$1", "instance", "(.+)"
-					), "container_name", "$1", "container", "(.+)"
-				), "pod_name", "$1", "pod", "(.+)"
-			)
-		) by (namespace, container_name, pod_name, node, %s)
-	)`
+					sum_over_time(container_memory_working_set_bytes{container!="", container!="POD", instance!="", %s}[%s] %s), "node", "$1", "instance", "(.+)"
+				), "container_name", "$1", "container", "(.+)"
+			), "pod_name", "$1", "pod", "(.+)"
+		)
+	) by (namespace, container_name, pod_name, node, %s)`
 	queryCPURequestsStr = `avg(
 		label_replace(
 			label_replace(
 				avg(
-					count_over_time(kube_pod_container_resource_requests{resource="cpu", unit="core", container!="",container!="POD", node!="", %s}[%s] %s)
-					*
-					avg_over_time(kube_pod_container_resource_requests{resource="cpu", unit="core", container!="",container!="POD", node!="", %s}[%s] %s)
+					sum_over_time(kube_pod_container_resource_requests{resource="cpu", unit="core", container!="",container!="POD", node!="", %s}[%s] %s)
 				) by (namespace,container,pod,node,%s) , "container_name","$1","container","(.+)"
 			), "pod_name","$1","pod","(.+)"
 		)
@@ -196,9 +182,7 @@ const (
 		label_replace(
 			label_replace(
 				avg(
-					count_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD", node!="", %s}[%s] %s)
-					*
-					avg_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD", node!="", %s}[%s] %s)
+					sum_over_time(kube_pod_container_resource_requests{resource="nvidia_com_gpu", container!="",container!="POD", node!="", %s}[%s] %s)
 					* %f
 				) by (namespace,container,pod,node,%s) , "container_name","$1","container","(.+)"
 			), "pod_name","$1","pod","(.+)"
@@ -253,7 +237,7 @@ const (
 )
 
 func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, cp costAnalyzerCloud.Provider, window string, offset string, filterNamespace string) (map[string]*CostData, error) {
-	queryRAMUsage := fmt.Sprintf(queryRAMUsageStr, env.GetPromClusterFilter(), window, offset, env.GetPromClusterFilter(), window, offset, env.GetPromClusterLabel())
+	queryRAMUsage := fmt.Sprintf(queryRAMUsageStr, env.GetPromClusterFilter(), window, offset, env.GetPromClusterLabel())
 	queryCPUUsage := fmt.Sprintf(queryCPUUsageStr, env.GetPromClusterFilter(), window, offset, env.GetPromClusterLabel())
 	queryNetZoneRequests := fmt.Sprintf(queryZoneNetworkUsage, env.GetPromClusterFilter(), window, "", env.GetPromClusterLabel())
 	queryNetRegionRequests := fmt.Sprintf(queryRegionNetworkUsage, env.GetPromClusterFilter(), window, "", env.GetPromClusterLabel())
@@ -1699,11 +1683,11 @@ func (cm *CostModel) costDataRange(cli prometheusClient.Client, cp costAnalyzerC
 
 	queryRAMAlloc := fmt.Sprintf(queryRAMAllocationByteHours, env.GetPromClusterFilter(), resStr, env.GetPromClusterLabel(), scrapeIntervalSeconds)
 	queryCPUAlloc := fmt.Sprintf(queryCPUAllocationVCPUHours, env.GetPromClusterFilter(), resStr, env.GetPromClusterLabel(), scrapeIntervalSeconds)
-	queryRAMRequests := fmt.Sprintf(queryRAMRequestsStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel(), env.GetPromClusterLabel())
-	queryRAMUsage := fmt.Sprintf(queryRAMUsageStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel())
-	queryCPURequests := fmt.Sprintf(queryCPURequestsStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel(), env.GetPromClusterLabel())
+	queryRAMRequests := fmt.Sprintf(queryRAMRequestsStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel(), env.GetPromClusterLabel())
+	queryRAMUsage := fmt.Sprintf(queryRAMUsageStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel())
+	queryCPURequests := fmt.Sprintf(queryCPURequestsStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel(), env.GetPromClusterLabel())
 	queryCPUUsage := fmt.Sprintf(queryCPUUsageStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel())
-	queryGPURequests := fmt.Sprintf(queryGPURequestsStr, env.GetPromClusterFilter(), resStr, "", env.GetPromClusterFilter(), resStr, "", resolution.Hours(), env.GetPromClusterLabel(), env.GetPromClusterLabel(), env.GetPromClusterLabel(), env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel())
+	queryGPURequests := fmt.Sprintf(queryGPURequestsStr, env.GetPromClusterFilter(), resStr, "", resolution.Hours(), env.GetPromClusterLabel(), env.GetPromClusterLabel(), env.GetPromClusterLabel(), env.GetPromClusterFilter(), resStr, "", env.GetPromClusterLabel())
 	queryPVRequests := fmt.Sprintf(queryPVRequestsStr, env.GetPromClusterFilter(), env.GetPromClusterLabel(), env.GetPromClusterLabel(), env.GetPromClusterFilter(), env.GetPromClusterLabel(), env.GetPromClusterLabel())
 	queryPVCAllocation := fmt.Sprintf(queryPVCAllocationFmt, env.GetPromClusterFilter(), resStr, env.GetPromClusterLabel(), scrapeIntervalSeconds)
 	queryPVHourlyCost := fmt.Sprintf(queryPVHourlyCostFmt, env.GetPromClusterFilter(), resStr)

+ 2 - 1
pkg/kubecost/asset.go

@@ -3841,6 +3841,7 @@ func (asr *AssetSetRange) InsertRange(that *AssetSetRange) error {
 	}
 
 	var err error
+	var as *AssetSet
 	for _, thatAS := range that.Assets {
 		if thatAS == nil || err != nil {
 			continue
@@ -3852,7 +3853,7 @@ func (asr *AssetSetRange) InsertRange(that *AssetSetRange) error {
 			err = fmt.Errorf("cannot merge AssetSet into window that does not exist: %s", thatAS.Window.String())
 			continue
 		}
-		as, err := asr.Get(i)
+		as, err = asr.Get(i)
 		if err != nil {
 			err = fmt.Errorf("AssetSetRange index does not exist: %d", i)
 			continue

BIN
ui/src/opencost-ui.png


BIN
ui/src/thumbnail.png