Przeglądaj źródła

Merge branch 'develop' into update-go-version-20260319-151109

Warwick 2 miesięcy temu
rodzic
commit
fca3279cbe

+ 4 - 0
CLAUDE.md

@@ -2,6 +2,10 @@
 
 
 This document provides guidance for AI assistants working with the OpenCost codebase.
 This document provides guidance for AI assistants working with the OpenCost codebase.
 
 
+## AI Assistant Behaviour
+
+- Never include claude.ai session links or URLs in commit messages or pull request bodies.
+
 ## Project Overview
 ## Project Overview
 
 
 OpenCost is an open source Kubernetes cost monitoring tool maintained by the Cloud Native Computing Foundation (CNCF). It provides real-time cost allocation, asset tracking, and cloud cost monitoring for Kubernetes clusters across multiple cloud providers.
 OpenCost is an open source Kubernetes cost monitoring tool maintained by the Cloud Native Computing Foundation (CNCF). It provides real-time cost allocation, asset tracking, and cloud cost monitoring for Kubernetes clusters across multiple cloud providers.

+ 18 - 18
go.mod

@@ -21,14 +21,14 @@ require (
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.13
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.13
 	github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // Todo: Upgrade to V2 SDK as V1 is now archived
 	github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // Todo: Upgrade to V2 SDK as V1 is now archived
 	github.com/aws/aws-sdk-go v1.55.8
 	github.com/aws/aws-sdk-go v1.55.8
-	github.com/aws/aws-sdk-go-v2 v1.41.3
-	github.com/aws/aws-sdk-go-v2/config v1.32.11
-	github.com/aws/aws-sdk-go-v2/credentials v1.19.11
+	github.com/aws/aws-sdk-go-v2 v1.41.4
+	github.com/aws/aws-sdk-go-v2/config v1.32.12
+	github.com/aws/aws-sdk-go-v2/credentials v1.19.12
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6
 	github.com/aws/aws-sdk-go-v2/service/athena v1.57.1
 	github.com/aws/aws-sdk-go-v2/service/athena v1.57.1
 	github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0
 	github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
-	github.com/aws/aws-sdk-go-v2/service/sts v1.41.8
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
+	github.com/aws/aws-sdk-go-v2/service/sts v1.41.9
 	github.com/aws/smithy-go v1.24.2
 	github.com/aws/smithy-go v1.24.2
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 	github.com/go-playground/validator/v10 v10.30.1
 	github.com/go-playground/validator/v10 v10.30.1
@@ -72,7 +72,7 @@ require (
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
 	github.com/Masterminds/semver/v3 v3.4.0 // indirect
 	github.com/Masterminds/semver/v3 v3.4.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
 	github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
 	github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
 	github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
 	github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
@@ -145,18 +145,18 @@ require (
 	github.com/Azure/go-autorest/tracing v0.6.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.1 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
 	github.com/apache/arrow/go/v15 v15.0.2 // indirect
 	github.com/apache/arrow/go/v15 v15.0.2 // indirect
-	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect

+ 36 - 36
go.sum

@@ -92,48 +92,48 @@ github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcy
 github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
 github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
 github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
 github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
 github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
 github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
-github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
-github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
-github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
-github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
+github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
+github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
+github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
+github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6 h1:xuOfOJR0SPBrHhzAXZ5c+8i1KyJ+aUVJ2cl8DT16qH4=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6 h1:xuOfOJR0SPBrHhzAXZ5c+8i1KyJ+aUVJ2cl8DT16qH4=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6/go.mod h1:rUVOV4y5upo55JxPss99p9FaN9BvqUjFgE/N54tvLuE=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6/go.mod h1:rUVOV4y5upo55JxPss99p9FaN9BvqUjFgE/N54tvLuE=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
 github.com/aws/aws-sdk-go-v2/service/athena v1.57.1 h1:I+YmJvkeAN+r4mUlH2CODoYyAkoP0cQWGItPPkK60hk=
 github.com/aws/aws-sdk-go-v2/service/athena v1.57.1 h1:I+YmJvkeAN+r4mUlH2CODoYyAkoP0cQWGItPPkK60hk=
 github.com/aws/aws-sdk-go-v2/service/athena v1.57.1/go.mod h1:yZ507NVXolOco9hA2+mKH3ELnLEOZ/4mGqkrp2phNYs=
 github.com/aws/aws-sdk-go-v2/service/athena v1.57.1/go.mod h1:yZ507NVXolOco9hA2+mKH3ELnLEOZ/4mGqkrp2phNYs=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0 h1:dgdIaG/GCiXMo16HAdFwpjt9Vn34bD2WVH5SiZdwzUc=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0 h1:dgdIaG/GCiXMo16HAdFwpjt9Vn34bD2WVH5SiZdwzUc=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0/go.mod h1:2dMnUs1QzlGzsm46i9oBHAxVHQp7b6qF7PljWcgVEVE=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0/go.mod h1:2dMnUs1QzlGzsm46i9oBHAxVHQp7b6qF7PljWcgVEVE=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
 github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
 github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
 github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
 github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=

+ 0 - 29
pkg/cloud/aws/athenaconfiguration.go

@@ -18,7 +18,6 @@ type AthenaConfiguration struct {
 	Workgroup  string     `json:"workgroup"`
 	Workgroup  string     `json:"workgroup"`
 	Account    string     `json:"account"`
 	Account    string     `json:"account"`
 	Authorizer Authorizer `json:"authorizer"`
 	Authorizer Authorizer `json:"authorizer"`
-	CURVersion string     `json:"curVersion,omitempty"` // "1.0" or "2.0", defaults to "2.0" if not specified
 }
 }
 
 
 func (ac *AthenaConfiguration) Validate() error {
 func (ac *AthenaConfiguration) Validate() error {
@@ -54,11 +53,6 @@ func (ac *AthenaConfiguration) Validate() error {
 		return fmt.Errorf("AthenaConfiguration: missing account")
 		return fmt.Errorf("AthenaConfiguration: missing account")
 	}
 	}
 
 
-	// Validate CURVersion if specified
-	if ac.CURVersion != "" && ac.CURVersion != "1.0" && ac.CURVersion != "2.0" {
-		return fmt.Errorf("AthenaConfiguration: invalid CURVersion '%s', must be '1.0' or '2.0'", ac.CURVersion)
-	}
-
 	return nil
 	return nil
 }
 }
 
 
@@ -109,10 +103,6 @@ func (ac *AthenaConfiguration) Equals(config cloud.Config) bool {
 		return false
 		return false
 	}
 	}
 
 
-	if ac.CURVersion != thatConfig.CURVersion {
-		return false
-	}
-
 	return true
 	return true
 }
 }
 
 
@@ -126,7 +116,6 @@ func (ac *AthenaConfiguration) Sanitize() cloud.Config {
 		Workgroup:  ac.Workgroup,
 		Workgroup:  ac.Workgroup,
 		Account:    ac.Account,
 		Account:    ac.Account,
 		Authorizer: ac.Authorizer.Sanitize().(Authorizer),
 		Authorizer: ac.Authorizer.Sanitize().(Authorizer),
-		CURVersion: ac.CURVersion,
 	}
 	}
 }
 }
 
 
@@ -201,18 +190,6 @@ func (ac *AthenaConfiguration) UnmarshalJSON(b []byte) error {
 	}
 	}
 	ac.Authorizer = authorizer
 	ac.Authorizer = authorizer
 
 
-	// Parse CURVersion if present (optional field)
-	if _, ok := fmap["curVersion"]; ok {
-		curVersion, err := cloud.GetInterfaceValue[string](fmap, "curVersion")
-		if err != nil {
-			return fmt.Errorf("AthenaConfiguration: UnmarshalJSON: %w", err)
-		}
-		ac.CURVersion = curVersion
-	} else {
-		// Default to 2.0 if not specified
-		ac.CURVersion = "2.0"
-	}
-
 	return nil
 	return nil
 }
 }
 
 
