فهرست منبع

Merge commit 'e0d9594b980771c137c08b5274bc290719259214' into feature/kubemodel

Sean Holcomb 1 هفته پیش
والد
کامیت
37ec3c9476
8فایلهای تغییر یافته به همراه277 افزوده شده و 27 حذف شده
  1. 1 1
      core/go.mod
  2. 2 2
      core/go.sum
  3. 7 7
      go.mod
  4. 14 14
      go.sum
  5. 1 1
      modules/collector-source/go.mod
  6. 2 2
      modules/collector-source/go.sum
  7. 54 0
      pkg/costmodel/costmodel.go
  8. 196 0
      pkg/costmodel/costmodel_test.go

+ 1 - 1
core/go.mod

@@ -75,7 +75,7 @@ require (
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/go-ini/ini v1.67.0 // indirect
-	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
+	github.com/go-jose/go-jose/v4 v4.1.4 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.22.4 // indirect

+ 2 - 2
core/go.sum

@@ -108,8 +108,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
 github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
-github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
-github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=

+ 7 - 7
go.mod

@@ -27,7 +27,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6
 	github.com/aws/aws-sdk-go-v2/service/athena v1.57.6
 	github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
 	github.com/aws/aws-sdk-go-v2/service/sts v1.42.1
 	github.com/aws/smithy-go v1.25.1
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
@@ -40,7 +40,7 @@ require (
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/kubecost/events v0.0.8
 	github.com/microcosm-cc/bluemonday v1.0.27
-	github.com/modelcontextprotocol/go-sdk v1.4.0
+	github.com/modelcontextprotocol/go-sdk v1.4.1
 	github.com/opencost/opencost/core v0.0.0-20250521155634-81d2b597d1bc
 	github.com/opencost/opencost/modules/collector-source v0.0.0-00010101000000-000000000000
 	github.com/opencost/opencost/modules/prometheus-source v0.0.0-00010101000000-000000000000
@@ -79,7 +79,7 @@ require (
 	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.13 // indirect
 	github.com/go-ini/ini v1.67.0 // indirect
-	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
+	github.com/go-jose/go-jose/v4 v4.1.4 // indirect
 	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
 	github.com/go-openapi/swag/conv v0.25.5 // indirect
 	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
@@ -107,7 +107,7 @@ require (
 	github.com/prometheus/common v0.67.5 // indirect
 	github.com/sagikazarmark/locafero v0.12.0 // indirect
 	github.com/segmentio/asm v1.2.1 // indirect
-	github.com/segmentio/encoding v0.5.3 // indirect
+	github.com/segmentio/encoding v0.5.4 // indirect
 	github.com/sony/gobreaker v1.0.0 // indirect
 	github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
 	github.com/tinylib/msgp v1.6.3 // indirect
@@ -145,15 +145,15 @@ require (
 	github.com/Azure/go-autorest/tracing v0.6.1 // 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/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect

+ 14 - 14
go.sum

@@ -94,8 +94,8 @@ 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-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
 github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
-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/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
 github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
 github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
 github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
@@ -116,14 +116,14 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumh
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0/go.mod h1:T6ndRfdhnXLIY5oKBHjYZDVj706los2zGdpThppquvA=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
-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/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
-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/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
 github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
 github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
 github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
@@ -182,8 +182,8 @@ github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9t
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
-github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
-github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -345,8 +345,8 @@ github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRi
 github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8=
-github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
+github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
+github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -404,8 +404,8 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
 github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
 github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
-github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
-github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
+github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=

+ 1 - 1
modules/collector-source/go.mod

@@ -55,7 +55,7 @@ require (
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/go-ini/ini v1.67.0 // indirect
-	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
+	github.com/go-jose/go-jose/v4 v4.1.4 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect

+ 2 - 2
modules/collector-source/go.sum

@@ -103,8 +103,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
 github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
-github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
-github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=

+ 54 - 0
pkg/costmodel/costmodel.go

@@ -33,6 +33,12 @@ const (
 	profileThreshold = 1000 * 1000 * 1000 // 1s (in ns)
 
 	unmountedPVsContainer = "unmounted-pvs"
+
+	annotationDomain = "opencost.io"
+
+	annotationStorageCost = annotationDomain + "/storage-hourly-cost"
+	annotationNodeCPUCost = annotationDomain + "/node-cpu-hourly-cost"
+	annotationNodeRAMCost = annotationDomain + "/node-ram-hourly-cost"
 )
 
 // isCron matches a CronJob name and captures the non-timestamp name
@@ -820,6 +826,11 @@ func (cm *CostModel) addPVData(pvClaimMapping map[string]*PersistentVolumeClaimD
 			storageClassMap["default"] = params
 			storageClassMap[""] = params
 		}
+
+		// Add custom cost annotation to storage class map
+		if key, found := storageClass.Annotations[annotationStorageCost]; found && params != nil {
+			params[annotationStorageCost] = key
+		}
 	}
 
 	pvs := cache.GetAllPersistentVolumes()
@@ -862,6 +873,24 @@ func (cm *CostModel) addPVData(pvClaimMapping map[string]*PersistentVolumeClaimD
 	return nil
 }
 
+// Checks if the provided cost string can be parsed into a finite, non-negative float64.
+// If the cost is invalid, it logs a warning with the cost value and the reason.
+func (cm *CostModel) costIsValid(cost string) bool {
+	parsedCost, err := strconv.ParseFloat(cost, 64)
+	if err != nil {
+		log.Warnf("Invalid cost value: %s. Error: %s", cost, err.Error())
+		return false
+	}
+
+	// Check if the parsed cost is a valid number (not NaN, not Inf, and non-negative)
+	if math.IsNaN(parsedCost) || math.IsInf(parsedCost, 0) || parsedCost < 0 {
+		log.Warnf("Invalid cost value: %s. Error: cost must be a finite, non-negative number", cost)
+		return false
+	}
+
+	return true
+}
+
 func (cm *CostModel) GetPVCost(pv *costAnalyzerCloud.PV, kpv *clustercache.PersistentVolume, defaultRegion string) error {
 	cp := cm.Provider
 	cfg, err := cp.GetConfig()
@@ -870,6 +899,21 @@ func (cm *CostModel) GetPVCost(pv *costAnalyzerCloud.PV, kpv *clustercache.Persi
 	}
 	key := cp.GetPVKey(kpv, pv.Parameters, defaultRegion)
 	pv.ProviderID = key.ID()
+
+	// If PV has a custom cost annotation, use that to mandate the cost
+	if cost, found := kpv.Annotations[annotationStorageCost]; found && cm.costIsValid(cost) {
+		log.Infof("Found custom cost from annotation for PV %s: %s", kpv.Name, cost)
+		pv.Cost = cost
+		return nil
+	}
+
+	// If SC has a custom cost annotation, use that to mandate the cost
+	if cost, found := pv.Parameters[annotationStorageCost]; found && cm.costIsValid(cost) {
+		log.Infof("Found custom cost from Storage Class annotation for PV %s: %s", kpv.Name, cost)
+		pv.Cost = cost
+		return nil
+	}
+
 	pvWithCost, err := cp.PVPricing(key)
 	if err != nil {
 		pv.Cost = cfg.Storage
@@ -931,6 +975,16 @@ func (cm *CostModel) GetNodeCost() (map[string]*costAnalyzerCloud.Node, error) {
 			}
 		}
 
+		if cost, found := n.Annotations[annotationNodeCPUCost]; found && cm.costIsValid(cost) {
+			log.Infof("Found custom CPU cost from annotation for Node %s: %s", n.Name, cost)
+			cnode.VCPUCost = cost
+		}
+
+		if cost, found := n.Annotations[annotationNodeRAMCost]; found && cm.costIsValid(cost) {
+			log.Infof("Found custom RAM cost from annotation for Node %s: %s", n.Name, cost)
+			cnode.RAMCost = cost
+		}
+
 		pmd.PricingTypeCounts[cnode.PricingType]++
 
 		// newCnode builds upon cnode but populates/overrides certain fields.

+ 196 - 0
pkg/costmodel/costmodel_test.go

@@ -8,8 +8,13 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/opencost/opencost/core/pkg/storage"
 	"github.com/opencost/opencost/core/pkg/util"
+	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/cloud/provider"
+	"github.com/opencost/opencost/pkg/config"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 )
@@ -350,3 +355,194 @@ func TestGetContainerAllocation(t *testing.T) {
 		})
 	}
 }
+
+func TestStorageCostAnnotations(t *testing.T) {
+	t.Parallel()
+
+	confMan := config.NewConfigFileManager(storage.NewFileStorage("../../"))
+
+	customProvider := &provider.CSVProvider{
+		CSVLocation: "../../configs/pricing_schema_pv.csv",
+		CustomProvider: &provider.CustomProvider{
+			Config: provider.NewProviderConfig(confMan, "../../configs/default.json"),
+		},
+	}
+	err := customProvider.DownloadPricingData()
+	assert.NoError(t, err)
+
+	costModel := &CostModel{
+		Provider: customProvider,
+	}
+
+	providerConfig, err := customProvider.GetConfig()
+	assert.NoError(t, err)
+	assert.NotNil(t, providerConfig)
+
+	type testCase struct {
+		name         string
+		pv           *models.PV
+		pvc          *clustercache.PersistentVolume
+		expectedCost string
+	}
+
+	testCases := []testCase{
+		{
+			name: "Cost from provider",
+			pv:   &models.PV{},
+			pvc: &clustercache.PersistentVolume{
+				Name: "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d",
+			},
+			expectedCost: "0.1337",
+		},
+		{
+			name: "Cost from custom provider config",
+			pv:   &models.PV{},
+			pvc: &clustercache.PersistentVolume{
+				Name: "fake-name",
+			},
+			expectedCost: providerConfig.Storage,
+		},
+		{
+			name: "Cost from annotations",
+			pv:   &models.PV{},
+			pvc: &clustercache.PersistentVolume{
+				Name: "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d",
+				Annotations: map[string]string{
+					annotationStorageCost: "123.123",
+				},
+			},
+			expectedCost: "123.123",
+		},
+		{
+			name: "Cost from storage class and with no annotations",
+			pv: &models.PV{
+				Parameters: map[string]string{
+					annotationStorageCost: "123.124",
+				},
+			},
+			pvc: &clustercache.PersistentVolume{
+				Name: "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d",
+			},
+			expectedCost: "123.124",
+		},
+		{
+			name: "Cost from storage class and with annotations",
+			pv: &models.PV{
+				Parameters: map[string]string{
+					annotationStorageCost: "123.124",
+				},
+			},
+			pvc: &clustercache.PersistentVolume{
+				Name: "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d",
+				Annotations: map[string]string{
+					annotationStorageCost: "123.125",
+				},
+			},
+			expectedCost: "123.125",
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+
+			err := costModel.GetPVCost(testCase.pv, testCase.pvc, "default-region")
+			assert.NoError(t, err)
+
+			assert.Equal(t, testCase.expectedCost, testCase.pv.Cost)
+		})
+	}
+}
+
+func TestNodeCostAnnotations(t *testing.T) {
+	t.Parallel()
+
+	confMan := config.NewConfigFileManager(storage.NewFileStorage("../../"))
+
+	customProvider := &provider.CSVProvider{
+		CSVLocation: "../../configs/pricing_schema_region.csv",
+		CustomProvider: &provider.CustomProvider{
+			Config: provider.NewProviderConfig(confMan, "../../configs/default.json"),
+		},
+	}
+	err := customProvider.DownloadPricingData()
+	assert.NoError(t, err)
+
+	costModel := &CostModel{
+		Provider: customProvider,
+		Cache: NewFakeNodeCache([]*clustercache.Node{
+			{
+				Name: "test-node-001",
+				Labels: map[string]string{
+					"topology.kubernetes.io/region": "regionone",
+				},
+			},
+			{
+				Name: "test-node-002",
+				Labels: map[string]string{
+					"topology.kubernetes.io/region": "regionone",
+				},
+				Annotations: map[string]string{
+					"opencost.io/node-cpu-hourly-cost": "111",
+					"opencost.io/node-ram-hourly-cost": "222",
+				},
+			},
+		}),
+	}
+	assert.NotNil(t, costModel)
+
+	providerConfig, err := customProvider.GetConfig()
+	assert.NoError(t, err)
+	assert.NotNil(t, providerConfig)
+
+	nodeCost, err := costModel.GetNodeCost()
+	assert.NoError(t, err)
+	assert.NotNil(t, nodeCost)
+	assert.NotEmpty(t, nodeCost)
+
+	type testCase struct {
+		node     string
+		VCPUCost string
+		RAMCost  string
+	}
+	testCases := []testCase{
+		{
+			node:     "test-node-001",
+			VCPUCost: "+Inf",
+			RAMCost:  "+Inf",
+		},
+		{
+			node:     "test-node-002",
+			VCPUCost: "111",
+			RAMCost:  "222",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.node, func(t *testing.T) {
+			t.Parallel()
+
+			nodeCost, ok := nodeCost[tc.node]
+			require.True(t, ok)
+
+			assert.Equal(t, tc.VCPUCost, nodeCost.VCPUCost)
+			assert.Equal(t, tc.RAMCost, nodeCost.RAMCost)
+		})
+	}
+}
+
+// FakeNodeCache implements ClusterCache interface for testing
+type FakeNodeCache struct {
+	clustercache.ClusterCache
+	nodes []*clustercache.Node
+}
+
+func (f FakeNodeCache) GetAllNodes() []*clustercache.Node {
+	return f.nodes
+}
+
+func NewFakeNodeCache(nodes []*clustercache.Node) FakeNodeCache {
+	return FakeNodeCache{
+		nodes: nodes,
+	}
+}