@@ -243,11 +220,6 @@ func ConvertAwsAthenaInfoToConfig(aai AwsAthenaInfo) cloud.KeyedConfig {
 
 
 	var config cloud.KeyedConfig
 	var config cloud.KeyedConfig
 	if aai.AthenaTable != "" || aai.AthenaDatabase != "" {
 	if aai.AthenaTable != "" || aai.AthenaDatabase != "" {
-		// Use CURVersion from config if specified, otherwise default to 2.0
-		curVersion := aai.CURVersion
-		if curVersion == "" {
-			curVersion = "2.0"
-		}
 		config = &AthenaConfiguration{
 		config = &AthenaConfiguration{
 			Bucket:     aai.AthenaBucketName,
 			Bucket:     aai.AthenaBucketName,
 			Region:     aai.AthenaRegion,
 			Region:     aai.AthenaRegion,
@@ -257,7 +229,6 @@ func ConvertAwsAthenaInfoToConfig(aai AwsAthenaInfo) cloud.KeyedConfig {
 			Workgroup:  aai.AthenaWorkgroup,
 			Workgroup:  aai.AthenaWorkgroup,
 			Account:    aai.AccountID,
 			Account:    aai.AccountID,
 			Authorizer: authorizer,
 			Authorizer: authorizer,
-			CURVersion: curVersion,
 		}
 		}
 	} else {
 	} else {
 		config = &S3Configuration{
 		config = &S3Configuration{

+ 1 - 159
pkg/cloud/aws/athenaconfiguration_test.go

@@ -161,62 +161,6 @@ func TestAthenaConfiguration_Validate(t *testing.T) {
 			},
 			},
 			expected: fmt.Errorf("AthenaConfiguration: missing account"),
 			expected: fmt.Errorf("AthenaConfiguration: missing account"),
 		},
 		},
-		"valid CUR version 1.0": {
-			config: AthenaConfiguration{
-				Bucket:     "bucket",
-				Region:     "region",
-				Database:   "database",
-				Catalog:    "catalog",
-				Table:      "table",
-				Workgroup:  "workgroup",
-				Account:    "account",
-				Authorizer: &ServiceAccount{},
-				CURVersion: "1.0",
-			},
-			expected: nil,
-		},
-		"valid CUR version 2.0": {
-			config: AthenaConfiguration{
-				Bucket:     "bucket",
-				Region:     "region",
-				Database:   "database",
-				Catalog:    "catalog",
-				Table:      "table",
-				Workgroup:  "workgroup",
-				Account:    "account",
-				Authorizer: &ServiceAccount{},
-				CURVersion: "2.0",
-			},
-			expected: nil,
-		},
-		"valid empty CUR version defaults to 2.0": {
-			config: AthenaConfiguration{
-				Bucket:     "bucket",
-				Region:     "region",
-				Database:   "database",
-				Catalog:    "catalog",
-				Table:      "table",
-				Workgroup:  "workgroup",
-				Account:    "account",
-				Authorizer: &ServiceAccount{},
-				CURVersion: "",
-			},
-			expected: nil,
-		},
-		"invalid CUR version": {
-			config: AthenaConfiguration{
-				Bucket:     "bucket",
-				Region:     "region",
-				Database:   "database",
-				Catalog:    "catalog",
-				Table:      "table",
-				Workgroup:  "workgroup",
-				Account:    "account",
-				Authorizer: &ServiceAccount{},
-				CURVersion: "3.0",
-			},
-			expected: fmt.Errorf("AthenaConfiguration: invalid CURVersion '3.0', must be '1.0' or '2.0'"),
-		},
 	}
 	}
 
 
 	for name, testCase := range testCases {
 	for name, testCase := range testCases {
@@ -571,68 +515,6 @@ func TestAthenaConfiguration_Equals(t *testing.T) {
 			},
 			},
 			expected: false,
 			expected: false,
 		},
 		},
-		"different CUR version": {
-			left: AthenaConfiguration{
-				Bucket:    "bucket",
-				Region:    "region",
-				Database:  "database",
-				Catalog:   "catalog",
-				Table:     "table",
-				Workgroup: "workgroup",
-				Account:   "account",
-				Authorizer: &AccessKey{
-					ID:     "id",
-					Secret: "secret",
-				},
-				CURVersion: "1.0",
-			},
-			right: &AthenaConfiguration{
-				Bucket:    "bucket",
-				Region:    "region",
-				Database:  "database",
-				Catalog:   "catalog",
-				Table:     "table",
-				Workgroup: "workgroup",
-				Account:   "account",
-				Authorizer: &AccessKey{
-					ID:     "id",
-					Secret: "secret",
-				},
-				CURVersion: "2.0",
-			},
-			expected: false,
-		},
-		"matching CUR version": {
-			left: AthenaConfiguration{
-				Bucket:    "bucket",
-				Region:    "region",
-				Database:  "database",
-				Catalog:   "catalog",
-				Table:     "table",
-				Workgroup: "workgroup",
-				Account:   "account",
-				Authorizer: &AccessKey{
-					ID:     "id",
-					Secret: "secret",
-				},
-				CURVersion: "1.0",
-			},
-			right: &AthenaConfiguration{
-				Bucket:    "bucket",
-				Region:    "region",
-				Database:  "database",
-				Catalog:   "catalog",
-				Table:     "table",
-				Workgroup: "workgroup",
-				Account:   "account",
-				Authorizer: &AccessKey{
-					ID:     "id",
-					Secret: "secret",
-				},
-				CURVersion: "1.0",
-			},
-			expected: true,
-		},
 		"different config": {
 		"different config": {
 			left: AthenaConfiguration{
 			left: AthenaConfiguration{
 				Bucket:    "bucket",
 				Bucket:    "bucket",
@@ -669,9 +551,7 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 		config AthenaConfiguration
 		config AthenaConfiguration
 	}{
 	}{
 		"Empty Config": {
 		"Empty Config": {
-			config: AthenaConfiguration{
-				CURVersion: "2.0", // Default value after JSON unmarshal
-			},
+			config: AthenaConfiguration{},
 		},
 		},
 		"AccessKey": {
 		"AccessKey": {
 			config: AthenaConfiguration{
 			config: AthenaConfiguration{
@@ -686,7 +566,6 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 					ID:     "id",
 					ID:     "id",
 					Secret: "secret",
 					Secret: "secret",
 				},
 				},
-				CURVersion: "2.0", // Default value after JSON unmarshal
 			},
 			},
 		},
 		},
 
 
@@ -700,7 +579,6 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 				Workgroup:  "workgroup",
 				Workgroup:  "workgroup",
 				Account:    "account",
 				Account:    "account",
 				Authorizer: &ServiceAccount{},
 				Authorizer: &ServiceAccount{},
-				CURVersion: "2.0", // Default value after JSON unmarshal
 			},
 			},
 		},
 		},
 		"AssumeRole with AccessKey": {
 		"AssumeRole with AccessKey": {
@@ -719,7 +597,6 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 					},
 					},
 					RoleARN: "12345",
 					RoleARN: "12345",
 				},
 				},
-				CURVersion: "2.0", // Default value after JSON unmarshal
 			},
 			},
 		},
 		},
 		"AssumeRole with ServiceAccount": {
 		"AssumeRole with ServiceAccount": {
@@ -735,7 +612,6 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 					Authorizer: &ServiceAccount{},
 					Authorizer: &ServiceAccount{},
 					RoleARN:    "12345",
 					RoleARN:    "12345",
 				},
 				},
-				CURVersion: "2.0", // Default value after JSON unmarshal
 			},
 			},
 		},
 		},
 		"RoleArnNil": {
 		"RoleArnNil": {
@@ -751,7 +627,6 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 					Authorizer: nil,
 					Authorizer: nil,
 					RoleARN:    "12345",
 					RoleARN:    "12345",
 				},
 				},
-				CURVersion: "2.0", // Default value after JSON unmarshal
 			},
 			},
 		},
 		},
 		"AssumeRole with AssumeRole with ServiceAccount": {
 		"AssumeRole with AssumeRole with ServiceAccount": {
@@ -770,39 +645,6 @@ func TestAthenaConfiguration_JSON(t *testing.T) {
 					},
 					},
 					RoleARN: "12345",
 					RoleARN: "12345",
 				},
 				},
-				CURVersion: "2.0", // Default value after JSON unmarshal
-			},
-		},
-		"CUR Version 1.0": {
-			config: AthenaConfiguration{
-				Bucket:    "bucket",
-				Region:    "region",
-				Database:  "database",
-				Catalog:   "catalog",
-				Table:     "table",
-				Workgroup: "workgroup",
-				Account:   "account",
-				Authorizer: &AccessKey{
-					ID:     "id",
-					Secret: "secret",
-				},
-				CURVersion: "1.0",
-			},
-		},
-		"CUR Version 2.0": {
-			config: AthenaConfiguration{
-				Bucket:    "bucket",
-				Region:    "region",
-				Database:  "database",
-				Catalog:   "catalog",
-				Table:     "table",
-				Workgroup: "workgroup",
-				Account:   "account",
-				Authorizer: &AccessKey{
-					ID:     "id",
-					Secret: "secret",
-				},
-				CURVersion: "2.0",
 			},
 			},
 		},
 		},
 	}
 	}

+ 72 - 28
pkg/cloud/aws/athenaintegration.go

@@ -10,6 +10,7 @@ import (
 	"github.com/aws/aws-sdk-go-v2/service/athena/types"
 	"github.com/aws/aws-sdk-go-v2/service/athena/types"
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/util/json"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/cloud"
 )
 )
@@ -17,6 +18,12 @@ import (
 const LabelColumnPrefix = "resource_tags_user_"
 const LabelColumnPrefix = "resource_tags_user_"
 const AWSLabelColumnPrefix = "resource_tags_aws_"
 const AWSLabelColumnPrefix = "resource_tags_aws_"
 const AthenaResourceTagPrefix = "resource_tags_"
 const AthenaResourceTagPrefix = "resource_tags_"
+const AthenaResourceTagsColumn = "resource_tags"
+
+const AthenaResourceTagsCastToJsonColumn = "CAST(resource_tags AS JSON) as resource_tags"
+
+const AthenaInvoiceEntityNameColumn = "bill_payer_account_name"
+const AthenaAccountNameColumn = "line_item_usage_account_name"
 
 
 // athenaDateLayout is the default AWS date format
 // athenaDateLayout is the default AWS date format
 const AthenaDateLayout = "2006-01-02 15:04:05.000"
 const AthenaDateLayout = "2006-01-02 15:04:05.000"
@@ -128,6 +135,18 @@ func (ai *AthenaIntegration) getCloudCost(start, end time.Time, limit int) (*ope
 			aqi.AWSTagColumns = append(aqi.AWSTagColumns, column)
 			aqi.AWSTagColumns = append(aqi.AWSTagColumns, column)
 		}
 		}
 	}
 	}
+
+	// CUR 2.0 specific columns, CUR 2.0 has ability to disable any column, so we check for any of these columns before querying
+	if allColumns[AthenaResourceTagsColumn] {
+		groupByColumns = append(groupByColumns, AthenaResourceTagsCastToJsonColumn)
+	}
+	if allColumns[AthenaAccountNameColumn] {
+		groupByColumns = append(groupByColumns, AthenaAccountNameColumn)
+	}
+	if allColumns[AthenaInvoiceEntityNameColumn] {
+		groupByColumns = append(groupByColumns, AthenaInvoiceEntityNameColumn)
+	}
+
 	var selectColumns []string
 	var selectColumns []string
 
 
 	// Duplicate GroupBy Columns into select columns
 	// Duplicate GroupBy Columns into select columns
@@ -162,7 +181,7 @@ func (ai *AthenaIntegration) getCloudCost(start, end time.Time, limit int) (*ope
 		aqi.ColumnIndexes[column] = i
 		aqi.ColumnIndexes[column] = i
 	}
 	}
 	whereDate := fmt.Sprintf(AthenaWhereDateFmt, start.Format("2006-01-02"), end.Format("2006-01-02"))
 	whereDate := fmt.Sprintf(AthenaWhereDateFmt, start.Format("2006-01-02"), end.Format("2006-01-02"))
-	wherePartitions := ai.GetPartitionWhere(start, end)
+	wherePartitions := ai.GetPartitionWhere(start, end, isCUR20(allColumns))
 
 
 	// Query for all line items with a resource_id or from AWS Marketplace, which did not end before
 	// Query for all line items with a resource_id or from AWS Marketplace, which did not end before
 	// the range or start after it. This captures all costs with any amount of
 	// the range or start after it. This captures all costs with any amount of
@@ -323,11 +342,9 @@ func (ai *AthenaIntegration) ConvertLabelToAWSTag(label string) string {
 
 
 // GetIsKubernetesColumn builds a column that determines if a row represents kubernetes spend
 // GetIsKubernetesColumn builds a column that determines if a row represents kubernetes spend
 func (ai *AthenaIntegration) GetIsKubernetesColumn(allColumns map[string]bool) string {
 func (ai *AthenaIntegration) GetIsKubernetesColumn(allColumns map[string]bool) string {
-	disjuncts := []string{
-		"line_item_product_code = 'AmazonEKS'", // EKS is always kubernetes
-	}
 	// tagColumns is a list of columns where the presence of a value indicates that a resource is part of a kubernetes cluster
 	// tagColumns is a list of columns where the presence of a value indicates that a resource is part of a kubernetes cluster
-	tagColumns := []string{
+	// Known columns hardcoded for CUR 1.0 and CUR 2.0
+	tagColumnsIsK8sCUR10 := []string{
 		"resource_tags_aws_eks_cluster_name",
 		"resource_tags_aws_eks_cluster_name",
 		"resource_tags_user_eks_cluster_name",
 		"resource_tags_user_eks_cluster_name",
 		"resource_tags_user_alpha_eksctl_io_cluster_name",
 		"resource_tags_user_alpha_eksctl_io_cluster_name",
@@ -335,43 +352,49 @@ func (ai *AthenaIntegration) GetIsKubernetesColumn(allColumns map[string]bool) s
 		"resource_tags_user_kubernetes_io_created_for_pvc_name",
 		"resource_tags_user_kubernetes_io_created_for_pvc_name",
 		"resource_tags_user_kubernetes_io_created_for_pv_name",
 		"resource_tags_user_kubernetes_io_created_for_pv_name",
 	}
 	}
+	tagColumnsIsK8sCUR20 := []string{
+		"resource_tags['aws_eks_cluster_name']",
+		"resource_tags['user_eks_cluster_name']",
+		"resource_tags['user_alpha_eksctl_io_cluster_name']",
+		"resource_tags['user_kubernetes_io_service_name']",
+		"resource_tags['user_kubernetes_io_created_for_pvc_name']",
+		"resource_tags['user_kubernetes_io_created_for_pv_name']",
+	}
 
 
-	for _, tagColumn := range tagColumns {
-		// if tag column is present in the CUR check for it
-		if _, ok := allColumns[tagColumn]; ok {
-			disjunctStr := fmt.Sprintf("%s <> ''", tagColumn)
+	disjuncts := []string{
+		"line_item_product_code = 'AmazonEKS'", // EKS is always kubernetes
+	}
+	if allColumns[AthenaResourceTagsColumn] {
+		// if resource tags column is present in the CUR check for IsKubernetes keys in the resource tags map
+		for _, tagColumn := range tagColumnsIsK8sCUR20 {
+			disjunctStr := fmt.Sprintf("COALESCE(%s, '') <> ''", tagColumn)
 			disjuncts = append(disjuncts, disjunctStr)
 			disjuncts = append(disjuncts, disjunctStr)
 		}
 		}
+	} else {
+		for _, tagColumn := range tagColumnsIsK8sCUR10 {
+			// if tag column is present in the CUR check for it
+			if _, ok := allColumns[tagColumn]; ok {
+				disjunctStr := fmt.Sprintf("%s <> ''", tagColumn)
+				disjuncts = append(disjuncts, disjunctStr)
+			}
+		}
 	}
 	}
 
 
 	return fmt.Sprintf("(%s) as is_kubernetes", strings.Join(disjuncts, " OR "))
 	return fmt.Sprintf("(%s) as is_kubernetes", strings.Join(disjuncts, " OR "))
 }
 }
 
 
-func (ai *AthenaIntegration) GetPartitionWhere(start, end time.Time) string {
+func (ai *AthenaIntegration) GetPartitionWhere(start, end time.Time, isCUR20 bool) string {
 	month := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, time.UTC)
 	month := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, time.UTC)
 	endMonth := time.Date(end.Year(), end.Month(), 1, 0, 0, 0, 0, time.UTC)
 	endMonth := time.Date(end.Year(), end.Month(), 1, 0, 0, 0, 0, time.UTC)
 	var disjuncts []string
 	var disjuncts []string
 
 
-	// For CUR 2.0, check if billing_period partitions actually exist
-	useBillingPeriodPartitions := false
-	if ai.CURVersion != "1.0" {
-		// Check if billing_period partitions exist in the table
-		if hasBillingPeriod, err := ai.HasBillingPeriodPartitions(); err == nil && hasBillingPeriod {
-			useBillingPeriodPartitions = true
-		}
-	}
-
 	for !month.After(endMonth) {
 	for !month.After(endMonth) {
-		if ai.CURVersion == "1.0" {
-			// CUR 1.0 uses year and month columns for partitioning
-			disjuncts = append(disjuncts, fmt.Sprintf("(year = '%d' AND month = '%d')", month.Year(), month.Month()))
-		} else if useBillingPeriodPartitions {
+		if isCUR20 {
 			// CUR 2.0 with billing_period partitions
 			// CUR 2.0 with billing_period partitions
 			disjuncts = append(disjuncts, fmt.Sprintf("(billing_period = '%d-%02d')", month.Year(), month.Month()))
 			disjuncts = append(disjuncts, fmt.Sprintf("(billing_period = '%d-%02d')", month.Year(), month.Month()))
 		} else {
 		} else {
-			// CUR 2.0 fallback - use date_format functions (less efficient but works without partitions)
-			disjuncts = append(disjuncts, fmt.Sprintf("(date_format(line_item_usage_start_date, '%%Y') = '%d' AND date_format(line_item_usage_start_date, '%%m') = '%02d')",
-				month.Year(), month.Month()))
+			// CUR 1.0 uses year and month columns for partitioning
+			disjuncts = append(disjuncts, fmt.Sprintf("(year = '%d' AND month = '%d')", month.Year(), month.Month()))
 		}
 		}
 		month = month.AddDate(0, 1, 0)
 		month = month.AddDate(0, 1, 0)
 	}
 	}
@@ -408,8 +431,24 @@ func athenaRowToCloudCost(row types.Row, aqi AthenaQueryIndexes) (*opencost.Clou
 		}
 		}
 	}
 	}
 
 
+	if _, ok := aqi.ColumnIndexes[AthenaResourceTagsCastToJsonColumn]; ok {
+		resourceTags := GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaResourceTagsCastToJsonColumn)
+		err := json.Unmarshal([]byte(resourceTags), &labels)
+		if err != nil {
+			log.Errorf("athenaRowToCloudCost: error unmarshalling resource tags: %s", err.Error())
+		}
+	}
+
 	invoiceEntityID := GetAthenaRowValue(row, aqi.ColumnIndexes, "bill_payer_account_id")
 	invoiceEntityID := GetAthenaRowValue(row, aqi.ColumnIndexes, "bill_payer_account_id")
 	accountID := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_usage_account_id")
 	accountID := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_usage_account_id")
+	invoiceEntityName := invoiceEntityID
+	accountName := accountID
+	if _, ok := aqi.ColumnIndexes[AthenaInvoiceEntityNameColumn]; ok {
+		invoiceEntityName = GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaInvoiceEntityNameColumn)
+	}
+	if _, ok := aqi.ColumnIndexes[AthenaAccountNameColumn]; ok {
+		accountName = GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaAccountNameColumn)
+	}
 	startStr := GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaDateTruncColumn)
 	startStr := GetAthenaRowValue(row, aqi.ColumnIndexes, AthenaDateTruncColumn)
 	providerID := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_resource_id")
 	providerID := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_resource_id")
 	productCode := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_product_code")
 	productCode := GetAthenaRowValue(row, aqi.ColumnIndexes, "line_item_product_code")
@@ -462,9 +501,9 @@ func athenaRowToCloudCost(row types.Row, aqi AthenaQueryIndexes) (*opencost.Clou
 		ProviderID:        providerID,
 		ProviderID:        providerID,
 		Provider:          opencost.AWSProvider,
 		Provider:          opencost.AWSProvider,
 		AccountID:         accountID,
 		AccountID:         accountID,
-		AccountName:       accountID,
+		AccountName:       accountName,
 		InvoiceEntityID:   invoiceEntityID,
 		InvoiceEntityID:   invoiceEntityID,
-		InvoiceEntityName: invoiceEntityID,
+		InvoiceEntityName: invoiceEntityName,
 		RegionID:          regionCode,
 		RegionID:          regionCode,
 		AvailabilityZone:  availabilityZone,
 		AvailabilityZone:  availabilityZone,
 		Service:           productCode,
 		Service:           productCode,
@@ -512,3 +551,8 @@ func (ai *AthenaIntegration) GetConnectionStatusFromResult(result cloud.EmptyChe
 	}
 	}
 	return cloud.SuccessfulConnection
 	return cloud.SuccessfulConnection
 }
 }
+
+// presence of any of resource_tags, line_item_usage_account_name, or bill_payer_account_name columns confirms CUR 2.0
+func isCUR20(allColumns map[string]bool) bool {
+	return allColumns[AthenaResourceTagsColumn] || allColumns[AthenaAccountNameColumn] || allColumns[AthenaInvoiceEntityNameColumn]
+}

+ 20 - 13
pkg/cloud/aws/athenaintegration_coverage_test.go

@@ -3,7 +3,6 @@ package aws
 import (
 import (
 	"strings"
 	"strings"
 	"testing"
 	"testing"
-	"time"
 
 
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/cloud"
 )
 )
@@ -185,15 +184,15 @@ func TestAthenaIntegration_ConvertLabelToAWSTag(t *testing.T) {
 func TestAthenaIntegration_GetIsKubernetesColumn(t *testing.T) {
 func TestAthenaIntegration_GetIsKubernetesColumn(t *testing.T) {
 	ai := &AthenaIntegration{}
 	ai := &AthenaIntegration{}
 
 
-	// Test with some tag columns present
-	allColumns := map[string]bool{
+	// Test with some tag columns present CUR 1.0
+	allColumnsCur10 := map[string]bool{
 		"resource_tags_user_eks_cluster_name":             true,
 		"resource_tags_user_eks_cluster_name":             true,
 		"resource_tags_user_alpha_eksctl_io_cluster_name": true,
 		"resource_tags_user_alpha_eksctl_io_cluster_name": true,
 		"resource_tags_user_kubernetes_io_service_name":   true,
 		"resource_tags_user_kubernetes_io_service_name":   true,
 		"some_other_column":                               true,
 		"some_other_column":                               true,
 	}
 	}
 
 
-	result := ai.GetIsKubernetesColumn(allColumns)
+	result := ai.GetIsKubernetesColumn(allColumnsCur10)
 	if !strings.Contains(result, "line_item_product_code = 'AmazonEKS'") {
 	if !strings.Contains(result, "line_item_product_code = 'AmazonEKS'") {
 		t.Errorf("GetIsKubernetesColumn() should always include EKS check, got: %v", result)
 		t.Errorf("GetIsKubernetesColumn() should always include EKS check, got: %v", result)
 	}
 	}
@@ -203,6 +202,23 @@ func TestAthenaIntegration_GetIsKubernetesColumn(t *testing.T) {
 	if !strings.Contains(result, " as is_kubernetes") {
 	if !strings.Contains(result, " as is_kubernetes") {
 		t.Errorf("GetIsKubernetesColumn() should alias result as is_kubernetes, got: %v", result)
 		t.Errorf("GetIsKubernetesColumn() should alias result as is_kubernetes, got: %v", result)
 	}
 	}
+
+	// Test with some tag columns present CUR 2.0
+	allColumnsCur20 := map[string]bool{
+		"resource_tags":     true,
+		"some_other_column": true,
+	}
+
+	result = ai.GetIsKubernetesColumn(allColumnsCur20)
+	if !strings.Contains(result, "line_item_product_code = 'AmazonEKS'") {
+		t.Errorf("GetIsKubernetesColumn() should always include EKS check, got: %v", result)
+	}
+	if !strings.Contains(result, "COALESCE(resource_tags['user_eks_cluster_name'], '') <> ''") {
+		t.Errorf("GetIsKubernetesColumn() should include checks for tag columns, got: %v", result)
+	}
+	if !strings.Contains(result, " as is_kubernetes") {
+		t.Errorf("GetIsKubernetesColumn() should alias result as is_kubernetes, got: %v", result)
+	}
 }
 }
 
 
 func TestAthenaQuerier_GetStatus(t *testing.T) {
 func TestAthenaQuerier_GetStatus(t *testing.T) {
@@ -284,12 +300,3 @@ func TestAthenaQuerier_Equals(t *testing.T) {
 		t.Errorf("Equals() should return false when comparing with different type")
 		t.Errorf("Equals() should return false when comparing with different type")
 	}
 	}
 }
 }
-
-// Helper function for parsing time in tests
-func mustParseTime(value string) time.Time {
-	t, err := time.Parse(time.RFC3339, value)
-	if err != nil {
-		panic(err)
-	}
-	return t
-}

+ 156 - 182
pkg/cloud/aws/athenaintegration_test.go

@@ -1,10 +1,8 @@
 package aws
 package aws
 
 
 import (
 import (
-	"fmt"
 	"os"
 	"os"
 	"reflect"
 	"reflect"
-	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -70,7 +68,7 @@ func TestAthenaIntegration_GetCloudCost(t *testing.T) {
 }
 }
 
 
 func Test_athenaRowToCloudCost(t *testing.T) {
 func Test_athenaRowToCloudCost(t *testing.T) {
-	aqi := AthenaQueryIndexes{
+	aqiCur10 := AthenaQueryIndexes{
 		ColumnIndexes: map[string]int{
 		ColumnIndexes: map[string]int{
 			"ListCostColumn":              0,
 			"ListCostColumn":              0,
 			"NetCostColumn":               1,
 			"NetCostColumn":               1,
@@ -97,6 +95,32 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 		IsK8sColumn:            "IsK8sColumn",
 		IsK8sColumn:            "IsK8sColumn",
 	}
 	}
 
 
+	aqiCur20 := AthenaQueryIndexes{
+		ColumnIndexes: map[string]int{
+			"ListCostColumn":                   0,
+			"NetCostColumn":                    1,
+			"AmortizedNetCostColumn":           2,
+			"AmortizedCostColumn":              3,
+			"IsK8sColumn":                      4,
+			AthenaDateTruncColumn:              5,
+			"line_item_resource_id":            6,
+			"bill_payer_account_id":            7,
+			"line_item_usage_account_id":       8,
+			"line_item_product_code":           9,
+			"line_item_usage_type":             10,
+			"product_region_code":              11,
+			"line_item_availability_zone":      12,
+			AthenaResourceTagsCastToJsonColumn: 13,
+		},
+		TagColumns:             []string{},
+		AWSTagColumns:          []string{},
+		ListCostColumn:         "ListCostColumn",
+		NetCostColumn:          "NetCostColumn",
+		AmortizedNetCostColumn: "AmortizedNetCostColumn",
+		AmortizedCostColumn:    "AmortizedCostColumn",
+		IsK8sColumn:            "IsK8sColumn",
+	}
+
 	tests := []struct {
 	tests := []struct {
 		name    string
 		name    string
 		row     []string
 		row     []string
@@ -105,51 +129,51 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 		wantErr bool
 		wantErr bool
 	}{
 	}{
 		{
 		{
-			name:    "incorrect row length",
+			name:    "incorrect row length CUR 1.0",
 			row:     []string{"not enough elements"},
 			row:     []string{"not enough elements"},
-			aqi:     aqi,
+			aqi:     aqiCur10,
 			want:    nil,
 			want:    nil,
 			wantErr: true,
 			wantErr: true,
 		},
 		},
 		{
 		{
-			name:    "invalid list cost",
+			name:    "invalid list cost CUR 1.0",
 			row:     []string{"invalid", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
 			row:     []string{"invalid", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
-			aqi:     aqi,
+			aqi:     aqiCur10,
 			want:    nil,
 			want:    nil,
 			wantErr: true,
 			wantErr: true,
 		},
 		},
 		{
 		{
-			name:    "invalid net cost",
+			name:    "invalid net cost CUR 1.0",
 			row:     []string{"1", "invalid", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
 			row:     []string{"1", "invalid", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
-			aqi:     aqi,
+			aqi:     aqiCur10,
 			want:    nil,
 			want:    nil,
 			wantErr: true,
 			wantErr: true,
 		},
 		},
 		{
 		{
-			name:    "invalid amortized net cost",
+			name:    "invalid amortized net cost CUR 1.0",
 			row:     []string{"1", "2", "invalid", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
 			row:     []string{"1", "2", "invalid", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
-			aqi:     aqi,
+			aqi:     aqiCur10,
 			want:    nil,
 			want:    nil,
 			wantErr: true,
 			wantErr: true,
 		},
 		},
 		{
 		{
-			name:    "invalid amortized cost",
+			name:    "invalid amortized cost CUR 1.0",
 			row:     []string{"1", "2", "3", "invalid", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
 			row:     []string{"1", "2", "3", "invalid", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
-			aqi:     aqi,
+			aqi:     aqiCur10,
 			want:    nil,
 			want:    nil,
 			wantErr: true,
 			wantErr: true,
 		},
 		},
 		{
 		{
-			name:    "invalid date",
+			name:    "invalid date CUR 1.0",
 			row:     []string{"1", "2", "3", "4", "true", "invalid", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
 			row:     []string{"1", "2", "3", "4", "true", "invalid", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
-			aqi:     aqi,
+			aqi:     aqiCur10,
 			want:    nil,
 			want:    nil,
 			wantErr: true,
 			wantErr: true,
 		},
 		},
 		{
 		{
-			name: "valid kubernetes with labels",
+			name: "valid kubernetes with labels CUR 1.0",
 			row:  []string{"1", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
 			row:  []string{"1", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
-			aqi:  aqi,
+			aqi:  aqiCur10,
 			want: &opencost.CloudCost{
 			want: &opencost.CloudCost{
 				Properties: &opencost.CloudCostProperties{
 				Properties: &opencost.CloudCostProperties{
 					ProviderID:        "resourceID",
 					ProviderID:        "resourceID",
@@ -197,7 +221,7 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 		{
 		{
 			name: "valid non-kubernetes, no labels",
 			name: "valid non-kubernetes, no labels",
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "", ""},
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "", ""},
-			aqi:  aqi,
+			aqi:  aqiCur10,
 			want: &opencost.CloudCost{
 			want: &opencost.CloudCost{
 				Properties: &opencost.CloudCostProperties{
 				Properties: &opencost.CloudCostProperties{
 					ProviderID:        "resourceID",
 					ProviderID:        "resourceID",
@@ -240,9 +264,9 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 			wantErr: false,
 			wantErr: false,
 		},
 		},
 		{
 		{
-			name: "valid load balancer product code",
+			name: "valid load balancer product code CUR 1.0",
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID/lbID", "payerAccountID", "usageAccountID", "AWSELB", "usageType", "regionCode", "availabilityZone", "", ""},
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID/lbID", "payerAccountID", "usageAccountID", "AWSELB", "usageType", "regionCode", "availabilityZone", "", ""},
-			aqi:  aqi,
+			aqi:  aqiCur10,
 			want: &opencost.CloudCost{
 			want: &opencost.CloudCost{
 				Properties: &opencost.CloudCostProperties{
 				Properties: &opencost.CloudCostProperties{
 					ProviderID:        "lbID",
 					ProviderID:        "lbID",
@@ -285,9 +309,9 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 			wantErr: false,
 			wantErr: false,
 		},
 		},
 		{
 		{
-			name: "valid non-kubernetes, Fargate CPU",
+			name: "valid non-kubernetes, Fargate CPU CUR 1.0",
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "CPU", "regionCode", "availabilityZone", "", ""},
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "CPU", "regionCode", "availabilityZone", "", ""},
-			aqi:  aqi,
+			aqi:  aqiCur10,
 			want: &opencost.CloudCost{
 			want: &opencost.CloudCost{
 				Properties: &opencost.CloudCostProperties{
 				Properties: &opencost.CloudCostProperties{
 					ProviderID:        "123:pod/resource/CPU",
 					ProviderID:        "123:pod/resource/CPU",
@@ -330,9 +354,9 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 			wantErr: false,
 			wantErr: false,
 		},
 		},
 		{
 		{
-			name: "valid non-kubernetes, Fargate RAM",
+			name: "valid non-kubernetes, Fargate RAM CUR 1.0",
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "GB", "regionCode", "availabilityZone", "", ""},
 			row:  []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "GB", "regionCode", "availabilityZone", "", ""},
-			aqi:  aqi,
+			aqi:  aqiCur10,
 			want: &opencost.CloudCost{
 			want: &opencost.CloudCost{
 				Properties: &opencost.CloudCostProperties{
 				Properties: &opencost.CloudCostProperties{
 					ProviderID:        "123:pod/resource/RAM",
 					ProviderID:        "123:pod/resource/RAM",
@@ -374,6 +398,54 @@ func Test_athenaRowToCloudCost(t *testing.T) {
 			},
 			},
 			wantErr: false,
 			wantErr: false,
 		},
 		},
+		{
+			name: "valid kubernetes with labels CUR 2.0",
+			row:  []string{"1", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", `{"test": "userTagTestValue", "aws_test": "awsTagTestValue"}`},
+			aqi:  aqiCur20,
+			want: &opencost.CloudCost{
+				Properties: &opencost.CloudCostProperties{
+					ProviderID:        "resourceID",
+					Provider:          "AWS",
+					AccountID:         "usageAccountID",
+					AccountName:       "usageAccountID",
+					InvoiceEntityID:   "payerAccountID",
+					InvoiceEntityName: "payerAccountID",
+					RegionID:          "regionCode",
+					AvailabilityZone:  "availabilityZone",
+					Service:           "productCode",
+					Category:          opencost.OtherCategory,
+					Labels: opencost.CloudCostLabels{
+						"test":     "userTagTestValue",
+						"aws_test": "awsTagTestValue",
+					},
+				},
+				Window: opencost.NewClosedWindow(
+					time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
+					time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
+				),
+				ListCost: opencost.CostMetric{
+					Cost:              1,
+					KubernetesPercent: 1,
+				},
+				NetCost: opencost.CostMetric{
+					Cost:              2,
+					KubernetesPercent: 1,
+				},
+				AmortizedNetCost: opencost.CostMetric{
+					Cost:              3,
+					KubernetesPercent: 1,
+				},
+				InvoicedCost: opencost.CostMetric{
+					Cost:              2,
+					KubernetesPercent: 1,
+				},
+				AmortizedCost: opencost.CostMetric{
+					Cost:              4,
+					KubernetesPercent: 1,
+				},
+			},
+			wantErr: false,
+		},
 	}
 	}
 	for _, tt := range tests {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
@@ -399,58 +471,13 @@ func stringsToRow(strings []string) types.Row {
 	return types.Row{Data: data}
 	return types.Row{Data: data}
 }
 }
 
 
-// mockAthenaQuerier is a mock that overrides HasBillingPeriodPartitions for testing
-type mockAthenaQuerier struct {
-	AthenaQuerier
-	hasBillingPeriodPartitions bool
-}
-
-func (m *mockAthenaQuerier) HasBillingPeriodPartitions() (bool, error) {
-	return m.hasBillingPeriodPartitions, nil
-}
-
-// mockAthenaIntegration is a mock that uses mockAthenaQuerier
-type mockAthenaIntegration struct {
-	*mockAthenaQuerier
-}
-
-func (m *mockAthenaIntegration) GetPartitionWhere(start, end time.Time) string {
-	// The partition logic using our mock's HasBillingPeriodPartitions result
-	month := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, time.UTC)
-	endMonth := time.Date(end.Year(), end.Month(), 1, 0, 0, 0, 0, time.UTC)
-	var disjuncts []string
-
-	// Using our mock's result for billing period partitions
-	useBillingPeriodPartitions := false
-	if m.mockAthenaQuerier.AthenaConfiguration.CURVersion != "1.0" {
-		useBillingPeriodPartitions = m.mockAthenaQuerier.hasBillingPeriodPartitions
-	}
-
-	for !month.After(endMonth) {
-		if m.mockAthenaQuerier.AthenaConfiguration.CURVersion == "1.0" {
-			// CUR 1.0 uses year and month columns for partitioning
-			disjuncts = append(disjuncts, fmt.Sprintf("(year = '%d' AND month = '%d')", month.Year(), month.Month()))
-		} else if useBillingPeriodPartitions {
-			// CUR 2.0 with billing_period partitions
-			disjuncts = append(disjuncts, fmt.Sprintf("(billing_period = '%d-%02d')", month.Year(), month.Month()))
-		} else {
-			// CUR 2.0 fallback - use date_format functions
-			disjuncts = append(disjuncts, fmt.Sprintf("(date_format(line_item_usage_start_date, '%%Y') = '%d' AND date_format(line_item_usage_start_date, '%%m') = '%02d')",
-				month.Year(), month.Month()))
-		}
-		month = month.AddDate(0, 1, 0)
-	}
-	return fmt.Sprintf("(%s)", strings.Join(disjuncts, " OR "))
-}
-
 func TestAthenaIntegration_GetPartitionWhere(t *testing.T) {
 func TestAthenaIntegration_GetPartitionWhere(t *testing.T) {
 	testCases := map[string]struct {
 	testCases := map[string]struct {
-		integration interface {
-			GetPartitionWhere(time.Time, time.Time) string
-		}
-		start    time.Time
-		end      time.Time
-		expected string
+		integration        *AthenaIntegration
+		start              time.Time
+		end                time.Time
+		resourceTagsColumn bool
+		expected           string
 	}{
 	}{
 		"CUR 1.0 single month": {
 		"CUR 1.0 single month": {
 			integration: &AthenaIntegration{
 			integration: &AthenaIntegration{
@@ -463,35 +490,32 @@ func TestAthenaIntegration_GetPartitionWhere(t *testing.T) {
 						Workgroup:  "workgroup",
 						Workgroup:  "workgroup",
 						Account:    "account",
 						Account:    "account",
 						Authorizer: &ServiceAccount{},
 						Authorizer: &ServiceAccount{},
-						CURVersion: "1.0",
 					},
 					},
 				},
 				},
 			},
 			},
-			start:    time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
-			expected: "((year = '2024' AND month = '1'))",
+			start:              time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
+			end:                time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
+			resourceTagsColumn: false,
+			expected:           "((year = '2024' AND month = '1'))",
 		},
 		},
 		"CUR 2.0 single month": {
 		"CUR 2.0 single month": {
-			integration: &mockAthenaIntegration{
-				mockAthenaQuerier: &mockAthenaQuerier{
-					AthenaQuerier: AthenaQuerier{
-						AthenaConfiguration: AthenaConfiguration{
-							Bucket:     "bucket",
-							Region:     "region",
-							Database:   "database",
-							Table:      "table",
-							Workgroup:  "workgroup",
-							Account:    "account",
-							Authorizer: &ServiceAccount{},
-							CURVersion: "2.0",
-						},
+			integration: &AthenaIntegration{
+				AthenaQuerier: AthenaQuerier{
+					AthenaConfiguration: AthenaConfiguration{
+						Bucket:     "bucket",
+						Region:     "region",
+						Database:   "database",
+						Table:      "table",
+						Workgroup:  "workgroup",
+						Account:    "account",
+						Authorizer: &ServiceAccount{},
 					},
 					},
-					hasBillingPeriodPartitions: true,
 				},
 				},
 			},
 			},
-			start:    time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
-			expected: "((billing_period = '2024-01'))",
+			start:              time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
+			end:                time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
+			resourceTagsColumn: true,
+			expected:           "((billing_period = '2024-01'))",
 		},
 		},
 		"CUR 1.0 multiple months": {
 		"CUR 1.0 multiple months": {
 			integration: &AthenaIntegration{
 			integration: &AthenaIntegration{
@@ -504,57 +528,51 @@ func TestAthenaIntegration_GetPartitionWhere(t *testing.T) {
 						Workgroup:  "workgroup",
 						Workgroup:  "workgroup",
 						Account:    "account",
 						Account:    "account",
 						Authorizer: &ServiceAccount{},
 						Authorizer: &ServiceAccount{},
-						CURVersion: "1.0",
 					},
 					},
 				},
 				},
 			},
 			},
-			start:    time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC),
-			expected: "((year = '2024' AND month = '1') OR (year = '2024' AND month = '2') OR (year = '2024' AND month = '3'))",
+			start:              time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
+			end:                time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC),
+			resourceTagsColumn: false,
+			expected:           "((year = '2024' AND month = '1') OR (year = '2024' AND month = '2') OR (year = '2024' AND month = '3'))",
 		},
 		},
 		"CUR 2.0 multiple months": {
 		"CUR 2.0 multiple months": {
-			integration: &mockAthenaIntegration{
-				mockAthenaQuerier: &mockAthenaQuerier{
-					AthenaQuerier: AthenaQuerier{
-						AthenaConfiguration: AthenaConfiguration{
-							Bucket:     "bucket",
-							Region:     "region",
-							Database:   "database",
-							Table:      "table",
-							Workgroup:  "workgroup",
-							Account:    "account",
-							Authorizer: &ServiceAccount{},
-							CURVersion: "2.0",
-						},
+			integration: &AthenaIntegration{
+				AthenaQuerier: AthenaQuerier{
+					AthenaConfiguration: AthenaConfiguration{
+						Bucket:     "bucket",
+						Region:     "region",
+						Database:   "database",
+						Table:      "table",
+						Workgroup:  "workgroup",
+						Account:    "account",
+						Authorizer: &ServiceAccount{},
 					},
 					},
-					hasBillingPeriodPartitions: true,
 				},
 				},
 			},
 			},
-			start:    time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC),
-			expected: "((billing_period = '2024-01') OR (billing_period = '2024-02') OR (billing_period = '2024-03'))",
+			start:              time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
+			end:                time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC),
+			resourceTagsColumn: true,
+			expected:           "((billing_period = '2024-01') OR (billing_period = '2024-02') OR (billing_period = '2024-03'))",
 		},
 		},
 		"CUR 2.0 across year boundary": {
 		"CUR 2.0 across year boundary": {
-			integration: &mockAthenaIntegration{
-				mockAthenaQuerier: &mockAthenaQuerier{
-					AthenaQuerier: AthenaQuerier{
-						AthenaConfiguration: AthenaConfiguration{
-							Bucket:     "bucket",
-							Region:     "region",
-							Database:   "database",
-							Table:      "table",
-							Workgroup:  "workgroup",
-							Account:    "account",
-							Authorizer: &ServiceAccount{},
-							CURVersion: "2.0",
-						},
+			integration: &AthenaIntegration{
+				AthenaQuerier: AthenaQuerier{
+					AthenaConfiguration: AthenaConfiguration{
+						Bucket:     "bucket",
+						Region:     "region",
+						Database:   "database",
+						Table:      "table",
+						Workgroup:  "workgroup",
+						Account:    "account",
+						Authorizer: &ServiceAccount{},
 					},
 					},
-					hasBillingPeriodPartitions: true,
 				},
 				},
 			},
 			},
-			start:    time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC),
-			expected: "((billing_period = '2023-12') OR (billing_period = '2024-01') OR (billing_period = '2024-02'))",
+			start:              time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC),
+			end:                time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC),
+			resourceTagsColumn: true,
+			expected:           "((billing_period = '2023-12') OR (billing_period = '2024-01') OR (billing_period = '2024-02'))",
 		},
 		},
 		"CUR 1.0 across year boundary": {
 		"CUR 1.0 across year boundary": {
 			integration: &AthenaIntegration{
 			integration: &AthenaIntegration{
@@ -567,63 +585,19 @@ func TestAthenaIntegration_GetPartitionWhere(t *testing.T) {
 						Workgroup:  "workgroup",
 						Workgroup:  "workgroup",
 						Account:    "account",
 						Account:    "account",
 						Authorizer: &ServiceAccount{},
 						Authorizer: &ServiceAccount{},
-						CURVersion: "1.0",
-					},
-				},
-			},
-			start:    time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC),
-			expected: "((year = '2023' AND month = '12') OR (year = '2024' AND month = '1') OR (year = '2024' AND month = '2'))",
-		},
-		"Default CUR version (empty string defaults to 2.0)": {
-			integration: &mockAthenaIntegration{
-				mockAthenaQuerier: &mockAthenaQuerier{
-					AthenaQuerier: AthenaQuerier{
-						AthenaConfiguration: AthenaConfiguration{
-							Bucket:     "bucket",
-							Region:     "region",
-							Database:   "database",
-							Table:      "table",
-							Workgroup:  "workgroup",
-							Account:    "account",
-							Authorizer: &ServiceAccount{},
-							CURVersion: "",
-						},
-					},
-					hasBillingPeriodPartitions: true,
-				},
-			},
-			start:    time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
-			expected: "((billing_period = '2024-01'))",
-		},
-		"CUR 2.0 fallback when no billing_period partitions": {
-			integration: &mockAthenaIntegration{
-				mockAthenaQuerier: &mockAthenaQuerier{
-					AthenaQuerier: AthenaQuerier{
-						AthenaConfiguration: AthenaConfiguration{
-							Bucket:     "bucket",
-							Region:     "region",
-							Database:   "database",
-							Table:      "table",
-							Workgroup:  "workgroup",
-							Account:    "account",
-							Authorizer: &ServiceAccount{},
-							CURVersion: "2.0",
-						},
 					},
 					},
-					hasBillingPeriodPartitions: false, // No billing_period partitions
 				},
 				},
 			},
 			},
-			start:    time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
-			end:      time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
-			expected: "((date_format(line_item_usage_start_date, '%Y') = '2024' AND date_format(line_item_usage_start_date, '%m') = '01'))",
+			start:              time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC),
+			end:                time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC),
+			resourceTagsColumn: false,
+			expected:           "((year = '2023' AND month = '12') OR (year = '2024' AND month = '1') OR (year = '2024' AND month = '2'))",
 		},
 		},
 	}
 	}
 
 
 	for name, testCase := range testCases {
 	for name, testCase := range testCases {
 		t.Run(name, func(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
-			actual := testCase.integration.GetPartitionWhere(testCase.start, testCase.end)
+			actual := testCase.integration.GetPartitionWhere(testCase.start, testCase.end, testCase.resourceTagsColumn)
 			if actual != testCase.expected {
 			if actual != testCase.expected {
 				t.Errorf("GetPartitionWhere() mismatch:\nActual:   %s\nExpected: %s", actual, testCase.expected)
 				t.Errorf("GetPartitionWhere() mismatch:\nActual:   %s\nExpected: %s", actual, testCase.expected)
 			}
 			}

+ 0 - 26
pkg/cloud/aws/athenaquerier.go

@@ -63,32 +63,6 @@ func (aq *AthenaQuerier) GetColumns() (map[string]bool, error) {
 	return columnSet, nil
 	return columnSet, nil
 }
 }
 
 
-// HasBillingPeriodPartitions checks if the table uses billing_period partitioning
-// by querying SHOW PARTITIONS and looking for billing_period partition keys
-func (aq *AthenaQuerier) HasBillingPeriodPartitions() (bool, error) {
-	// Use SHOW PARTITIONS to check if billing_period partitions exist
-	query := fmt.Sprintf("SHOW PARTITIONS \"%s\"", aq.Table)
-	hasBillingPeriodPartition := false
-
-	athenaErr := aq.Query(context.TODO(), query, GetAthenaQueryFunc(func(row types.Row) {
-		if len(row.Data) > 0 && row.Data[0].VarCharValue != nil {
-			partitionValue := *row.Data[0].VarCharValue
-			// Check if partition follows billing_period=YYYY-MM format
-			if strings.HasPrefix(partitionValue, "billing_period=") {
-				hasBillingPeriodPartition = true
-			}
-		}
-	}))
-
-	if athenaErr != nil {
-		// If SHOW PARTITIONS fails, assume no billing_period partitions
-		log.Debugf("AthenaQuerier[%s]: SHOW PARTITIONS failed: %s", aq.Key(), athenaErr.Error())
-		return false, athenaErr
-	}
-
-	return hasBillingPeriodPartition, nil
-}
-
 func (aq *AthenaQuerier) Query(ctx context.Context, query string, fn func(*athena.GetQueryResultsOutput) bool) error {
 func (aq *AthenaQuerier) Query(ctx context.Context, query string, fn func(*athena.GetQueryResultsOutput) bool) error {
 	err := aq.Validate()
 	err := aq.Validate()
 	if err != nil {
 	if err != nil {

+ 22 - 7
pkg/cloud/aws/provider.go

@@ -412,7 +412,6 @@ type AwsAthenaInfo struct {
 	ServiceKeySecret string `json:"serviceKeySecret"`
 	ServiceKeySecret string `json:"serviceKeySecret"`
 	AccountID        string `json:"projectID"`
 	AccountID        string `json:"projectID"`
 	MasterPayerARN   string `json:"masterPayerARN"`
 	MasterPayerARN   string `json:"masterPayerARN"`
-	CURVersion       string `json:"curVersion"` // "1.0" or "2.0", defaults to "2.0" if not specified
 }
 }
 
 
 // IsEmpty returns true if all fields in config are empty, false if not.
 // IsEmpty returns true if all fields in config are empty, false if not.
@@ -525,7 +524,6 @@ func (aws *AWS) GetAWSAthenaInfo() (*AwsAthenaInfo, error) {
 		ServiceKeySecret: aak.SecretAccessKey,
 		ServiceKeySecret: aak.SecretAccessKey,
 		AccountID:        config.AthenaProjectID,
 		AccountID:        config.AthenaProjectID,
 		MasterPayerARN:   config.MasterPayerARN,
 		MasterPayerARN:   config.MasterPayerARN,
-		CURVersion:       config.AthenaCURVersion,
 	}, nil
 	}, nil
 }
 }
 
 
@@ -575,9 +573,6 @@ func configUpdaterWithReaderAndType(r io.Reader, updateType string) func(c *mode
 				c.MasterPayerARN = aai.MasterPayerARN
 				c.MasterPayerARN = aai.MasterPayerARN
 			}
 			}
 			c.AthenaProjectID = aai.AccountID
 			c.AthenaProjectID = aai.AccountID
-			if aai.CURVersion != "" {
-				c.AthenaCURVersion = aai.CURVersion
-			}
 		default:
 		default:
 			a := make(map[string]any)
 			a := make(map[string]any)
 			err := json.NewDecoder(r).Decode(&a)
 			err := json.NewDecoder(r).Decode(&a)
@@ -855,8 +850,28 @@ func (aws *AWS) getRegionPricing(nodeList []*clustercache.Node) (*http.Response,
 
 
 // SpotRefreshEnabled determines whether the required configs to run the spot feed query have been set up
 // SpotRefreshEnabled determines whether the required configs to run the spot feed query have been set up
 func (aws *AWS) SpotRefreshEnabled() bool {
 func (aws *AWS) SpotRefreshEnabled() bool {
-	// Need a valid value for at least one of these fields to consider spot pricing as enabled
-	return len(aws.SpotDataBucket) != 0 || len(aws.SpotDataRegion) != 0 || len(aws.ProjectID) != 0
+	// Guard against nil receiver
+	if aws == nil {
+		return false
+	}
+
+	// Fallback if config is not initialized
+	if aws.Config == nil {
+		return len(aws.SpotDataBucket) != 0 ||
+			len(aws.SpotDataRegion) != 0 ||
+			len(aws.ProjectID) != 0
+	}
+
+	// Check if spot data feed is explicitly disabled via config
+	c, err := aws.Config.GetCustomPricingData()
+	if err == nil && c.SpotDataFeedEnabled == "false" {
+		return false
+	}
+
+	// Default behavior
+	return len(aws.SpotDataBucket) != 0 ||
+		len(aws.SpotDataRegion) != 0 ||
+		len(aws.ProjectID) != 0
 }
 }
 
 
 // DownloadPricingData fetches data from the AWS Pricing API
 // DownloadPricingData fetches data from the AWS Pricing API

+ 152 - 0
pkg/cloud/aws/provider_test.go

@@ -11,6 +11,7 @@ import (
 
 
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/core/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/cloud/models"
 	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/config"
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 )
 )
 
 
@@ -841,3 +842,154 @@ func TestAWS_getFargatePod(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
+
+// fakeProviderConfig implements models.ProviderConfig for testing
+type fakeProviderConfig struct {
+	customPricing *models.CustomPricing
+}
+
+func (f *fakeProviderConfig) GetCustomPricingData() (*models.CustomPricing, error) {
+	if f.customPricing != nil {
+		return f.customPricing, nil
+	}
+	return &models.CustomPricing{}, nil
+}
+
+func (f *fakeProviderConfig) Update(func(*models.CustomPricing) error) (*models.CustomPricing, error) {
+	return f.customPricing, nil
+}
+
+func (f *fakeProviderConfig) UpdateFromMap(map[string]string) (*models.CustomPricing, error) {
+	return f.customPricing, nil
+}
+
+func (f *fakeProviderConfig) ConfigFileManager() *config.ConfigFileManager {
+	return nil
+}
+
+func TestAWS_SpotRefreshEnabled(t *testing.T) {
+	tests := []struct {
+		name                string
+		spotDataBucket      string
+		spotDataRegion      string
+		projectID           string
+		spotDataFeedEnabled string
+		want                bool
+	}{
+		{
+			name:                "disabled via config - with bucket",
+			spotDataBucket:      "my-bucket",
+			spotDataRegion:      "us-east-1",
+			projectID:           "123456789",
+			spotDataFeedEnabled: "false",
+			want:                false,
+		},
+		{
+			name:                "disabled via config - with projectID only",
+			projectID:           "123456789",
+			spotDataFeedEnabled: "false",
+			want:                false,
+		},
+		{
+			name:                "enabled by default - with bucket",
+			spotDataBucket:      "my-bucket",
+			spotDataRegion:      "us-east-1",
+			projectID:           "123456789",
+			spotDataFeedEnabled: "",
+			want:                true,
+		},
+		{
+			name:                "enabled explicitly - with bucket",
+			spotDataBucket:      "my-bucket",
+			spotDataRegion:      "us-east-1",
+			projectID:           "123456789",
+			spotDataFeedEnabled: "true",
+			want:                true,
+		},
+		{
+			name:                "no spot config - disabled",
+			spotDataBucket:      "",
+			spotDataRegion:      "",
+			projectID:           "",
+			spotDataFeedEnabled: "",
+			want:                false,
+		},
+		{
+			name:                "no spot config - but explicitly enabled",
+			spotDataBucket:      "",
+			spotDataRegion:      "",
+			projectID:           "",
+			spotDataFeedEnabled: "true",
+			want:                false,
+		},
+		{
+			name:                "only projectID set - enabled by default",
+			projectID:           "123456789",
+			spotDataFeedEnabled: "",
+			want:                true,
+		},
+		{
+			name:                "only bucket set - enabled by default",
+			spotDataBucket:      "my-bucket",
+			spotDataFeedEnabled: "",
+			want:                true,
+		},
+		{
+			name:                "only region set - enabled by default",
+			spotDataRegion:      "us-east-1",
+			spotDataFeedEnabled: "",
+			want:                true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			aws := &AWS{
+				SpotDataBucket: tt.spotDataBucket,
+				SpotDataRegion: tt.spotDataRegion,
+				ProjectID:      tt.projectID,
+				Config: &fakeProviderConfig{
+					customPricing: &models.CustomPricing{
+						SpotDataFeedEnabled: tt.spotDataFeedEnabled,
+					},
+				},
+			}
+
+			got := aws.SpotRefreshEnabled()
+			if got != tt.want {
+				t.Errorf("AWS.SpotRefreshEnabled() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+
+	// Test nil Config scenario to ensure no panic
+	t.Run("nil config - falls back to field check", func(t *testing.T) {
+		aws := &AWS{
+			SpotDataBucket: "my-bucket",
+			SpotDataRegion: "us-east-1",
+			ProjectID:      "123456789",
+			Config:         nil, // nil Config should not cause panic
+		}
+
+		got := aws.SpotRefreshEnabled()
+		want := true // Should fall back to field-based check
+		if got != want {
+			t.Errorf("AWS.SpotRefreshEnabled() with nil Config = %v, want %v", got, want)
+		}
+	})
+
+	t.Run("nil config - no spot fields", func(t *testing.T) {
+		aws := &AWS{
+			SpotDataBucket: "",
+			SpotDataRegion: "",
+			ProjectID:      "",
+			Config:         nil, // nil Config should not cause panic
+		}
+
+		got := aws.SpotRefreshEnabled()
+		want := false // No fields set, should return false
+		if got != want {
+			t.Errorf("AWS.SpotRefreshEnabled() with nil Config and no fields = %v, want %v", got, want)
+		}
+	})
+}

+ 0 - 2
pkg/cloud/config/configurations_test.go

@@ -116,7 +116,6 @@ var (
 						ID:     "id",
 						ID:     "id",
 						Secret: "secret",
 						Secret: "secret",
 					},
 					},
-					CURVersion: "2.0",
 				},
 				},
 			},
 			},
 		},
 		},
@@ -150,7 +149,6 @@ var (
 						Authorizer: &aws.ServiceAccount{},
 						Authorizer: &aws.ServiceAccount{},
 						RoleARN:    "roleArn",
 						RoleARN:    "roleArn",
 					},
 					},
-					CURVersion: "2.0",
 				},
 				},
 			},
 			},
 		},
 		},

+ 0 - 1
pkg/cloud/config/controller_handlers_test.go

@@ -62,7 +62,6 @@ func Test_ParseConfig_Athena(t *testing.T) {
 			ID:     "id",
 			ID:     "id",
 			Secret: "secret",
 			Secret: "secret",
 		},
 		},
-		CURVersion: "curversion",
 	}
 	}
 
 
 	configBytes, err := json.Marshal(config)
 	configBytes, err := json.Marshal(config)

+ 0 - 3
pkg/cloud/config/controller_test.go

@@ -22,7 +22,6 @@ var validAthenaConf = &aws.AthenaConfiguration{
 	Workgroup:  "workgroup",
 	Workgroup:  "workgroup",
 	Account:    "account",
 	Account:    "account",
 	Authorizer: &aws.ServiceAccount{},
 	Authorizer: &aws.ServiceAccount{},
-	CURVersion: "2.0",
 }
 }
 
 
 // Config with the same key as the baseline but is not equal to it because of the change in the non-keyed property Workgroup
 // Config with the same key as the baseline but is not equal to it because of the change in the non-keyed property Workgroup
@@ -34,7 +33,6 @@ var validAthenaConfModifiedProperty = &aws.AthenaConfiguration{
 	Workgroup:  "workgroup1",
 	Workgroup:  "workgroup1",
 	Account:    "account",
 	Account:    "account",
 	Authorizer: &aws.ServiceAccount{},
 	Authorizer: &aws.ServiceAccount{},
-	CURVersion: "2.0",
 }
 }
 
 
 // Config with the same key as baseline but is invalid due to missing Authorizer
 // Config with the same key as baseline but is invalid due to missing Authorizer
@@ -46,7 +44,6 @@ var invalidAthenaConf = &aws.AthenaConfiguration{
 	Workgroup:  "workgroup",
 	Workgroup:  "workgroup",
 	Account:    "account",
 	Account:    "account",
 	Authorizer: nil,
 	Authorizer: nil,
-	CURVersion: "2.0",
 }
 }
 
 
 // A valid config with a different key from the baseline
 // A valid config with a different key from the baseline

+ 1 - 1
pkg/cloud/models/models.go

@@ -154,6 +154,7 @@ type CustomPricing struct {
 	AwsSpotDataRegion            string `json:"awsSpotDataRegion,omitempty"`
 	AwsSpotDataRegion            string `json:"awsSpotDataRegion,omitempty"`
 	AwsSpotDataBucket            string `json:"awsSpotDataBucket,omitempty"`
 	AwsSpotDataBucket            string `json:"awsSpotDataBucket,omitempty"`
 	AwsSpotDataPrefix            string `json:"awsSpotDataPrefix,omitempty"`
 	AwsSpotDataPrefix            string `json:"awsSpotDataPrefix,omitempty"`
+	SpotDataFeedEnabled          string `json:"spotDataFeedEnabled,omitempty"`
 	ProjectID                    string `json:"projectID,omitempty"`
 	ProjectID                    string `json:"projectID,omitempty"`
 	AthenaProjectID              string `json:"athenaProjectID,omitempty"`
 	AthenaProjectID              string `json:"athenaProjectID,omitempty"`
 	AthenaBucketName             string `json:"athenaBucketName"`
 	AthenaBucketName             string `json:"athenaBucketName"`
@@ -163,7 +164,6 @@ type CustomPricing struct {
 	AthenaTable                  string `json:"athenaTable"`
 	AthenaTable                  string `json:"athenaTable"`
 	AthenaWorkgroup              string `json:"athenaWorkgroup"`
 	AthenaWorkgroup              string `json:"athenaWorkgroup"`
 	MasterPayerARN               string `json:"masterPayerARN"`
 	MasterPayerARN               string `json:"masterPayerARN"`
-	AthenaCURVersion             string `json:"athenaCURVersion,omitempty"` // "1.0" or "2.0", defaults to "2.0"
 	BillingDataDataset           string `json:"billingDataDataset,omitempty"`
 	BillingDataDataset           string `json:"billingDataDataset,omitempty"`
 	CustomPricesEnabled          string `json:"customPricesEnabled"`
 	CustomPricesEnabled          string `json:"customPricesEnabled"`
 	AzureSubscriptionID          string `json:"azureSubscriptionID"`
 	AzureSubscriptionID          string `json:"azureSubscriptionID"`

+ 0 - 32
pkg/cloud/models/models_test.go

@@ -120,35 +120,3 @@ func TestSetSetCustomPricingField(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
-
-func TestCustomPricing_AthenaCURVersion(t *testing.T) {
-	testCases := map[string]struct {
-		curVersion string
-		expected   string
-	}{
-		"CUR version 1.0": {
-			curVersion: "1.0",
-			expected:   "1.0",
-		},
-		"CUR version 2.0": {
-			curVersion: "2.0",
-			expected:   "2.0",
-		},
-		"empty CUR version": {
-			curVersion: "",
-			expected:   "",
-		},
-	}
-
-	for name, testCase := range testCases {
-		t.Run(name, func(t *testing.T) {
-			cp := &CustomPricing{
-				AthenaCURVersion: testCase.curVersion,
-			}
-
-			if cp.AthenaCURVersion != testCase.expected {
-				t.Errorf("expected AthenaCURVersion to be '%s', got '%s'", testCase.expected, cp.AthenaCURVersion)
-			}
-		})
-	}
-}

+ 12 - 2
pkg/cloud/otc/pricingapi.go

@@ -9,6 +9,8 @@ import (
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/log"
 )
 )
 
 
+var otcHTTPClient = http.DefaultClient
+
 // Fetches and flattens all product entries across multiple services with pagination
 // Fetches and flattens all product entries across multiple services with pagination
 func (otc *OTC) fetchPaginatedProducts(serviceNames []string) ([]Product, error) {
 func (otc *OTC) fetchPaginatedProducts(serviceNames []string) ([]Product, error) {
 	const baseURL = "https://calculator.otc-service.com/de/open-telekom-price-api/"
 	const baseURL = "https://calculator.otc-service.com/de/open-telekom-price-api/"
@@ -20,14 +22,22 @@ func (otc *OTC) fetchPaginatedProducts(serviceNames []string) ([]Product, error)
 	for {
 	for {
 		url := fmt.Sprintf("%s?%s&columns%%5B0%%5D=productIdParameter&columns%%5B1%%5D=opiFlavour&columns%%5B2%%5D=osUnit&columns%%5B3%%5D=vCpu&columns%%5B4%%5D=ram&columns%%5B5%%5D=priceAmount&limitFrom=%d", baseURL, query, limitFrom)
 		url := fmt.Sprintf("%s?%s&columns%%5B0%%5D=productIdParameter&columns%%5B1%%5D=opiFlavour&columns%%5B2%%5D=osUnit&columns%%5B3%%5D=vCpu&columns%%5B4%%5D=ram&columns%%5B5%%5D=priceAmount&limitFrom=%d", baseURL, query, limitFrom)
 
 
-		resp, err := http.Get(url)
+		resp, err := otcHTTPClient.Get(url)
 		if err != nil {
 		if err != nil {
+			if resp != nil {
+				resp.Body.Close()
+			}
 			log.Errorf("Error fetching products from OTC API: %v", err)
 			log.Errorf("Error fetching products from OTC API: %v", err)
 			return nil, err
 			return nil, err
 		}
 		}
-		defer resp.Body.Close()
+		if resp.StatusCode != http.StatusOK {
+			io.Copy(io.Discard, resp.Body)
+			resp.Body.Close()
+			return nil, fmt.Errorf("OTC API returned unexpected status %d", resp.StatusCode)
+		}
 
 
 		pageData, stats, err := otc.loadPaginatedResponse(resp)
 		pageData, stats, err := otc.loadPaginatedResponse(resp)
+		resp.Body.Close()
 		if err != nil {
 		if err != nil {
 			log.Errorf("Error loading paginated response: %v", err)
 			log.Errorf("Error loading paginated response: %v", err)
 			return nil, err
 			return nil, err