瀏覽代碼

Modular Opencost (#3031)

Signed-off-by: r2k1 <yokree@gmail.com>
Signed-off-by: Matt Bolt <mbolt35@gmail.com>
Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Co-authored-by: r2k1 <yokree@gmail.com>
Co-authored-by: Sean Holcomb <seanholcomb@gmail.com>
Co-authored-by: Thomas Nguyen <thomasvn.dev@gmail.com>
Matt Bolt 11 月之前
父節點
當前提交
a2c287d402
共有 100 個文件被更改,包括 9826 次插入468 次删除
  1. 1 2
      Dockerfile
  2. 0 1
      config/invalid.json
  3. 94 22
      core/go.mod
  4. 240 48
      core/go.sum
  5. 414 0
      core/pkg/clustercache/clustercache.go
  6. 24 0
      core/pkg/clusters/clusterinfo.go
  7. 63 0
      core/pkg/clusters/util.go
  8. 88 0
      core/pkg/diagnostics/diagnostics.go
  9. 20 0
      core/pkg/diagnostics/exporter/controller.go
  10. 11 0
      core/pkg/diagnostics/exporter/encoder.go
  11. 24 0
      core/pkg/diagnostics/exporter/exporter.go
  12. 40 0
      core/pkg/diagnostics/exporter/source.go
  13. 187 0
      core/pkg/diagnostics/service.go
  14. 430 0
      core/pkg/diagnostics/service_test.go
  15. 0 0
      core/pkg/errors/panic.go
  16. 308 0
      core/pkg/exporter/controller.go
  17. 106 0
      core/pkg/exporter/encoder.go
  18. 130 0
      core/pkg/exporter/exporter.go
  19. 111 0
      core/pkg/exporter/exporter_test.go
  20. 99 0
      core/pkg/exporter/pathing/bingenpath.go
  21. 84 0
      core/pkg/exporter/pathing/eventpath.go
  22. 209 0
      core/pkg/exporter/pathing/path_test.go
  23. 11 0
      core/pkg/exporter/pathing/pathing.go
  24. 99 0
      core/pkg/exporter/pathing/pathutils/pathutils.go
  25. 27 0
      core/pkg/exporter/source.go
  26. 146 0
      core/pkg/exporter/validator/validator.go
  27. 253 0
      core/pkg/exporter/validator/validator_test.go
  28. 22 0
      core/pkg/heartbeat/exporter/controller.go
  29. 11 0
      core/pkg/heartbeat/exporter/encoder.go
  30. 24 0
      core/pkg/heartbeat/exporter/exporter.go
  31. 87 0
      core/pkg/heartbeat/exporter/heartbeat_test.go
  32. 80 0
      core/pkg/heartbeat/exporter/source.go
  33. 59 0
      core/pkg/heartbeat/exporter/source_test.go
  34. 34 0
      core/pkg/heartbeat/heartbeat.go
  35. 0 0
      core/pkg/kubeconfig/loader.go
  36. 41 0
      core/pkg/nodestats/config.go
  37. 59 0
      core/pkg/nodestats/formatter.go
  38. 132 0
      core/pkg/nodestats/nodes_test.go
  39. 216 0
      core/pkg/nodestats/nodestats.go
  40. 90 0
      core/pkg/nodestats/request.go
  41. 15 15
      core/pkg/opencost/allocation_test.go
  42. 0 16
      core/pkg/opencost/common.go
  43. 43 0
      core/pkg/opencost/exporter/allocation/source.go
  44. 43 0
      core/pkg/opencost/exporter/asset/source.go
  45. 151 0
      core/pkg/opencost/exporter/controllers.go
  46. 397 0
      core/pkg/opencost/exporter/exporter_test.go
  47. 55 0
      core/pkg/opencost/exporter/exporters.go
  48. 43 0
      core/pkg/opencost/exporter/networkinsight/source.go
  49. 0 36
      core/pkg/opencost/window.go
  50. 1 1
      core/pkg/opencost/window_test.go
  51. 46 0
      core/pkg/pipelines/name.go
  52. 142 0
      core/pkg/source/datasource.go
  53. 1340 0
      core/pkg/source/decoders.go
  54. 46 3
      core/pkg/source/error.go
  55. 1 1
      core/pkg/source/error_test.go
  56. 68 0
      core/pkg/source/future.go
  57. 110 0
      core/pkg/source/querygroup.go
  58. 241 0
      core/pkg/source/queryresult.go
  59. 0 0
      core/pkg/storage/azurestorage.go
  60. 0 0
      core/pkg/storage/bucketstorage.go
  61. 0 0
      core/pkg/storage/bucketstorage_test.go
  62. 0 0
      core/pkg/storage/filestorage.go
  63. 0 0
      core/pkg/storage/filestorage_test.go
  64. 0 0
      core/pkg/storage/gcsstorage.go
  65. 124 0
      core/pkg/storage/memfile/memfile.go
  66. 57 0
      core/pkg/storage/memfile/util.go
  67. 171 0
      core/pkg/storage/memorystorage.go
  68. 382 0
      core/pkg/storage/memorystorage_test.go
  69. 0 0
      core/pkg/storage/prefixedbucketstorage.go
  70. 15 0
      core/pkg/storage/s3storage.go
  71. 0 0
      core/pkg/storage/storage.go
  72. 6 0
      core/pkg/storage/storagetypes.go
  73. 0 0
      core/pkg/storage/storagetypes_test.go
  74. 0 0
      core/pkg/storage/tlsconfig.go
  75. 21 0
      core/pkg/util/atomic/atomicrunstate.go
  76. 70 21
      core/pkg/util/atomic/atomicrunstate_test.go
  77. 4 9
      core/pkg/util/buffer.go
  78. 57 0
      core/pkg/util/buffer_test.go
  79. 52 0
      core/pkg/util/iterutil/iterutil.go
  80. 140 0
      core/pkg/util/iterutil/iterutil_test.go
  81. 28 0
      core/pkg/util/maputil/maputil.go
  82. 129 0
      core/pkg/util/maputil/maputil_test.go
  83. 66 1
      core/pkg/util/mathutil/mathutil.go
  84. 48 0
      core/pkg/util/mathutil/mathutil_test.go
  85. 22 0
      core/pkg/util/promutil/promutil_test.go
  86. 34 0
      core/pkg/util/sliceutil/sliceutil.go
  87. 158 0
      core/pkg/util/sliceutil/sliceutil_test.go
  88. 9 0
      core/pkg/util/stringutil/stringutil.go
  89. 64 53
      core/pkg/util/stringutil/stringutil_test.go
  90. 9 1
      core/pkg/util/timeutil/timeutil.go
  91. 17 2
      core/pkg/util/worker/worker.go
  92. 二進制
      docs/image-1.png
  93. 42 0
      docs/modular-opencost.md
  94. 80 75
      go.mod
  95. 175 159
      go.sum
  96. 18 2
      justfile
  97. 3 0
      modules/collector-source/README.md
  98. 114 0
      modules/collector-source/go.mod
  99. 809 0
      modules/collector-source/go.sum
  100. 86 0
      modules/collector-source/pkg/collector/clustermap.go

+ 1 - 2
Dockerfile

@@ -22,8 +22,7 @@ RUN go mod download
 
 # Build the binary
 RUN set -e ;\
-    go test ./test/*.go;\
-    go test ./pkg/*;\
+    go test ./...;\
     cd cmd/costmodel;\
     GOOS=linux \
     go build -a -installsuffix cgo \

+ 0 - 1
config/invalid.json

@@ -1 +0,0 @@
-{"provider":"base","description":"Default prices based on GCP us-central1","CPU":"0.031611","spotCPU":"0.006655","RAM":"0.004237","spotRAM":"0.000892","GPU":"0.95","spotGPU":"0.308","storage":"0.00005479452","zoneNetworkEgress":"0.01","regionNetworkEgress":"0.01","internetNetworkEgress":"0.12","firstFiveForwardingRulesCost":"","additionalForwardingRuleCost":"","LBIngressDataCost":"","athenaBucketName":"","athenaRegion":"","athenaDatabase":"","athenaTable":"","athenaWorkgroup":"","masterPayerARN":"","customPricesEnabled":"false","defaultIdle":"","azureSubscriptionID":"","azureClientID":"","azureClientSecret":"","azureTenantID":"","azureBillingRegion":"","azureOfferDurableID":"","azureStorageSubscriptionID":"","azureStorageAccount":"","azureStorageAccessKey":"","azureStorageContainer":"","azureContainerPath":"","azureCloud":"","currencyCode":"","discount":"","negotiatedDiscount":"","sharedOverhead":"","clusterName":"","sharedNamespaces":"","sharedLabelNames":"","sharedLabelValues":"","shareTenancyCosts":"true","readOnly":"","editorAccess":"","kubecostToken":"","googleAnalyticsTag":"","excludeProviderID":""}

+ 94 - 22
core/go.mod

@@ -1,63 +1,135 @@
 module github.com/opencost/opencost/core
 
-go 1.22.7
+go 1.24.2
 
 require (
+	cloud.google.com/go/storage v1.36.0
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
+	github.com/aws/aws-sdk-go-v2 v1.36.3
+	github.com/aws/aws-sdk-go-v2/config v1.29.10
 	github.com/davecgh/go-spew v1.1.1
-	github.com/goccy/go-json v0.9.11
-	github.com/google/go-cmp v0.6.0
+	github.com/goccy/go-json v0.10.5
+	github.com/google/go-cmp v0.7.0
+	github.com/google/uuid v1.6.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.6.0
 	github.com/json-iterator/go v1.1.12
+	github.com/julienschmidt/httprouter v1.3.0
+	github.com/minio/minio-go/v7 v7.0.88
 	github.com/patrickmn/go-cache v2.1.0+incompatible
+	github.com/pkg/errors v0.9.1
+	github.com/prometheus/common v0.63.0
 	github.com/rs/zerolog v1.26.1
 	github.com/spf13/viper v1.8.1
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.10.0
 	golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
-	golang.org/x/sync v0.6.0
-	golang.org/x/text v0.14.0
-	google.golang.org/grpc v1.62.0
-	google.golang.org/protobuf v1.33.0
-	k8s.io/api v0.25.3
-	k8s.io/apimachinery v0.25.3
+	golang.org/x/oauth2 v0.27.0
+	golang.org/x/sync v0.12.0
+	golang.org/x/text v0.23.0
+	google.golang.org/api v0.162.0
+	google.golang.org/grpc v1.68.1
+	google.golang.org/protobuf v1.36.5
+	gopkg.in/yaml.v2 v2.4.0
+	k8s.io/api v0.33.1
+	k8s.io/apimachinery v0.33.1
+	k8s.io/client-go v0.33.1
+	k8s.io/kubelet v0.33.1
+	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e
 )
 
 require (
+	cloud.google.com/go v0.112.0 // indirect
+	cloud.google.com/go/compute/metadata v0.5.0 // indirect
+	cloud.google.com/go/iam v1.1.6 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.63 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
+	github.com/aws/smithy-go v1.22.2 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 	github.com/fatih/color v1.16.0 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
-	github.com/go-logr/logr v1.2.4 // indirect
+	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.20.2 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/gnostic-models v0.6.9 // indirect
+	github.com/google/s2a-go v0.1.7 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-hclog v1.6.2 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/minio/crc64nvme v1.0.1 // indirect
+	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/oklog/run v1.1.0 // indirect
 	github.com/pelletier/go-toml v1.9.3 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/rs/xid v1.6.0 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
-	golang.org/x/net v0.23.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
+	go.opentelemetry.io/otel v1.33.0 // indirect
+	go.opentelemetry.io/otel/metric v1.33.0 // indirect
+	go.opentelemetry.io/otel/trace v1.33.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/term v0.30.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
+	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/klog/v2 v2.80.0 // indirect
-	k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
-	sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
-	sigs.k8s.io/yaml v1.3.0 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
 )

+ 240 - 48
core/go.sum

@@ -18,15 +18,21 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
 cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
 cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
 cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
+cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
+cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -36,18 +42,64 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
+cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc=
+github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
 github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -55,11 +107,20 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -67,30 +128,53 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
-github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -116,10 +200,12 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
+github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -131,14 +217,16 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -150,10 +238,20 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
@@ -190,22 +288,40 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
 github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -218,6 +334,12 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
+github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs=
+github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -237,32 +359,48 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
 github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
+github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
+github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
 github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
@@ -274,17 +412,26 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -301,6 +448,22 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
+go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
+go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
+go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
+go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
+go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
@@ -312,6 +475,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -386,8 +551,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -400,6 +565,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
+golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -411,8 +578,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -462,10 +629,13 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -474,11 +644,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -531,10 +703,14 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -557,6 +733,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR
 google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
 google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
 google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps=
+google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -605,8 +783,12 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -627,8 +809,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
-google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
+google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -641,13 +823,15 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -669,21 +853,29 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.25.3 h1:Q1v5UFfYe87vi5H7NU0p4RXC26PPMT8KOpr1TLQbCMQ=
-k8s.io/api v0.25.3/go.mod h1:o42gKscFrEVjHdQnyRenACrMtbuJsVdP+WVjqejfzmI=
-k8s.io/apimachinery v0.25.3 h1:7o9ium4uyUOM76t6aunP0nZuex7gDf8VGwkR5RcJnQc=
-k8s.io/apimachinery v0.25.3/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo=
-k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
-k8s.io/klog/v2 v2.80.0 h1:lyJt0TWMPaGoODa8B8bUuxgHS3W/m/bNr2cca3brA/g=
-k8s.io/klog/v2 v2.80.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4=
-k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
+k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
+k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
+k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
+k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
+k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
+k8s.io/kubelet v0.33.1 h1:x4LCw1/iZVWOKA4RoITnuB8gMHnw31HPB3S0EF0EexE=
+k8s.io/kubelet v0.33.1/go.mod h1:8WpdC9M95VmsqIdGSQrajXooTfT5otEj8pGWOm+KKfQ=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
-sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

+ 414 - 0
core/pkg/clustercache/clustercache.go

@@ -0,0 +1,414 @@
+package clustercache
+
+import (
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/ptr"
+
+	appsv1 "k8s.io/api/apps/v1"
+	batchv1 "k8s.io/api/batch/v1"
+	v1 "k8s.io/api/core/v1"
+	policyv1 "k8s.io/api/policy/v1"
+	stv1 "k8s.io/api/storage/v1"
+)
+
+type Namespace struct {
+	Name        string
+	Labels      map[string]string
+	Annotations map[string]string
+}
+
+type Pod struct {
+	UID               types.UID
+	Name              string
+	Namespace         string
+	Labels            map[string]string
+	Annotations       map[string]string
+	OwnerReferences   []metav1.OwnerReference
+	Status            PodStatus
+	Spec              PodSpec
+	DeletionTimestamp *time.Time
+}
+
+type PodStatus struct {
+	PodIP             string
+	Phase             v1.PodPhase
+	ContainerStatuses []v1.ContainerStatus
+}
+
+type PodSpec struct {
+	NodeName      string
+	Containers    []Container
+	Volumes       []v1.Volume
+	RestartPolicy v1.RestartPolicy
+}
+
+type Container struct {
+	Name      string
+	Resources v1.ResourceRequirements
+}
+
+type Node struct {
+	Name           string
+	Labels         map[string]string
+	Annotations    map[string]string
+	Status         v1.NodeStatus
+	SpecProviderID string
+}
+
+type Service struct {
+	Name         string
+	Namespace    string
+	SpecSelector map[string]string
+	Type         v1.ServiceType
+	Status       v1.ServiceStatus
+	ClusterIP    string
+}
+
+type DaemonSet struct {
+	Name           string
+	Namespace      string
+	Labels         map[string]string
+	SpecContainers []v1.Container
+}
+
+type Deployment struct {
+	Name                    string
+	Namespace               string
+	Labels                  map[string]string
+	Annotations             map[string]string
+	MatchLabels             map[string]string
+	SpecSelector            *metav1.LabelSelector
+	SpecReplicas            *int32
+	SpecStrategy            appsv1.DeploymentStrategy
+	StatusAvailableReplicas int32
+	PodSpec                 PodSpec
+}
+
+type StatefulSet struct {
+	Name         string
+	Namespace    string
+	Labels       map[string]string
+	Annotations  map[string]string
+	SpecSelector *metav1.LabelSelector
+	SpecReplicas *int32
+	PodSpec      PodSpec
+}
+
+type PersistentVolumeClaim struct {
+	Name        string
+	Namespace   string
+	Spec        v1.PersistentVolumeClaimSpec
+	Labels      map[string]string
+	Annotations map[string]string
+}
+
+type StorageClass struct {
+	Name        string
+	Labels      map[string]string
+	Annotations map[string]string
+	Parameters  map[string]string
+	Provisioner string
+	TypeMeta    metav1.TypeMeta
+	Size        int
+}
+
+type Job struct {
+	Name      string
+	Namespace string
+	Status    batchv1.JobStatus
+}
+
+type PersistentVolume struct {
+	Name        string
+	Namespace   string
+	Labels      map[string]string
+	Annotations map[string]string
+	Spec        v1.PersistentVolumeSpec
+	Status      v1.PersistentVolumeStatus
+}
+
+type ReplicationController struct {
+	Name      string
+	Namespace string
+	Spec      v1.ReplicationControllerSpec
+}
+
+type PodDisruptionBudget struct {
+	Name      string
+	Namespace string
+	Spec      policyv1.PodDisruptionBudgetSpec
+	Status    policyv1.PodDisruptionBudgetStatus
+}
+
+type ReplicaSet struct {
+	Name            string
+	Namespace       string
+	OwnerReferences []metav1.OwnerReference
+	SpecSelector    *metav1.LabelSelector
+	Spec            appsv1.ReplicaSetSpec
+}
+
+type Volume struct {
+}
+
+// GetControllerOf returns a pointer to a copy of the controllerRef if controllee has a controller
+func GetControllerOf(pod *Pod) *metav1.OwnerReference {
+	ref := GetControllerOfNoCopy(pod)
+	if ref == nil {
+		return nil
+	}
+	cp := *ref
+	cp.Controller = ptr.To(*ref.Controller)
+	if ref.BlockOwnerDeletion != nil {
+		cp.BlockOwnerDeletion = ptr.To(*ref.BlockOwnerDeletion)
+	}
+	return &cp
+}
+
+// GetControllerOfNoCopy returns a pointer to the controllerRef if controllee has a controller
+func GetControllerOfNoCopy(pod *Pod) *metav1.OwnerReference {
+	refs := pod.OwnerReferences
+	for i := range refs {
+		if refs[i].Controller != nil && *refs[i].Controller {
+			return &refs[i]
+		}
+	}
+	return nil
+}
+
+func TransformNamespace(input *v1.Namespace) *Namespace {
+	return &Namespace{
+		Name:        input.Name,
+		Annotations: input.Annotations,
+		Labels:      input.Labels,
+	}
+}
+
+func TransformPodContainer(input v1.Container) Container {
+	return Container{
+		Name:      input.Name,
+		Resources: input.Resources,
+	}
+}
+
+func TransformPodStatus(input v1.PodStatus) PodStatus {
+	return PodStatus{
+		PodIP:             input.PodIP,
+		Phase:             input.Phase,
+		ContainerStatuses: input.ContainerStatuses,
+	}
+}
+
+func TransformPodSpec(input v1.PodSpec) PodSpec {
+	containers := make([]Container, len(input.Containers))
+	for i, container := range input.Containers {
+		containers[i] = TransformPodContainer(container)
+	}
+	return PodSpec{
+		NodeName:      input.NodeName,
+		Containers:    containers,
+		Volumes:       input.Volumes,
+		RestartPolicy: input.RestartPolicy,
+	}
+
+}
+
+func TransformTimestamp(input *metav1.Time) *time.Time {
+	if input == nil {
+		return nil
+	}
+
+	t := input.Time
+	return &t
+}
+
+func TransformPod(input *v1.Pod) *Pod {
+	return &Pod{
+		UID:               input.UID,
+		Name:              input.Name,
+		Namespace:         input.Namespace,
+		Labels:            input.Labels,
+		Annotations:       input.Annotations,
+		OwnerReferences:   input.OwnerReferences,
+		Spec:              TransformPodSpec(input.Spec),
+		Status:            TransformPodStatus(input.Status),
+		DeletionTimestamp: TransformTimestamp(input.DeletionTimestamp),
+	}
+}
+
+func TransformNode(input *v1.Node) *Node {
+	return &Node{
+		Name:           input.Name,
+		Labels:         input.Labels,
+		Annotations:    input.Annotations,
+		Status:         input.Status,
+		SpecProviderID: input.Spec.ProviderID,
+	}
+}
+
+func TransformService(input *v1.Service) *Service {
+	return &Service{
+		Name:         input.Name,
+		Namespace:    input.Namespace,
+		SpecSelector: input.Spec.Selector,
+		Type:         input.Spec.Type,
+		Status:       input.Status,
+		ClusterIP:    input.Spec.ClusterIP,
+	}
+}
+
+func TransformDaemonSet(input *appsv1.DaemonSet) *DaemonSet {
+	return &DaemonSet{
+		Name:           input.Name,
+		Namespace:      input.Namespace,
+		Labels:         input.Labels,
+		SpecContainers: input.Spec.Template.Spec.Containers,
+	}
+}
+
+func TransformDeployment(input *appsv1.Deployment) *Deployment {
+	return &Deployment{
+		Name:                    input.Name,
+		Namespace:               input.Namespace,
+		Labels:                  input.Labels,
+		MatchLabels:             input.Spec.Selector.MatchLabels,
+		SpecReplicas:            input.Spec.Replicas,
+		SpecSelector:            input.Spec.Selector,
+		SpecStrategy:            input.Spec.Strategy,
+		StatusAvailableReplicas: input.Status.AvailableReplicas,
+		PodSpec:                 TransformPodSpec(input.Spec.Template.Spec),
+	}
+}
+
+func TransformStatefulSet(input *appsv1.StatefulSet) *StatefulSet {
+	return &StatefulSet{
+		Name:         input.Name,
+		Namespace:    input.Namespace,
+		SpecSelector: input.Spec.Selector,
+		SpecReplicas: input.Spec.Replicas,
+		PodSpec:      TransformPodSpec(input.Spec.Template.Spec),
+	}
+}
+
+func TransformPersistentVolume(input *v1.PersistentVolume) *PersistentVolume {
+	return &PersistentVolume{
+		Name:        input.Name,
+		Namespace:   input.Namespace,
+		Labels:      input.Labels,
+		Annotations: input.Annotations,
+		Spec:        input.Spec,
+		Status:      input.Status,
+	}
+}
+
+func TransformPersistentVolumeClaim(input *v1.PersistentVolumeClaim) *PersistentVolumeClaim {
+	return &PersistentVolumeClaim{
+		Name:        input.Name,
+		Namespace:   input.Namespace,
+		Spec:        input.Spec,
+		Labels:      input.Labels,
+		Annotations: input.Annotations,
+	}
+}
+
+func TransformStorageClass(input *stv1.StorageClass) *StorageClass {
+	return &StorageClass{
+		Name:        input.Name,
+		Annotations: input.Annotations,
+		Labels:      input.Labels,
+		Parameters:  input.Parameters,
+		Provisioner: input.Provisioner,
+		TypeMeta:    input.TypeMeta,
+		Size:        input.Size(),
+	}
+}
+
+func TransformJob(input *batchv1.Job) *Job {
+	return &Job{
+		Name:      input.Name,
+		Namespace: input.Namespace,
+		Status:    input.Status,
+	}
+}
+
+func TransformReplicationController(input *v1.ReplicationController) *ReplicationController {
+	return &ReplicationController{
+		Name:      input.Name,
+		Namespace: input.Namespace,
+		Spec:      input.Spec,
+	}
+}
+
+func TransformPodDisruptionBudget(input *policyv1.PodDisruptionBudget) *PodDisruptionBudget {
+	return &PodDisruptionBudget{
+		Name:      input.Name,
+		Namespace: input.Namespace,
+		Spec:      input.Spec,
+		Status:    input.Status,
+	}
+}
+
+func TransformReplicaSet(input *appsv1.ReplicaSet) *ReplicaSet {
+	return &ReplicaSet{
+		Name:            input.Name,
+		Namespace:       input.Namespace,
+		OwnerReferences: input.OwnerReferences,
+		Spec:            input.Spec,
+		SpecSelector:    input.Spec.Selector,
+	}
+}
+
+// ClusterCache defines an contract for an object which caches components within a cluster, ensuring
+// up to date resources using watchers
+type ClusterCache interface {
+	// Run starts the watcher processes
+	Run()
+
+	// Stops the watcher processes
+	Stop()
+
+	// GetAllNamespaces returns all the cached namespaces
+	GetAllNamespaces() []*Namespace
+
+	// GetAllNodes returns all the cached nodes
+	GetAllNodes() []*Node
+
+	// GetAllPods returns all the cached pods
+	GetAllPods() []*Pod
+
+	// GetAllServices returns all the cached services
+	GetAllServices() []*Service
+
+	// GetAllDaemonSets returns all the cached DaemonSets
+	GetAllDaemonSets() []*DaemonSet
+
+	// GetAllDeployments returns all the cached deployments
+	GetAllDeployments() []*Deployment
+
+	// GetAllStatfulSets returns all the cached StatefulSets
+	GetAllStatefulSets() []*StatefulSet
+
+	// GetAllReplicaSets returns all the cached ReplicaSets
+	GetAllReplicaSets() []*ReplicaSet
+
+	// GetAllPersistentVolumes returns all the cached persistent volumes
+	GetAllPersistentVolumes() []*PersistentVolume
+
+	// GetAllPersistentVolumeClaims returns all the cached persistent volume claims
+	GetAllPersistentVolumeClaims() []*PersistentVolumeClaim
+
+	// GetAllStorageClasses returns all the cached storage classes
+	GetAllStorageClasses() []*StorageClass
+
+	// GetAllJobs returns all the cached jobs
+	GetAllJobs() []*Job
+
+	// GetAllPodDisruptionBudgets returns all cached pod disruption budgets
+	GetAllPodDisruptionBudgets() []*PodDisruptionBudget
+
+	// GetAllReplicationControllers returns all cached replication controllers
+	GetAllReplicationControllers() []*ReplicationController
+}

+ 24 - 0
core/pkg/clusters/clusterinfo.go

@@ -1,5 +1,7 @@
 package clusters
 
+import "maps"
+
 // The following constants are used as keys into the cluster info map data structure
 const (
 	ClusterInfoIdKey               = "id"
@@ -73,3 +75,25 @@ type ClusterInfoProvider interface {
 	// GetClusterInfo returns a string map containing the local/remote connected cluster info
 	GetClusterInfo() map[string]string
 }
+
+// ClusterInfoDecorator is a ClusterInfoProvider that decorates another ClusterInfoProvider with additional info.
+type ClusterInfoDecorator struct {
+	provider       ClusterInfoProvider
+	additionalInfo map[string]string
+}
+
+// NewClusterInfoDecorator creates a new ClusterInfoDecorator which will append additional info to the cluster info
+// returned by the provider.
+func NewClusterInfoDecorator(provider ClusterInfoProvider, additionalInfo map[string]string) ClusterInfoProvider {
+	return &ClusterInfoDecorator{
+		provider:       provider,
+		additionalInfo: additionalInfo,
+	}
+}
+
+// GetClusterInfo returns a string map containing the local/remote connected cluster info
+func (cid *ClusterInfoDecorator) GetClusterInfo() map[string]string {
+	info := cid.provider.GetClusterInfo()
+	maps.Copy(info, cid.additionalInfo)
+	return info
+}

+ 63 - 0
core/pkg/clusters/util.go

@@ -0,0 +1,63 @@
+package clusters
+
+import "fmt"
+
+// MapToClusterInfo returns a ClusterInfo using parsed data from a string map. If
+// parsing the map fails for id and/or name, an error is returned.
+func MapToClusterInfo(info map[string]string) (*ClusterInfo, error) {
+	var id string
+	var name string
+
+	if i, ok := info[ClusterInfoIdKey]; ok {
+		id = i
+	} else {
+		return nil, fmt.Errorf("cluster info missing id")
+	}
+	if n, ok := info[ClusterInfoNameKey]; ok {
+		name = n
+	} else {
+		name = id
+	}
+
+	var clusterProfile string
+	var provider string
+	var account string
+	var project string
+	var region string
+	var provisioner string
+
+	if cp, ok := info[ClusterInfoProfileKey]; ok {
+		clusterProfile = cp
+	}
+
+	if pvdr, ok := info[ClusterInfoProviderKey]; ok {
+		provider = pvdr
+	}
+
+	if acct, ok := info[ClusterInfoAccountKey]; ok {
+		account = acct
+	}
+
+	if proj, ok := info[ClusterInfoProjectKey]; ok {
+		project = proj
+	}
+
+	if reg, ok := info[ClusterInfoRegionKey]; ok {
+		region = reg
+	}
+
+	if pvsr, ok := info[ClusterInfoProvisionerKey]; ok {
+		provisioner = pvsr
+	}
+
+	return &ClusterInfo{
+		ID:          id,
+		Name:        name,
+		Profile:     clusterProfile,
+		Provider:    provider,
+		Account:     account,
+		Project:     project,
+		Region:      region,
+		Provisioner: provisioner,
+	}, nil
+}

+ 88 - 0
core/pkg/diagnostics/diagnostics.go

@@ -0,0 +1,88 @@
+package diagnostics
+
+import (
+	"context"
+	"time"
+)
+
+// DiagnosticsEventName is used to represent the name of the diagnostics export pipeline event to categorize for storage.
+const DiagnosticsEventName string = "diagnostics"
+
+// DiagnosticResult represent the result of a diagnostic run, and contains basic diagnostic information and additional
+// custom diagnostic information appended by the specific runner.
+type DiagnosticResult struct {
+	// Unique Identifier for the diagnostic run result.
+	ID string `json:"id"`
+
+	// Name of the diagnostic that ran.
+	Name string `json:"name"`
+
+	// Description of the diagnostic run, human readable description of what the diagnostic shows.
+	Description string `json:"description"`
+
+	// Category of the diagnostic run, which can be used to group similar diagnostics together.
+	Category string `json:"category"`
+
+	// Timestamp containing the time when the diagnostic run was executed.
+	Timestamp time.Time `json:"timestamp"`
+
+	// Error message if the diagnostic run failed. If this field is non-empty, the diagnostic run should be
+	// considered a failure.
+	Error string `json:"error,omitempty"`
+
+	// Details contains additional custom information about the diagnostic run that can be added by the diagnostic
+	// runner.
+	Details map[string]any `json:"details,omitempty"`
+}
+
+// DiagnosticsRunReport is a struct that contains the start time of the diagnostics run, and all of the results.
+type DiagnosticsRunReport struct {
+	// StartTime contains the time when the full diagnostics run started
+	StartTime time.Time `json:"startTime"`
+
+	// Results contains all of the results of the diagnostics run.
+	Results []*DiagnosticResult `json:"results"`
+}
+
+// DiagnosticRunner is a function that executes a diagnostic and returns the result. The function should return a map containing
+// any additional information about the diagnostic run, and a detailed error if the run failed.
+type DiagnosticRunner func(context.Context) (map[string]any, error)
+
+// Diagnostic is a struct that contains the basic information about a registed diagnostic within a DiagnosticService.
+type Diagnostic struct {
+	// Name of the diagnostic that is registered.
+	Name string
+
+	// Description of the diagnostic that is registered.
+	Description string
+
+	// Category of the diagnostic that is registered.
+	Category string
+}
+
+// DiagnosticService is an interface that defines the basic contract for a service that registers and runs diagnostics on demand and provides
+// the results.
+type DiagnosticService interface {
+	// Register registers a new diagnostic runner implementation with the service that will run the next time diagnostics are requested.
+	// An error is returned if a runner failed to register. Note that category _and_ name must be a unique combination.
+	Register(name, description, category string, runner DiagnosticRunner) error
+
+	// Unregister unregisters a diagnostic runner implementation with the service. True is returned if the runner was unregistered successfully,
+	// false otherwise.
+	Unregister(name, category string) bool
+
+	// Run executes all registered diagnostics and returns the results.
+	Run(ctx context.Context) []*DiagnosticResult
+
+	// RunCategory executes all registered diagnostics in the provided category.
+	RunCategory(ctx context.Context, category string) []*DiagnosticResult
+
+	// RunDiagnostic executes a specific diagnostic by category and name. If the diagnostic does not exist, nil is returned.
+	RunDiagnostic(ctx context.Context, category, name string) *DiagnosticResult
+
+	// Diagnostics returns a list of all registered diagnostics.
+	Diagnostics() []Diagnostic
+
+	// Total returns the total number of registered diagnostics.
+	Total() int
+}

+ 20 - 0
core/pkg/diagnostics/exporter/controller.go

@@ -0,0 +1,20 @@
+package exporter
+
+import (
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/storage"
+)
+
+// NewDiagnosticsExportController creates a new EventExportController for DiagnosticsRunReport events.
+func NewDiagnosticsExportController(
+	clusterId string,
+	applicationName string,
+	store storage.Storage,
+	service diagnostics.DiagnosticService,
+) *exporter.EventExportController[diagnostics.DiagnosticsRunReport] {
+	return exporter.NewEventExportController(
+		NewDiagnosticSource(service),
+		NewDiagnosticExporter(clusterId, applicationName, store),
+	)
+}

+ 11 - 0
core/pkg/diagnostics/exporter/encoder.go

@@ -0,0 +1,11 @@
+package exporter
+
+import (
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+	"github.com/opencost/opencost/core/pkg/exporter"
+)
+
+// NewDiagnosticsEncoder returns a JSON encoder used to encode DiagnosticsRunReport events.
+func NewDiagnosticsEncoder() exporter.Encoder[diagnostics.DiagnosticsRunReport] {
+	return exporter.NewJSONEncoder[diagnostics.DiagnosticsRunReport]()
+}

+ 24 - 0
core/pkg/diagnostics/exporter/exporter.go

@@ -0,0 +1,24 @@
+package exporter
+
+import (
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/storage"
+)
+
+// NewDiagnosticExporter creates a new `StorageExporter[DiagnosticsRunReport]` instance for exporting diagnostic run events.
+func NewDiagnosticExporter(clusterId string, applicationName string, storage storage.Storage) exporter.EventExporter[diagnostics.DiagnosticsRunReport] {
+	pathing, err := pathing.NewEventStoragePathFormatter("federated", clusterId, diagnostics.DiagnosticsEventName, applicationName)
+	if err != nil {
+		log.Errorf("failed to create pathing formatter: %v", err)
+		return nil
+	}
+
+	return exporter.NewEventStorageExporter(
+		pathing,
+		NewDiagnosticsEncoder(),
+		storage,
+	)
+}

+ 40 - 0
core/pkg/diagnostics/exporter/source.go

@@ -0,0 +1,40 @@
+package exporter
+
+import (
+	"context"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+)
+
+// DiagnosticSource is an `export.ExportSource` implementation that provides the basic data for a `DiagnosticResult` payload.
+type DiagnosticSource struct {
+	diagnosticService diagnostics.DiagnosticService
+}
+
+// NewDiagnosticSource creates a new `DiagnosticSource` instance. It accepts the `DiagnosticService` implementation
+// that will be used to retrieve the diagnostic results.
+func NewDiagnosticSource(diagnosticService diagnostics.DiagnosticService) *DiagnosticSource {
+	return &DiagnosticSource{
+		diagnosticService: diagnosticService,
+	}
+}
+
+// Make creates a new `DiagnosticsRunReport` instance with the provided current time.
+func (ds *DiagnosticSource) Make(t time.Time) *diagnostics.DiagnosticsRunReport {
+	ctx := context.Background()
+
+	// returning nil will prevent export -- skip for 0 registered diagnostics
+	if ds.diagnosticService.Total() == 0 {
+		return nil
+	}
+
+	return &diagnostics.DiagnosticsRunReport{
+		StartTime: t,
+		Results:   ds.diagnosticService.Run(ctx),
+	}
+}
+
+func (ds *DiagnosticSource) Name() string {
+	return diagnostics.DiagnosticsEventName + "-source"
+}

+ 187 - 0
core/pkg/diagnostics/service.go

@@ -0,0 +1,187 @@
+package diagnostics
+
+import (
+	"context"
+	"fmt"
+	"iter"
+	"maps"
+	"slices"
+	"sync"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/opencost/opencost/core/pkg/util/maputil"
+	"github.com/opencost/opencost/core/pkg/util/worker"
+)
+
+// basic composite type for diagnostics and the runner function
+type runner struct {
+	diagnostic Diagnostic
+	run        DiagnosticRunner
+}
+
+// OpencostDiagnosticsService is an implementation of the `DiagnosticService` contract that provides concurrent diagnostic
+// execution and result collection.
+type OpencostDiagnosticService struct {
+	lock    sync.RWMutex
+	runners map[string]map[string]*runner
+	count   int
+}
+
+func NewDiagnosticService() DiagnosticService {
+	return &OpencostDiagnosticService{
+		runners: make(map[string]map[string]*runner),
+		count:   0,
+	}
+}
+
+// Register registers a new diagnostic runner implementation with the service that will run the next time diagnostics are requested.
+// An error is returned if a runner failed to register. Note that category _and_ name must be a unique combination.
+func (ocds *OpencostDiagnosticService) Register(name string, description string, category string, r DiagnosticRunner) error {
+	ocds.lock.Lock()
+	defer ocds.lock.Unlock()
+
+	categoryRunners, exists := ocds.runners[category]
+	if !exists {
+		categoryRunners = make(map[string]*runner)
+		ocds.runners[category] = categoryRunners
+	}
+
+	if _, exists := categoryRunners[name]; exists {
+		return fmt.Errorf("runner with name %s already exists in category %s", name, category)
+	}
+
+	categoryRunners[name] = &runner{
+		diagnostic: Diagnostic{
+			Name:        name,
+			Description: description,
+			Category:    category,
+		},
+		run: r,
+	}
+
+	ocds.count += 1
+
+	return nil
+}
+
+// Unregister unregisters a diagnostic runner implementation with the service. True is returned if the runner was unregistered successfully,
+// false otherwise.
+func (ocds *OpencostDiagnosticService) Unregister(name string, category string) bool {
+	ocds.lock.Lock()
+	defer ocds.lock.Unlock()
+
+	categoryRunners, exists := ocds.runners[category]
+	if !exists {
+		return false
+	}
+
+	if _, exists := categoryRunners[name]; !exists {
+		return false
+	}
+
+	delete(categoryRunners, name)
+	if len(categoryRunners) == 0 {
+		delete(ocds.runners, category)
+	}
+
+	ocds.count -= 1
+
+	return true
+}
+
+// Run executes all registered diagnostics and returns the results.
+func (ocds *OpencostDiagnosticService) Run(ctx context.Context) []*DiagnosticResult {
+	ocds.lock.RLock()
+	defer ocds.lock.RUnlock()
+
+	return runAll(ctx, maputil.Flatten(ocds.runners))
+}
+
+// RunCategory executes all registered diagnostics in the provided category.
+func (ocds *OpencostDiagnosticService) RunCategory(ctx context.Context, category string) []*DiagnosticResult {
+	ocds.lock.RLock()
+	defer ocds.lock.RUnlock()
+
+	categoryRunners, exists := ocds.runners[category]
+	if !exists {
+		return nil
+	}
+
+	return runAll(ctx, maps.Values(categoryRunners))
+}
+
+// RunDiagnostic executes a specific diagnostic by category and name. If the diagnostic does not exist, nil is returned.
+func (ocds *OpencostDiagnosticService) RunDiagnostic(ctx context.Context, category, name string) *DiagnosticResult {
+	ocds.lock.RLock()
+	defer ocds.lock.RUnlock()
+
+	categoryRunners, exists := ocds.runners[category]
+	if !exists {
+		return nil
+	}
+
+	r, exists := categoryRunners[name]
+	if !exists {
+		return nil
+	}
+
+	diagRunner := diagRunnerFor(ctx)
+
+	return diagRunner(r)
+}
+
+// runAll executes all runners in the provided iterator with a specific worker pool size,
+// and returns the results when all diagnostic runners have completed.
+func runAll(ctx context.Context, runners iter.Seq[*runner]) []*DiagnosticResult {
+	allContext, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	return worker.ConcurrentIterCollect(5, diagRunnerFor(allContext), runners)
+}
+
+// diagRunnerFor returns a diagnostic runner function that executes the diagnostic and creates the DiagnosticResult
+// leveraging the provided context as a parent.
+func diagRunnerFor(ctx context.Context) func(*runner) *DiagnosticResult {
+	return func(r *runner) *DiagnosticResult {
+		result := &DiagnosticResult{
+			ID:          uuid.Must(uuid.NewV7()).String(),
+			Name:        r.diagnostic.Name,
+			Description: r.diagnostic.Description,
+			Category:    r.diagnostic.Category,
+		}
+
+		c, cancelDiag := context.WithTimeout(ctx, 5*time.Second)
+		defer cancelDiag()
+
+		details, err := r.run(c)
+		if err != nil {
+			result.Error = err.Error()
+		} else {
+			result.Details = details
+		}
+
+		result.Timestamp = time.Now().UTC()
+		return result
+	}
+}
+
+// Diagnostics returns a list of all registered diagnostics.
+func (ocds *OpencostDiagnosticService) Diagnostics() []Diagnostic {
+	ocds.lock.RLock()
+	defer ocds.lock.RUnlock()
+
+	diagnostics := maputil.FlatMap(ocds.runners, func(r *runner) Diagnostic {
+		return r.diagnostic
+	})
+
+	return slices.Collect(diagnostics)
+}
+
+// Total returns the total number of registered diagnostics.
+func (ocds *OpencostDiagnosticService) Total() int {
+	ocds.lock.RLock()
+	defer ocds.lock.RUnlock()
+
+	return ocds.count
+}

+ 430 - 0
core/pkg/diagnostics/service_test.go

@@ -0,0 +1,430 @@
+package diagnostics
+
+import (
+	"cmp"
+	"context"
+	"fmt"
+	"slices"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/util/json"
+)
+
+const (
+	TestDiagnosticNameA       = "TestDiagnosticA"
+	TestDiagnosticNameB       = "TestDiagnosticB"
+	TestDiagnosticNameC       = "TestDiagnosticC"
+	TestDiagnosticNameD       = "TestDiagnosticD"
+	TestDiagnosticNameE       = "TestDiagnosticE"
+	TestDiagnosticNameF       = "TestDiagnosticF"
+	TestDiagnosticNameTimeout = "TestDiagnosticTimeout"
+
+	TestDiagnosticDescriptionA       = "Diagnostic A Description..."
+	TestDiagnosticDescriptionB       = "Diagnostic B Description..."
+	TestDiagnosticDescriptionC       = "Diagnostic C Description..."
+	TestDiagnosticDescriptionD       = "Diagnostic D Description..."
+	TestDiagnosticDescriptionE       = "Diagnostic E Description..."
+	TestDiagnosticDescriptionF       = "Diagnostic F Description..."
+	TestDiagnosticDescriptionTimeout = "Diagnostic Timeout will run for longer than 5 seconds..."
+
+	TestDiagnosticCategoryBlue  = "TestCategoryBlue"
+	TestDiagnosticCategoryRed   = "TestCategoryRed"
+	TestDiagnosticCategoryGreen = "TestCategoryGreen"
+)
+
+// TestDiagnostic is a general structure used to capture test diagnostic data
+type TestDiagnostic struct {
+	Name        string
+	Description string
+	Category    string
+	Run         DiagnosticRunner
+}
+
+// generate a runner func that will run for the provided duration and return a map with the key: "test"
+// and the value of testName provided.
+func runnerFor(testName string, duration time.Duration) DiagnosticRunner {
+	return func(ctx context.Context) (map[string]any, error) {
+		fmt.Printf("Running Diagnostic: %s\n", testName)
+		defer fmt.Printf("Finished Diagnostic: %s\n", testName)
+
+		select {
+		case <-ctx.Done():
+			fmt.Printf("context cancelled: %v\n", ctx.Err())
+			return nil, ctx.Err()
+		case <-time.After(duration):
+			return map[string]any{
+				"test": testName,
+			}, nil
+		}
+	}
+}
+
+var (
+	TestDiagnosticA = TestDiagnostic{
+		Name:        TestDiagnosticNameA,
+		Description: TestDiagnosticDescriptionA,
+		Category:    TestDiagnosticCategoryRed,
+		Run:         runnerFor(TestDiagnosticNameA, 250*time.Millisecond),
+	}
+	TestDiagnosticB = TestDiagnostic{
+		Name:        TestDiagnosticNameB,
+		Description: TestDiagnosticDescriptionB,
+		Category:    TestDiagnosticCategoryRed,
+		Run:         runnerFor(TestDiagnosticNameB, 150*time.Millisecond),
+	}
+	TestDiagnosticC = TestDiagnostic{
+		Name:        TestDiagnosticNameC,
+		Description: TestDiagnosticDescriptionC,
+		Category:    TestDiagnosticCategoryBlue,
+		Run:         runnerFor(TestDiagnosticNameC, 350*time.Millisecond),
+	}
+	TestDiagnosticD = TestDiagnostic{
+		Name:        TestDiagnosticNameD,
+		Description: TestDiagnosticDescriptionD,
+		Category:    TestDiagnosticCategoryBlue,
+		Run:         runnerFor(TestDiagnosticNameD, 450*time.Millisecond),
+	}
+	TestDiagnosticE = TestDiagnostic{
+		Name:        TestDiagnosticNameE,
+		Description: TestDiagnosticDescriptionE,
+		Category:    TestDiagnosticCategoryGreen,
+		Run:         runnerFor(TestDiagnosticNameE, 550*time.Millisecond),
+	}
+	TestDiagnosticF = TestDiagnostic{
+		Name:        TestDiagnosticNameF,
+		Description: TestDiagnosticDescriptionF,
+		Category:    TestDiagnosticCategoryGreen,
+		Run:         runnerFor(TestDiagnosticNameF, 650*time.Millisecond),
+	}
+	TestDiagnosticTimeout = TestDiagnostic{
+		Name:        TestDiagnosticNameTimeout,
+		Description: TestDiagnosticDescriptionTimeout,
+		Category:    TestDiagnosticCategoryGreen,
+		Run:         runnerFor(TestDiagnosticNameTimeout, 6*time.Second),
+	}
+)
+
+func TestDiagnosticsRegisterAndRun(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticD,
+		TestDiagnosticE,
+		TestDiagnosticF,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	// Register a duplicate diagnostic and expect an error
+	err := d.Register(TestDiagnosticA.Name, TestDiagnosticA.Description, TestDiagnosticA.Category, TestDiagnosticA.Run)
+	if err == nil {
+		t.Fatalf("expected error when registering duplicate diagnostic %s", TestDiagnosticA.Name)
+	}
+
+	c := context.Background()
+	results := d.Run(c)
+
+	if len(results) != len(diags) {
+		t.Fatalf("expected %d results, got %d", len(diags), len(results))
+	}
+
+	for _, result := range results {
+		if result.Error != "" {
+			t.Errorf("expected no error, got %s", result.Error)
+		}
+
+		if result.Category == "" {
+			t.Errorf("expected category, got empty")
+		}
+
+		if result.Name == "" {
+			t.Errorf("expected name, got empty")
+		}
+
+		if result.Timestamp.IsZero() {
+			t.Errorf("expected timestamp, got zero")
+		}
+
+		if result.Details == nil {
+			t.Errorf("expected details, got nil")
+		}
+
+		if result.Details["test"] != result.Name {
+			t.Errorf("expected test name %s, got %s", result.Name, result.Details["test"])
+		}
+
+		j, err := json.Marshal(result)
+		if err != nil {
+			t.Errorf("failed to marshal result: %v", err)
+		}
+		js := string(j)
+		if js == "" {
+			t.Errorf("expected non-empty JSON, got empty")
+		}
+
+		t.Logf("%s", js)
+	}
+}
+
+func TestDiagnosticsServiceTimeout(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticTimeout,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	c := context.Background()
+	results := d.Run(c)
+
+	if len(results) != len(diags) {
+		t.Fatalf("expected %d results, got %d", len(diags), len(results))
+	}
+
+	foundTimeoutDiagnostic := false
+
+	for _, result := range results {
+		if result.Name == TestDiagnosticNameTimeout {
+			foundTimeoutDiagnostic = true
+			if result.Error == "" {
+				t.Errorf("expected timeout error, but got empty error")
+			} else {
+				t.Logf("Diagnostic %s/%s completed with error as expected: %s", result.Category, result.Name, result.Error)
+			}
+		} else {
+			t.Logf("Diagnostic %s/%s completed successfully", result.Category, result.Name)
+		}
+	}
+
+	if !foundTimeoutDiagnostic {
+		t.Errorf("expected to find timeout diagnostic, but it was not found")
+	}
+}
+
+func TestDiagnosticsList(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticD,
+		TestDiagnosticE,
+		TestDiagnosticF,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	diagList := d.Diagnostics()
+	slices.SortFunc(diagList, func(a, b Diagnostic) int {
+		return cmp.Compare(a.Category+"/"+a.Name, b.Category+"/"+b.Name)
+	})
+
+	slices.SortFunc(diags, func(a, b TestDiagnostic) int {
+		return cmp.Compare(a.Category+"/"+a.Name, b.Category+"/"+b.Name)
+	})
+
+	if !slices.EqualFunc(diags, diagList, isEqual) {
+		t.Errorf("expected diagnostics list to match registered diagnostics")
+	}
+
+	for _, diagItem := range diagList {
+		t.Logf("Diagnostic: %+v", diagItem)
+	}
+}
+
+func TestUnregisterDiagnostic(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticD,
+		TestDiagnosticE,
+		TestDiagnosticF,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	if !d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
+		t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameA)
+	}
+
+	if d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
+		t.Errorf("unregistering diagnostic %s/%s again should fail", TestDiagnosticCategoryRed, TestDiagnosticNameA)
+	}
+
+	if d.Unregister(TestDiagnosticNameB, "nonexistent") {
+		t.Errorf("unregistering nonexistent diagnostic should fail")
+	}
+
+	results := d.Run(context.Background())
+	if len(results) != len(diags)-1 {
+		t.Fatalf("expected %d results, got %d", len(diags)-1, len(results))
+	}
+
+	for _, result := range results {
+		if result.Name == TestDiagnosticNameA {
+			t.Errorf("expected diagnostic %s/%s to be unregistered", TestDiagnosticCategoryRed, TestDiagnosticNameA)
+		}
+	}
+}
+
+func TestUnregisterAllFromCategory(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticD,
+		TestDiagnosticE,
+		TestDiagnosticF,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	if !d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
+		t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameA)
+	}
+
+	if !d.Unregister(TestDiagnosticNameB, TestDiagnosticCategoryRed) {
+		t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameB)
+	}
+
+	results := d.RunCategory(context.Background(), TestDiagnosticCategoryRed)
+	if len(results) != 0 {
+		t.Fatalf("expected 0 results for category %s, got %d", TestDiagnosticCategoryRed, len(results))
+	}
+}
+
+func TestRunCategoryDiagnostics(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticD,
+		TestDiagnosticE,
+		TestDiagnosticF,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	c := context.Background()
+	results := d.RunCategory(c, TestDiagnosticCategoryBlue)
+
+	if len(results) != 2 {
+		t.Fatalf("expected 2 results for category %s, got %d", TestDiagnosticCategoryBlue, len(results))
+	}
+
+	for _, result := range results {
+		if result.Category != TestDiagnosticCategoryBlue {
+			t.Errorf("expected category %s, got %s", TestDiagnosticCategoryBlue, result.Category)
+		}
+	}
+}
+
+func TestRunSingleDiagnostic(t *testing.T) {
+	t.Parallel()
+
+	d := NewDiagnosticService()
+
+	diags := []TestDiagnostic{
+		TestDiagnosticA,
+		TestDiagnosticB,
+		TestDiagnosticC,
+		TestDiagnosticD,
+		TestDiagnosticE,
+		TestDiagnosticF,
+	}
+
+	for _, diag := range diags {
+		if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
+			t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
+		}
+	}
+
+	c := context.Background()
+	result := d.RunDiagnostic(c, TestDiagnosticCategoryGreen, TestDiagnosticNameF)
+
+	if result == nil {
+		t.Fatalf("expected a result for diagnostic %s, got nil", TestDiagnosticNameF)
+	}
+
+	if result.Name != TestDiagnosticNameF {
+		t.Errorf("expected name %s, got %s", TestDiagnosticNameF, result.Name)
+	}
+
+	// Run category without name
+	result = d.RunDiagnostic(c, TestDiagnosticCategoryGreen, "not-a-valid-diagnostic-name")
+	if result != nil {
+		t.Fatalf("expected nil result for invalid diagnostic name, got %v", result)
+	}
+
+	// Run without category
+	result = d.RunDiagnostic(c, "not-a-valid-category", TestDiagnosticNameF)
+	if result != nil {
+		t.Fatalf("expected nil result for invalid category, got %v", result)
+	}
+
+}
+
+func isEqual(a TestDiagnostic, b Diagnostic) bool {
+	if a.Name != b.Name {
+		return false
+	}
+	if a.Description != b.Description {
+		return false
+	}
+	if a.Category != b.Category {
+		return false
+	}
+	return true
+}

+ 0 - 0
pkg/errors/panic.go → core/pkg/errors/panic.go


+ 308 - 0
core/pkg/exporter/controller.go

@@ -0,0 +1,308 @@
+package exporter
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/util/atomic"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
+	"github.com/opencost/opencost/core/pkg/util/typeutil"
+)
+
+// ExportController is a controller interface that is responsible for exporting data on a specific interval.
+type ExportController interface {
+	// Name returns the name of the controller
+	Name() string
+
+	// Start starts a background compute processing loop, which will compute the data for the current resolution and export it
+	// on the provided interval. This function will return `true` if the loop was started successfully, and `false` if it was
+	// already running.
+	Start(interval time.Duration) bool
+
+	// Stops the compute processing loop
+	Stop()
+}
+
+// EventExportController[T] is used to export timestamped events of type T on a specific interval.
+type EventExportController[T any] struct {
+	runState atomic.AtomicRunState
+	source   ExportSource[T]
+	exporter EventExporter[T]
+	typeName string
+}
+
+// NewEventExportController creates a new `EventExportController[T]` instance which is used to export timestamped events of type T
+// on a specific interval.
+func NewEventExportController[T any](source ExportSource[T], exporter EventExporter[T]) *EventExportController[T] {
+	return &EventExportController[T]{
+		source:   source,
+		exporter: exporter,
+		typeName: reflect.TypeOf((*T)(nil)).Elem().String(),
+	}
+}
+
+// Name returns the name of the controller, which is the name of the T-type
+func (cd *EventExportController[T]) Name() string {
+	return cd.typeName
+}
+
+// Start starts a background export loop, which will create a new event instance for the current minute-truncated time
+// and export it on the provided interval. This function will return `true` if the loop was started successfully, and
+// `false` if it was already running.
+func (cd *EventExportController[T]) Start(interval time.Duration) bool {
+	cd.runState.WaitForReset()
+	if !cd.runState.Start() {
+		return false
+	}
+
+	go func() {
+		for {
+			select {
+			case <-cd.runState.OnStop():
+				cd.runState.Reset()
+				return // exit go routine
+
+			case <-time.After(interval):
+			}
+
+			// truncate the time to the second to ensure broad enough coverage for event exports
+			t := time.Now().UTC().Truncate(time.Second)
+
+			evt := cd.source.Make(t)
+			if evt == nil {
+				log.Debugf("[%s] No event data to export", cd.typeName)
+				continue
+			}
+
+			err := cd.exporter.Export(t, evt)
+			if err != nil {
+				log.Warnf("[%s] Error during Write: %s", cd.typeName, err)
+			}
+		}
+	}()
+
+	return true
+}
+
+// Stops the export loop
+func (cd *EventExportController[T]) Stop() {
+	cd.runState.Stop()
+}
+
+// ComputeExportController[T] is a controller type which leverages a `ComputeSource[T]` and `Exporter[T]`
+// to regularly compute the data for the current resolution and export it on a specific interval.
+type ComputeExportController[T any] struct {
+	runState         atomic.AtomicRunState
+	source           ComputeSource[T]
+	exporter         ComputeExporter[T]
+	resolution       time.Duration
+	sourceResolution time.Duration
+	lastExport       time.Time
+	typeName         string
+}
+
+// NewComputeExportController creates a new `ComputeExportController[T]` instance.
+func NewComputeExportController[T any](
+	source ComputeSource[T],
+	exporter ComputeExporter[T],
+	resolution time.Duration,
+	sourceResolution time.Duration,
+) *ComputeExportController[T] {
+	return &ComputeExportController[T]{
+		source:           source,
+		resolution:       resolution,
+		sourceResolution: sourceResolution,
+		exporter:         exporter,
+		typeName:         reflect.TypeOf((*T)(nil)).Elem().String(),
+	}
+}
+
+// Name returns the name of the controller, which is a combination of the type name and the resolution
+func (cd *ComputeExportController[T]) Name() string {
+	return cd.typeName + "-" + timeutil.FormatStoreResolution(cd.resolution)
+}
+
+// Start starts a background compute processing loop, which will compute the data for the current resolution and export it
+// on the provided interval. This function will return `true` if the loop was started successfully, and `false` if it was
+// already running.
+func (cd *ComputeExportController[T]) Start(interval time.Duration) bool {
+	// Before we attempt to start, we must ensure we are not in a stopping state
+	cd.runState.WaitForReset()
+
+	// This will atomically check the current state to ensure we can run, then advances the state.
+	// If the state is already started, it will return false.
+	if !cd.runState.Start() {
+		return false
+	}
+
+	// our run state is advanced, let's execute our action on the interval
+	// spawn a new goroutine which will loop and wait the interval each iteration
+	go func() {
+		for {
+			// use a select statement to receive whichever channel receives data first
+			select {
+			// if our stop channel receives data, it means we have explicitly called
+			// Stop(), and must reset our AtomicRunState to it's initial idle state
+			case <-cd.runState.OnStop():
+				cd.runState.Reset()
+				return // exit go routine
+
+			// After our interval elapses, fall through
+			case <-time.After(interval):
+			}
+
+			now := time.Now().UTC()
+			windows := cd.exportWindowsFor(now)
+
+			for _, window := range windows {
+				err := cd.export(window)
+				if err != nil {
+					// Check ErrorCollection to set Warnings and Errors
+					if source.IsErrorCollection(err) {
+						c := err.(source.QueryErrorCollection)
+						errors, warnings := c.ToErrorAndWarningStrings()
+
+						cd.logErrors(window, warnings, errors)
+						continue
+					}
+
+					log.Errorf("[%s] %s", cd.typeName, err)
+				} else {
+					cd.lastExport = now
+				}
+			}
+		}
+	}()
+
+	return true
+}
+
+// exportWindows uses the last export time to determine the current time windows to
+// export. This will, at most, return 2 windows: the previous resolution window and
+// the current resolution window.
+func (cd *ComputeExportController[T]) exportWindowsFor(now time.Time) []opencost.Window {
+	start := now.Truncate(cd.resolution)
+	end := start.Add(cd.resolution)
+
+	if cd.lastExport.IsZero() {
+		return []opencost.Window{
+			opencost.NewClosedWindow(start, end),
+		}
+	}
+
+	lastStart := cd.lastExport.Truncate(cd.resolution)
+	if lastStart.Equal(start) {
+		return []opencost.Window{
+			opencost.NewClosedWindow(start, end),
+		}
+	}
+	lastEnd := lastStart.Add(cd.resolution)
+
+	// we've identified that the last export window is not the same as the current,
+	// so we should export the previous resolution window as well as the current one
+	return []opencost.Window{
+		opencost.NewClosedWindow(lastStart, lastEnd),
+		opencost.NewClosedWindow(start, end),
+	}
+}
+
+// export computes and exports the data for a given time window
+func (cd *ComputeExportController[T]) export(window opencost.Window) error {
+	if window.IsOpen() {
+		return fmt.Errorf("window is open: %s", window.String())
+	}
+
+	start, end := *window.Start(), *window.End()
+
+	log.Debugf("[%s] Reporting for window: %s - %s", cd.typeName, start.UTC(), end.UTC())
+
+	if !cd.source.CanCompute(start, end) {
+		return fmt.Errorf("cannot compute window: [Start: %s, End: %s]", start, end)
+	}
+
+	set, err := cd.source.Compute(start, end, cd.sourceResolution)
+	// all errors but NoDataError are considered a halt to the export
+	if err != nil && !source.IsNoDataError(err) {
+		return err
+	}
+
+	log.Debugf("[%s] Exporting data for window: %s - %s", cd.typeName, start.UTC(), end.UTC())
+	err = cd.exporter.Export(window, set)
+	if err != nil {
+		return fmt.Errorf("write error: %w", err)
+	}
+
+	return nil
+}
+
+// Stops the compute processing loop
+func (cd *ComputeExportController[T]) Stop() {
+	cd.runState.Stop()
+}
+
+// temporary
+func (cd *ComputeExportController[T]) logErrors(window opencost.Window, warnings []string, errors []string) {
+	start, end := window.Start(), window.End()
+	for _, w := range warnings {
+		log.Warnf("[%s] (%s-%s) %s", cd.typeName, start.Format(time.RFC3339), end.Format(time.RFC3339), w)
+	}
+
+	for _, e := range errors {
+		log.Errorf("[%s] (%s-%s) %s", cd.typeName, start.Format(time.RFC3339), end.Format(time.RFC3339), e)
+	}
+}
+
+type ComputeExportControllerGroup[T any] struct {
+	controllers []*ComputeExportController[T]
+}
+
+func NewComputeExportControllerGroup[T any](controllers ...*ComputeExportController[T]) *ComputeExportControllerGroup[T] {
+	return &ComputeExportControllerGroup[T]{controllers: controllers}
+}
+
+func (g *ComputeExportControllerGroup[T]) Name() string {
+	var sb strings.Builder
+	sb.WriteRune('[')
+	for i, c := range g.controllers {
+		if i > 0 {
+			sb.WriteRune('/')
+		}
+		sb.WriteString(c.Name())
+	}
+	sb.WriteRune(']')
+	return sb.String()
+}
+
+func (g *ComputeExportControllerGroup[T]) Start(interval time.Duration) bool {
+	if len(g.controllers) == 0 {
+		log.Warnf("ComputeExportControllerGroup[%s] has no controllers to start", typeutil.TypeOf[T]())
+		return false
+	}
+
+	for _, c := range g.controllers {
+		if !c.Start(interval) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (g *ComputeExportControllerGroup[T]) Stop() {
+	for _, c := range g.controllers {
+		c.Stop()
+	}
+}
+
+func (g *ComputeExportControllerGroup[T]) Resolutions() []time.Duration {
+	resolutions := make([]time.Duration, 0, len(g.controllers))
+	for _, c := range g.controllers {
+		resolutions = append(resolutions, c.resolution)
+	}
+	return resolutions
+}

+ 106 - 0
core/pkg/exporter/encoder.go

@@ -0,0 +1,106 @@
+package exporter
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding"
+
+	"github.com/opencost/opencost/core/pkg/util/json"
+)
+
+// Encoder[T] is a generic interface for encoding an instance of a T type into a byte slice.
+type Encoder[T any] interface {
+	Encode(*T) ([]byte, error)
+
+	// FileExt returns the file extension for the encoded data. This can be used by a pathing strategy
+	// to append the file extension when exporting the data. Returning an empty string will typically
+	// omit the file extension completely.
+	FileExt() string
+}
+
+// BinaryMarshalerPtr[T] is a generic constraint to ensure types passed to the encoder implement
+// encoding.BinaryMarshaler and are pointers to T.
+type BinaryMarshalerPtr[T any] interface {
+	encoding.BinaryMarshaler
+	*T
+}
+
+// BingenEncoder[T, U] is a generic encoder that uses the BinaryMarshaler interface to encode data.
+// It supports any type T that implements the encoding.BinaryMarshaler interface.
+type BingenEncoder[T any, U BinaryMarshalerPtr[T]] struct{}
+
+// NewBingenEncoder creates an `Encoder[T]` implementation which supports binary encoding for the `T`
+// type.
+func NewBingenEncoder[T any, U BinaryMarshalerPtr[T]]() Encoder[T] {
+	return new(BingenEncoder[T, U])
+}
+
+// Encode encodes the provided data of type T into a byte slice using the BinaryMarshaler interface.
+func (b *BingenEncoder[T, U]) Encode(data *T) ([]byte, error) {
+	var bingenData U = data
+	return bingenData.MarshalBinary()
+}
+
+// FileExt returns the file extension for the encoded data. In this case, it returns an empty string
+// to indicate that there is no specific file extension for the binary encoded data.
+func (b *BingenEncoder[T, U]) FileExt() string {
+	return ""
+}
+
+// JSONEncoder[T] is a generic encoder that uses the JSON encoding format to encode data.
+type JSONEncoder[T any] struct{}
+
+// NewJSONEncoder creates an `Encoder[T]` implementation which supports JSON encoding for the `T`
+// type.
+func NewJSONEncoder[T any]() Encoder[T] {
+	return new(JSONEncoder[T])
+}
+
+// Encode encodes the provided data of type T into a byte slice using JSON encoding.
+func (j *JSONEncoder[T]) Encode(data *T) ([]byte, error) {
+	return json.Marshal(data)
+}
+
+// FileExt returns the file extension for the encoded data. In this case, it returns "json" to indicate
+// that the data is in JSON format.
+func (j *JSONEncoder[T]) FileExt() string {
+	return "json"
+}
+
+type GZipEncoder[T any] struct {
+	encoder Encoder[T]
+}
+
+// NewGZipEncoder creates a new GZip encoder which wraps the provided encoder.
+// The encoder is used to encode the data before compressing it with GZip.
+func NewGZipEncoder[T any](encoder Encoder[T]) Encoder[T] {
+	return &GZipEncoder[T]{
+		encoder: encoder,
+	}
+}
+
+// Encode encodes the provided data of type T into a byte slice using JSON encoding.
+func (gz *GZipEncoder[T]) Encode(data *T) ([]byte, error) {
+	encoded, err := gz.encoder.Encode(data)
+	if err != nil {
+		return nil, err
+	}
+
+	var buf bytes.Buffer
+
+	gzWriter, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
+	if err != nil {
+		return nil, err
+	}
+
+	gzWriter.Write(encoded)
+	gzWriter.Close()
+
+	return buf.Bytes(), nil
+}
+
+// FileExt returns the file extension for the encoded data. In this case, it returns the wrapped encoder's
+// file extension with ".gz" appended to indicate that the data is compressed with GZip.
+func (gz *GZipEncoder[T]) FileExt() string {
+	return gz.encoder.FileExt() + ".gz"
+}

+ 130 - 0
core/pkg/exporter/exporter.go

@@ -0,0 +1,130 @@
+package exporter
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
+	"github.com/opencost/opencost/core/pkg/exporter/validator"
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/storage"
+)
+
+// Exporter[T] is a generic interface for exporting T instances to a specific storage destination.
+type Exporter[TimeUnit any, T any] interface {
+	// Export performs the export operation for the provided data.
+	Export(time TimeUnit, data *T) error
+}
+
+// EventExporter[T] is an alias type of an Exporter[time.Time, T] that writes data that is timestamped.
+type EventExporter[T any] Exporter[time.Time, T]
+
+// ComputeExporter[T] is an alias type of an Exporter[opencost.Window, T] that writes data for a specific window.
+type ComputeExporter[T any] Exporter[opencost.Window, T]
+
+// EventStorageExporter[T] is an implementation of an Exporter[T] that writes data to a storage backend using
+// the `github.com/opencost/opencost/core/pkg/storage` package, a pathing strategy, and an encoder.
+type EventStorageExporter[T any] struct {
+	paths   pathing.StoragePathFormatter[time.Time]
+	encoder Encoder[T]
+	storage storage.Storage
+}
+
+// NewEventStorageExporter creates a new StorageExporter instance, which is responsible for exporting data to a storage backend.
+// It uses a pathing strategy to determine the storage location, an encoder to convert the data to binary format, and
+// a storage backend to write the data.
+func NewEventStorageExporter[T any](
+	paths pathing.StoragePathFormatter[time.Time],
+	encoder Encoder[T],
+	storage storage.Storage,
+) EventExporter[T] {
+	return &EventStorageExporter[T]{
+		paths:   paths,
+		encoder: encoder,
+		storage: storage,
+	}
+}
+
+// Export performs the export operation for the provided data. It encodes the data using the encoder and writes it to
+// the storage backend using the pathing strategy.
+func (se *EventStorageExporter[T]) Export(t time.Time, data *T) error {
+	path := se.paths.ToFullPath("", t, se.encoder.FileExt())
+
+	bin, err := se.encoder.Encode(data)
+	if err != nil {
+		return fmt.Errorf("failed to encode data: %w", err)
+	}
+
+	log.Debugf("writing new binary data to storage %s", path)
+	err = se.storage.Write(path, bin)
+	if err != nil {
+		return fmt.Errorf("failed to write binary data to file '%s': %w", path, err)
+	}
+
+	return nil
+}
+
+// ComputeStorageExporter[T] is an implementation of ComputeExporter[T] that writes data to a storage backend using
+// `github.com/opencost/opencost/core/pkg/storage`, a pathing strategy, and an encoder.
+type ComputeStorageExporter[T any] struct {
+	resolution time.Duration
+	paths      pathing.StoragePathFormatter[opencost.Window]
+	encoder    Encoder[T]
+	storage    storage.Storage
+	validator  validator.ExportValidator[T]
+}
+
+// NewComputeStorageExporter creates a new ComputeStorageExporter instance, which is responsible for exporting
+// data for a specific window to a storage backend. It uses a pathing strategy to determine the storage location,
+// an encoder to convert the data to binary format, and a validator to check the data before export. The pipeline
+// name and resolution are also provided to help identify the data being exported.
+func NewComputeStorageExporter[T any](
+	paths pathing.StoragePathFormatter[opencost.Window],
+	encoder Encoder[T],
+	storage storage.Storage,
+	validator validator.ExportValidator[T],
+) ComputeExporter[T] {
+	return &ComputeStorageExporter[T]{
+		paths:     paths,
+		encoder:   encoder,
+		storage:   storage,
+		validator: validator,
+	}
+}
+
+// Export performs validation on the provided window and data, determines if it should overwrite existing data,
+// and stores the data in the location specified by the pathing formatter.
+func (se *ComputeStorageExporter[T]) Export(window opencost.Window, data *T) error {
+	if se.validator != nil {
+		err := se.validator.Validate(window, data)
+		if err != nil {
+			return fmt.Errorf("failed to validate data: %w", err)
+		}
+	}
+
+	path := se.paths.ToFullPath("", window, se.encoder.FileExt())
+
+	currentExists, err := se.storage.Exists(path)
+	if err != nil {
+		return fmt.Errorf("unable to check for existing data from storage path: %w", err)
+	}
+
+	if currentExists && se.validator != nil && !se.validator.IsOverwrite(data) {
+		log.Debugf("retaining existing data in storage at path: %s", path)
+		return nil
+	}
+
+	bin, err := se.encoder.Encode(data)
+	if err != nil {
+		return fmt.Errorf("failed to encode data: %w", err)
+	}
+
+	log.Debugf("writing new binary data to storage %s", path)
+	err = se.storage.Write(path, bin)
+	if err != nil {
+		return fmt.Errorf("failed to write binary data to file '%s': %w", path, err)
+	}
+
+	return nil
+}

+ 111 - 0
core/pkg/exporter/exporter_test.go

@@ -0,0 +1,111 @@
+package exporter
+
+import (
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
+	"github.com/opencost/opencost/core/pkg/exporter/validator"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/pipelines"
+	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/json"
+)
+
+const (
+	TestClusterId = "test-cluster"
+	TestEventName = "test-event-path"
+)
+
+type TestData struct {
+	Message string `json:"message"`
+}
+
+func TestStorageExporters(t *testing.T) {
+	t.Run("test event storage exporter", func(t *testing.T) {
+		store := storage.NewMemoryStorage()
+		p, err := pathing.NewEventStoragePathFormatter("federated", TestClusterId, TestEventName)
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
+		encoder := NewJSONEncoder[TestData]()
+		export := NewEventStorageExporter(p, encoder, store)
+
+		ts := time.Now().UTC().Truncate(time.Minute)
+
+		export.Export(ts, &TestData{
+			Message: "TestMessage-1",
+		})
+
+		expectedPath := p.ToFullPath("", ts, "json")
+		t.Logf("expected path: %s", expectedPath)
+
+		data, err := store.Read(expectedPath)
+		if err != nil {
+			t.Fatalf("failed to read data from store: %v", err)
+		}
+
+		if len(data) == 0 {
+			t.Fatalf("expected data to be non-empty, got empty")
+		}
+
+		t.Logf("Data: %s", string(data))
+
+		var td *TestData = new(TestData)
+		if err := json.Unmarshal(data, td); err != nil {
+			t.Fatalf("failed to unmarshal data: %v", err)
+		}
+
+		if td.Message != "TestMessage-1" {
+			t.Fatalf("expected message to be 'TestMessage-1', got '%s'", td.Message)
+		}
+	})
+
+	t.Run("test compute storage exporter", func(t *testing.T) {
+		res := 24 * time.Hour
+		store := storage.NewMemoryStorage()
+		p, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AllocationPipelineName, &res)
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
+		encoder := NewBingenEncoder[opencost.AllocationSet]()
+		export := NewComputeStorageExporter[opencost.AllocationSet](
+			p,
+			encoder,
+			store,
+			validator.NewSetValidator[opencost.AllocationSet](24*time.Hour),
+		)
+
+		start := time.Now().UTC().Truncate(res)
+		end := start.Add(res)
+
+		toExport := opencost.GenerateMockAllocationSet(start)
+		err = export.Export(opencost.NewClosedWindow(start, end), toExport)
+		if err != nil {
+			t.Fatalf("failed to export data: %v", err)
+		}
+
+		expectedPath := p.ToFullPath("", opencost.NewClosedWindow(start, end), "")
+
+		data, err := store.Read(expectedPath)
+		if err != nil {
+			t.Fatalf("failed to read data from store: %v", err)
+		}
+
+		if len(data) == 0 {
+			t.Fatalf("expected data to be non-empty, got empty")
+		}
+
+		var as *opencost.AllocationSet = new(opencost.AllocationSet)
+		err = as.UnmarshalBinary(data)
+		if err != nil {
+			t.Fatalf("failed to unmarshal data: %v", err)
+		}
+
+		if as.IsEmpty() {
+			t.Fatalf("expected allocation set to be non-empty, got empty")
+		}
+	})
+}

+ 99 - 0
core/pkg/exporter/pathing/bingenpath.go

@@ -0,0 +1,99 @@
+package pathing
+
+import (
+	"fmt"
+	"path"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/exporter/pathing/pathutils"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
+)
+
+const (
+	baseStorageDir string = "etl/bingen"
+)
+
+// BingenStoragePathFormatter is an implementation of the StoragePathFormatter interface for
+// a cluster separated storage path of the format:
+//
+//	<root>/federated/<cluster>/etl/bingen/<pipeline>/<resolution>/<epoch-start>-<epoch-end>
+type BingenStoragePathFormatter struct {
+	rootDir    string
+	clusterId  string
+	pipeline   string
+	resolution string
+}
+
+// NewBingenStoragePathFormatter creates a StoragePathFormatter for a cluster separated storage path
+// with the given root directory, cluster id, pipeline, and resolution. To omit the resolution directory
+// structure, provide a `nil` resolution.
+func NewBingenStoragePathFormatter(rootDir, clusterId, pipeline string, resolution *time.Duration) (StoragePathFormatter[opencost.Window], error) {
+	res := "."
+	if resolution != nil {
+		res = timeutil.FormatStoreResolution(*resolution)
+	}
+
+	if clusterId == "" {
+		return nil, fmt.Errorf("cluster id cannot be empty")
+	}
+
+	if pipeline == "" {
+		return nil, fmt.Errorf("pipeline cannot be empty")
+	}
+
+	return &BingenStoragePathFormatter{
+		rootDir:    rootDir,
+		clusterId:  clusterId,
+		pipeline:   pipeline,
+		resolution: res,
+	}, nil
+}
+
+// RootDir returns the root directory of the storage path formatter.
+func (bsf *BingenStoragePathFormatter) RootDir() string {
+	return bsf.rootDir
+}
+
+// ToFullPath returns the full path to a file name within the storage directory using the format:
+//
+//	<root>/federated/<cluster>/etl/bingen/<pipeline>/<resolution>/<prefix>.<start-epoch>-<end-epoch>
+func (bsf *BingenStoragePathFormatter) ToFullPath(prefix string, window opencost.Window, fileExt string) string {
+	fileName := toBingenFileName(prefix, window, fileExt)
+
+	return path.Join(
+		bsf.rootDir,
+		bsf.clusterId,
+		baseStorageDir,
+		bsf.pipeline,
+		bsf.resolution,
+		fileName,
+	)
+}
+
+// toBingenFileName formats the file name as <prefix>.<start-epoch>-<end-epoch> if a prefix is non-empty.
+// If prefix is an empty string, then just the format <start-epoch>-<end-epoch> is returned.
+func toBingenFileName(prefix string, window opencost.Window, fileExt string) string {
+	start, end := derefTimeOrZero(window.Start()), derefTimeOrZero(window.End())
+
+	suffix := pathutils.FormatEpochRange(start, end)
+	if fileExt != "" {
+		suffix = fmt.Sprintf("%s.%s", suffix, fileExt)
+	}
+
+	if prefix == "" {
+		return suffix
+	}
+
+	return fmt.Sprintf("%s.%s", prefix, suffix)
+}
+
+// derefTimeOrZero dereferences a time.Time pointer and returns the zero value if the pointer is nil.
+// This prevents nil pointer dereference errors when using windows. This is mostly an assertion, as
+// generally windows for pathing will be pre-validated.
+func derefTimeOrZero(t *time.Time) time.Time {
+	if t == nil {
+		return time.Time{}
+	}
+	return *t
+}

+ 84 - 0
core/pkg/exporter/pathing/eventpath.go

@@ -0,0 +1,84 @@
+package pathing
+
+import (
+	"fmt"
+	"path"
+	"time"
+)
+
+// 2006-01-02T15:04:05Z07:00
+
+// EventStorageTimeFormat is YYYYMMDDHHmmss
+const EventStorageTimeFormat = "20060102150405"
+
+// EventStoragePathFormatter is an implementation of the StoragePathFormatter interface for
+// a cluster separated storage path of the format:
+//
+//	<root>/federated/<cluster>/<event>/<sub-paths...>/YYYYMMDDHHmmss
+type EventStoragePathFormatter struct {
+	rootDir   string
+	clusterId string
+	event     string
+	subPaths  []string
+}
+
+// NewBingenStoragePathFormatter creates a StoragePathFormatter for a cluster separated storage path
+// with the given root directory, cluster id, pipeline, and resolution. To omit the resolution directory
+// structure, provide a `nil` resolution.
+func NewEventStoragePathFormatter(rootDir, clusterId, event string, subPaths ...string) (StoragePathFormatter[time.Time], error) {
+	if clusterId == "" {
+		return nil, fmt.Errorf("cluster id cannot be empty")
+	}
+
+	if event == "" {
+		return nil, fmt.Errorf("event cannot be empty")
+	}
+
+	for _, subPath := range subPaths {
+		if subPath == "" {
+			return nil, fmt.Errorf("subpaths cannot be empty")
+		}
+	}
+
+	return &EventStoragePathFormatter{
+		rootDir:   rootDir,
+		clusterId: clusterId,
+		event:     event,
+		subPaths:  subPaths,
+	}, nil
+}
+
+// RootDir returns the root directory of the storage path formatter.
+func (espf *EventStoragePathFormatter) RootDir() string {
+	return espf.rootDir
+}
+
+// ToFullPath returns the full path to a file name within the storage directory using the format:
+//
+//	<root>/federated/<cluster>/<event>/YYYYMMDDHHmm.json
+func (espf *EventStoragePathFormatter) ToFullPath(prefix string, timestamp time.Time, fileExt string) string {
+	fileName := toEventFileName(prefix, timestamp, fileExt)
+
+	return path.Join(
+		espf.rootDir,
+		espf.clusterId,
+		espf.event,
+		path.Join(espf.subPaths...),
+		fileName,
+	)
+}
+
+// toEventFileName formats the file name as <prefix>.<timestamp>. if a non-empty fileExt is provided,
+// then the file extension is appended to the file name.
+func toEventFileName(prefix string, timestamp time.Time, fileExt string) string {
+	suffix := timestamp.Format(EventStorageTimeFormat)
+	if fileExt != "" {
+		suffix = fmt.Sprintf("%s.%s", suffix, fileExt)
+	}
+
+	if prefix == "" {
+		return suffix
+	}
+
+	return fmt.Sprintf("%s.%s", prefix, suffix)
+}

+ 209 - 0
core/pkg/exporter/pathing/path_test.go

@@ -0,0 +1,209 @@
+package pathing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+)
+
+func TestBingenPathFormatter(t *testing.T) {
+	type testCase struct {
+		name       string
+		rootPath   string
+		clusterID  string
+		pipeline   string
+		resolution *time.Duration
+		prefix     string
+		expected   string
+	}
+
+	testCases := []testCase{
+		{
+			name:       "no resolution",
+			rootPath:   "federated",
+			clusterID:  "cluster-a",
+			pipeline:   "allocation",
+			resolution: nil,
+			prefix:     "",
+			expected:   "federated/cluster-a/etl/bingen/allocation/1704110400-1704114000",
+		},
+		{
+			name:       "with resolution",
+			rootPath:   "federated",
+			clusterID:  "cluster-a",
+			pipeline:   "allocation",
+			resolution: &[]time.Duration{1 * time.Hour}[0],
+			prefix:     "",
+			expected:   "federated/cluster-a/etl/bingen/allocation/1h/1704110400-1704114000",
+		},
+		{
+			name:       "no resolution with prefix",
+			rootPath:   "federated",
+			clusterID:  "cluster-a",
+			pipeline:   "allocation",
+			resolution: nil,
+			prefix:     "test",
+			expected:   "federated/cluster-a/etl/bingen/allocation/test.1704110400-1704114000",
+		},
+		{
+			name:       "with resolution with prefix",
+			rootPath:   "federated",
+			clusterID:  "cluster-a",
+			pipeline:   "allocation",
+			resolution: &[]time.Duration{1 * time.Hour}[0],
+			prefix:     "test",
+			expected:   "federated/cluster-a/etl/bingen/allocation/1h/test.1704110400-1704114000",
+		},
+		{
+			name:       "daily resolution",
+			rootPath:   "federated",
+			clusterID:  "cluster-a",
+			pipeline:   "allocation",
+			resolution: &[]time.Duration{24 * time.Hour}[0],
+			prefix:     "",
+			expected:   "federated/cluster-a/etl/bingen/allocation/1d/1704110400-1704196800",
+		},
+		{
+			name:       "weekly resolution",
+			rootPath:   "federated",
+			clusterID:  "cluster-a",
+			pipeline:   "allocation",
+			resolution: &[]time.Duration{7 * 24 * time.Hour}[0],
+			prefix:     "",
+			expected:   "federated/cluster-a/etl/bingen/allocation/1w/1704110400-1704715200",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			pathing, err := NewBingenStoragePathFormatter(tc.rootPath, tc.clusterID, tc.pipeline, tc.resolution)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			start := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
+			end := time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC)
+			if tc.resolution != nil {
+				end = start.Add(*tc.resolution)
+			}
+
+			result := pathing.ToFullPath(tc.prefix, opencost.NewClosedWindow(start, end), "")
+			if result != tc.expected {
+				t.Errorf("Expected %s, got %s", tc.expected, result)
+			}
+		})
+	}
+}
+
+func TestEventPathFormatter(t *testing.T) {
+	type testCase struct {
+		name      string
+		rootPath  string
+		clusterID string
+		event     string
+		subPaths  []string
+		prefix    string
+		fileExt   string
+		expected  string
+	}
+
+	testCases := []testCase{
+		{
+			name:      "with root path with file extension",
+			rootPath:  "/tmp/federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{},
+			prefix:    "",
+			fileExt:   "json",
+			expected:  "/tmp/federated/cluster-a/heartbeat/20240101124000.json",
+		},
+		{
+			name:      "with file extension",
+			rootPath:  "federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{},
+			prefix:    "",
+			fileExt:   "json",
+			expected:  "federated/cluster-a/heartbeat/20240101124000.json",
+		},
+		{
+			name:      "with root path with file extension with sub-paths",
+			rootPath:  "/tmp/federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{"foo", "bar"},
+			prefix:    "",
+			fileExt:   "json",
+			expected:  "/tmp/federated/cluster-a/heartbeat/foo/bar/20240101124000.json",
+		},
+		{
+			name:      "without file extension",
+			rootPath:  "federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{},
+			prefix:    "",
+			fileExt:   "",
+			expected:  "federated/cluster-a/heartbeat/20240101124000",
+		},
+		{
+			name:      "with prefix with file extension",
+			rootPath:  "federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{},
+			prefix:    "test",
+			fileExt:   "json",
+			expected:  "federated/cluster-a/heartbeat/test.20240101124000.json",
+		},
+		{
+			name:      "with prefix with file extension with sub-paths",
+			rootPath:  "federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{"foo", "bar", "baz"},
+			prefix:    "test",
+			fileExt:   "json",
+			expected:  "federated/cluster-a/heartbeat/foo/bar/baz/test.20240101124000.json",
+		},
+		{
+			name:      "with prefix without file extension",
+			rootPath:  "federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{},
+			prefix:    "test",
+			fileExt:   "",
+			expected:  "federated/cluster-a/heartbeat/test.20240101124000",
+		},
+		{
+			name:      "with prefix without file extension with sub-paths",
+			rootPath:  "federated",
+			clusterID: "cluster-a",
+			event:     "heartbeat",
+			subPaths:  []string{"foo"},
+			prefix:    "test",
+			fileExt:   "",
+			expected:  "federated/cluster-a/heartbeat/foo/test.20240101124000",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			pathing, err := NewEventStoragePathFormatter(tc.rootPath, tc.clusterID, tc.event, tc.subPaths...)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+
+			timestamp := time.Date(2024, 1, 1, 12, 40, 0, 0, time.UTC)
+
+			result := pathing.ToFullPath(tc.prefix, timestamp, tc.fileExt)
+			if result != tc.expected {
+				t.Errorf("Expected %s, got %s", tc.expected, result)
+			}
+		})
+	}
+}

+ 11 - 0
core/pkg/exporter/pathing/pathing.go

@@ -0,0 +1,11 @@
+package pathing
+
+// StoragePathFormatter is an interface used to format storage paths for exporting data types.
+type StoragePathFormatter[T any] interface {
+	// RootDir returns the root directory for the storage path.
+	RootDir() string
+
+	// ToFullPath returns the full path to a file name within the storage
+	// directory leveraging a prefix and an incoming T type (generally a daterange or timestamp).
+	ToFullPath(prefix string, in T, fileExt string) string
+}

+ 99 - 0
core/pkg/exporter/pathing/pathutils/pathutils.go

@@ -0,0 +1,99 @@
+package pathutils
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+)
+
+// FormatEpochRange returns a string representation of the given start and end times in the form:
+// <start-epoch>-<end-epoch>
+func FormatEpochRange(start, end time.Time) string {
+	startStr := strconv.FormatInt(start.Unix(), 10)
+	endStr := strconv.FormatInt(end.Unix(), 10)
+	return fmt.Sprintf("%s-%s", startStr, endStr)
+}
+
+// FormatEpochWindow returns a string representation of the given window in the form:
+// <start-epoch>-<end-epoch>
+//
+// If the window is not closed, an error is returned.
+func FormatEpochWindow(window opencost.Window) (string, error) {
+	start := window.Start()
+	end := window.End()
+	if start == nil || end == nil {
+		return "", fmt.Errorf("illegal window: %s", window)
+	}
+
+	return FormatEpochRange(*start, *end), nil
+}
+
+// EpochFormatToWindow converts an epoch formatted file name to a Window.
+func EpochFormatToWindow(fileName string) (opencost.Window, error) {
+	var window opencost.Window
+
+	tokens := strings.Split(fileName, "-")
+	if len(tokens) != 2 {
+		return window, fmt.Errorf("invalid path format")
+	}
+
+	startUnix, err := strconv.ParseInt(tokens[0], 10, 64)
+	if err != nil {
+		return window, fmt.Errorf("Failed to Parse start(%s): %s\n", tokens[0], err.Error())
+	}
+	endUnix, err := strconv.ParseInt(tokens[1], 10, 64)
+	if err != nil {
+		return window, fmt.Errorf("Failed to Parse end(%s): %s\n", tokens[1], err.Error())
+	}
+
+	start := time.Unix(startUnix, 0)
+	end := time.Unix(endUnix, 0)
+
+	return opencost.NewWindow(&start, &end), nil
+}
+
+// FormatUTFRange returns a string representation of the given start and end times in the form:
+// <start-utf>-<end-utf>
+func FormatUTFRange(start, end time.Time) string {
+	startStr := start.Format(time.RFC3339)
+	endStr := end.Format(time.RFC3339)
+	return fmt.Sprintf("%s-%s", startStr, endStr)
+}
+
+// FormatUTFWindow returns a string representation of the given window in the form:
+// <start-epoch>-<end-epoch>
+//
+// If the window is not closed, an error is returned.
+func FormatUTFWindow(window opencost.Window) (string, error) {
+	start := window.Start()
+	end := window.End()
+	if start == nil || end == nil {
+		return "", fmt.Errorf("illegal window: %s", window)
+	}
+
+	return FormatEpochRange(*start, *end), nil
+}
+
+// UTFFormatToWindow converts an epoch UTF file name to a Window.
+func UTFFormatToWindow(fileName string) (opencost.Window, error) {
+	var window opencost.Window
+
+	tokens := strings.Split(fileName, "-")
+	if len(tokens) != 2 {
+		return window, fmt.Errorf("invalid path format")
+	}
+
+	start, err := time.Parse(time.RFC3339, tokens[0])
+	if err != nil {
+		return window, fmt.Errorf("Failed to Parse start(%s): %s\n", tokens[0], err.Error())
+	}
+	end, err := time.Parse(time.RFC3339, tokens[1])
+	if err != nil {
+		return window, fmt.Errorf("Failed to Parse end(%s): %s\n", tokens[1], err.Error())
+	}
+
+	return opencost.NewWindow(&start, &end), nil
+}

+ 27 - 0
core/pkg/exporter/source.go

@@ -0,0 +1,27 @@
+package exporter
+
+import "time"
+
+// ExportSource[T] provides a factory style contract for creating new `T` instances for exporting.
+type ExportSource[T any] interface {
+	Make(timestamp time.Time) *T
+
+	// Name returns the name of the ExportSource.
+	Name() string
+}
+
+// ComputeSource[T] provides an interface for a compute data source.
+type ComputeSource[T any] interface {
+	// CanCompute should return true iff the ComputeSource can effectively act as
+	// a source of T data for the given time range. For example, a ComputeSource
+	// with two-day coverage cannot fulfill a range from three days ago, and should
+	// not be left to return an error in Compute. Instead, it should report that is
+	// cannot compute and allow another Source to handle the computation.
+	CanCompute(start, end time.Time) bool
+
+	// Compute should compute a single T for the given time range, optionally using the given resolution.
+	Compute(start, end time.Time, resolution time.Duration) (*T, error)
+
+	// Name returns the name of the ComputeSource
+	Name() string
+}

+ 146 - 0
core/pkg/exporter/validator/validator.go

@@ -0,0 +1,146 @@
+package validator
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+)
+
+var (
+	// ErrNilSet is used as a validation error when the set passed is nil.
+	ErrNilSet error = errors.New("invalid set: nil")
+
+	// ErrNilWindowStart is used as a validation error when the set passed
+	// has an open Window Start.
+	ErrNilWindowStart error = errors.New("invalid set: nil window.Start")
+
+	// ErrNilWindowEnd is used as a validation error when the set passed
+	// has an open Window End.
+	ErrNilWindowEnd error = errors.New("invalid set: nil window.End")
+
+	// ErrEmptySet is used as a validation error when the set passed is
+	// empty.
+	ErrEmptySet error = errors.New("invalid set: empty")
+)
+
+// SetConstraint is a helper constraint for an Export[T] implementation
+type SetConstraint[T any] interface {
+	IsEmpty() bool
+	*T
+}
+
+// Validator is an implementation of an object capable of validating a T instance prior to
+// insertion into a store.
+type ExportValidator[T any] interface {
+	// Validate determines whether or not the given data can be legally
+	// added to the store.
+	Validate(window opencost.Window, data *T) error
+
+	// IsOverwrite determines whether or not the provided data can be used
+	// to overwrite existing data in the storage.
+	IsOverwrite(data *T) bool
+}
+
+// validation of a window, which is a common pattern in the validator implementations
+func validateWindow(window opencost.Window) (start, end time.Time, err error) {
+	s, e := window.Start(), window.End()
+	if s == nil {
+		err = ErrNilWindowStart
+		return
+	}
+	if e == nil {
+		err = ErrNilWindowEnd
+		return
+	}
+
+	start = *s
+	end = *e
+
+	return
+}
+
+//--------------------------------------------------------------------------
+//  Chain Validator
+//--------------------------------------------------------------------------
+
+// chain validator is used to chain multiple validators together.
+type chainValidator[T any] struct {
+	validators []ExportValidator[T]
+}
+
+// NewChainValidator creates a single validator instances which chains together many validators.
+func NewChainValidator[T any](validators ...ExportValidator[T]) ExportValidator[T] {
+	return &chainValidator[T]{validators: validators}
+}
+
+func (cv *chainValidator[T]) Validate(window opencost.Window, data *T) error {
+	for _, validator := range cv.validators {
+		err := validator.Validate(window, data)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (cv *chainValidator[T]) IsOverwrite(data *T) bool {
+	for _, validator := range cv.validators {
+		if !validator.IsOverwrite(data) {
+			return false
+		}
+	}
+	return true
+}
+
+//--------------------------------------------------------------------------
+//  Set Validator
+//--------------------------------------------------------------------------
+
+// setValidator is used for the a potentially "empty" set of data that should avoid
+// overwriting existing data in the store, and applies a window and resolution validation.
+type setValidator[T any, U SetConstraint[T]] struct {
+	resolution time.Duration
+}
+
+// NewSetValidator is used for the a potentially "empty" set of data that should avoid
+// overwriting existing data in the store, and applies a window and resolution validation.
+func NewSetValidator[T any, U SetConstraint[T]](resolution time.Duration) ExportValidator[T] {
+	return &setValidator[T, U]{
+		resolution: resolution,
+	}
+}
+
+// IsValid determines whether the provided start and end times are valid for the data provided.
+func (sv *setValidator[T, U]) Validate(window opencost.Window, data *T) error {
+	if data == nil {
+		return ErrNilSet
+	}
+
+	start, end, err := validateWindow(window)
+	if err != nil {
+		return err
+	}
+
+	// Check Resolution
+	resolution := end.Sub(start)
+	if resolution != sv.resolution {
+		return fmt.Errorf("invalid set: resolution of %ds != %ds", uint64(resolution.Seconds()), uint64(sv.resolution.Seconds()))
+	}
+
+	// Check UTC Multiple
+	nearestUTCMultiple := opencost.RoundBack(start.UTC(), sv.resolution)
+	if !start.Equal(nearestUTCMultiple) {
+		return fmt.Errorf("invalid set: start %s is not a UTC multiple of resolution %ds, the nearest valid start is %s", start.String(), uint64(sv.resolution.Seconds()), nearestUTCMultiple.String())
+	}
+
+	return nil
+}
+
+// IsOverwrite should return true if the data is not nil and the set is not empty
+func (sv *setValidator[T, U]) IsOverwrite(data *T) bool {
+	var set U = data
+
+	return set != nil && !set.IsEmpty()
+}

+ 253 - 0
core/pkg/exporter/validator/validator_test.go

@@ -0,0 +1,253 @@
+package validator
+
+import (
+	"fmt"
+	"slices"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
+)
+
+func TestWindowValidator(t *testing.T) {
+	v := NewSetValidator[opencost.AllocationSet](time.Hour)
+
+	end := time.Now().UTC()
+	start := end.Add(-time.Hour)
+
+	set := opencost.NewAllocationSet(start, end)
+
+	invalidEnd := opencost.NewWindow(&start, nil)
+	invalidStart := opencost.NewWindow(nil, &end)
+
+	s := start.Truncate(time.Hour)
+	e := end.Truncate(time.Hour)
+	valid := opencost.NewWindow(&s, &e)
+
+	// Invalid End
+	set.Window = invalidEnd
+	err := v.Validate(set.Window, set)
+	if err == nil {
+		t.Errorf("Validator returned valid flag for invalid window in set")
+	}
+
+	// InValid Start
+	set.Window = invalidStart
+	err = v.Validate(set.Window, set)
+	if err == nil {
+		t.Errorf("Validator returned valid flag for invalid window in set")
+	}
+
+	// Valid
+	set.Window = valid
+	err = v.Validate(set.Window, set)
+	if err != nil {
+		t.Errorf("Validator returned an error for a valid window: %v", err)
+	}
+
+}
+
+func TestUTCResolutionValidator(t *testing.T) {
+	start := opencost.RoundBack(time.Now().UTC(), timeutil.Week)
+
+	set := opencost.NewAllocationSet(start, start.Add(time.Hour))
+
+	testCases := map[string]struct {
+		resolution time.Duration
+		window     opencost.Window
+		expected   bool
+	}{
+		"Invalid End": {
+			resolution: time.Hour,
+			window:     opencost.NewWindow(&start, nil),
+			expected:   false,
+		},
+		"Invalid Start": {
+			resolution: time.Hour,
+			window:     opencost.NewWindow(nil, &start),
+			expected:   false,
+		},
+		"Hour: Invalid Resolution": {
+			resolution: time.Hour,
+			window:     opencost.NewClosedWindow(start, start.Add(2*time.Hour)),
+			expected:   false,
+		},
+		"Hour: Invalid UTC position": {
+			resolution: time.Hour,
+			window:     opencost.NewClosedWindow(start.Add(time.Minute), start.Add(time.Hour).Add(time.Minute)),
+			expected:   false,
+		},
+		"Hour: Valid": {
+			resolution: time.Hour,
+			window:     opencost.NewClosedWindow(start, start.Add(time.Hour)),
+			expected:   true,
+		},
+		"Day: Invalid UTC position": {
+			resolution: timeutil.Day,
+			window:     opencost.NewClosedWindow(start.Add(time.Minute), start.Add(timeutil.Day).Add(time.Minute)),
+			expected:   false,
+		},
+		"Day: Valid": {
+			resolution: timeutil.Day,
+			window:     opencost.NewClosedWindow(start, start.Add(timeutil.Day)),
+			expected:   true,
+		},
+		"Week: Invalid UTC position": {
+			resolution: timeutil.Week,
+			window:     opencost.NewClosedWindow(start.Add(timeutil.Day), start.Add(timeutil.Week).Add(timeutil.Day)),
+			expected:   false,
+		},
+		"Week: Valid": {
+			resolution: timeutil.Week,
+			window:     opencost.NewClosedWindow(start, start.Add(timeutil.Week)),
+			expected:   true,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			v := NewSetValidator[opencost.AllocationSet](tc.resolution)
+			set.Window = tc.window
+			err := v.Validate(tc.window, set)
+			isValid := err == nil
+			if tc.expected != isValid {
+				t.Errorf("Validator returned incorrect flag")
+			}
+			if tc.expected && err != nil {
+				t.Errorf("Validator returned unexpected error")
+			}
+			if !tc.expected && err == nil {
+				t.Errorf("Validator did not returned expected error")
+			}
+
+		})
+	}
+}
+
+func TestEmptyAndNil(t *testing.T) {
+	v := NewSetValidator[opencost.AllocationSet](time.Hour)
+
+	end := time.Now().UTC().Truncate(time.Hour)
+	start := end.Add(-time.Hour)
+
+	window := opencost.NewClosedWindow(start, end)
+	emptySet := opencost.NewAllocationSet(start, end)
+	nilSet := (*opencost.AllocationSet)(nil)
+
+	err := v.Validate(window, nilSet)
+	if err == nil {
+		t.Errorf("Validator returned valid flag for nil data")
+	}
+
+	isEmpty := !v.IsOverwrite(emptySet)
+	if !isEmpty {
+		t.Errorf("Validator returned overwrite flag for empty data")
+	}
+}
+
+type collection struct {
+	vs []string
+}
+
+func (c *collection) add(v string) {
+	c.vs = append(c.vs, v)
+}
+
+func (c *collection) clear() {
+	c.vs = []string{}
+}
+
+type appendingValidator struct {
+	tag  string
+	tags *collection
+	fail bool
+}
+
+func newAppendingValidator(tag string, tags *collection) *appendingValidator {
+	return &appendingValidator{
+		tag:  tag,
+		tags: tags,
+	}
+}
+
+func newFailingValidator(tag string, tags *collection) *appendingValidator {
+	return &appendingValidator{
+		tag:  tag,
+		tags: tags,
+		fail: true,
+	}
+}
+
+func (av *appendingValidator) Validate(window opencost.Window, data *opencost.AllocationSet) error {
+	if av.fail {
+		return fmt.Errorf("failed validator: %s", av.tag)
+	}
+	av.tags.add("Validate: " + av.tag)
+	return nil
+}
+
+func (av *appendingValidator) IsOverwrite(data *opencost.AllocationSet) bool {
+	av.tags.add("IsOverwrite: " + av.tag)
+	return true
+}
+
+func TestChainValidation(t *testing.T) {
+	tags := new(collection)
+
+	validators := []ExportValidator[opencost.AllocationSet]{
+		newAppendingValidator("a", tags),
+		newAppendingValidator("b", tags),
+		newAppendingValidator("c", tags),
+		newAppendingValidator("d", tags),
+	}
+
+	v := NewChainValidator(validators...)
+
+	end := time.Now().UTC().Truncate(time.Hour)
+	start := end.Add(-time.Hour)
+
+	window := opencost.NewClosedWindow(start, end)
+	set := opencost.NewAllocationSet(start, end)
+
+	err := v.Validate(window, set)
+	if err != nil {
+		t.Errorf("Validator returned unexpected error: %v", err)
+	}
+
+	if !slices.Contains(tags.vs, "Validate: a") {
+		t.Errorf("Validator did not call validate on first validator")
+	}
+	if !slices.Contains(tags.vs, "Validate: b") {
+		t.Errorf("Validator did not call validate on second validator")
+	}
+	if !slices.Contains(tags.vs, "Validate: c") {
+		t.Errorf("Validator did not call validate on third validator")
+	}
+	if !slices.Contains(tags.vs, "Validate: d") {
+		t.Errorf("Validator did not call validate on fourth validator")
+	}
+
+	tags.clear()
+
+	// Test failing validator
+	validators = []ExportValidator[opencost.AllocationSet]{
+		newAppendingValidator("a", tags),
+		newAppendingValidator("b", tags),
+		newFailingValidator("c", tags),
+		newAppendingValidator("d", tags),
+	}
+
+	v = NewChainValidator(validators...)
+	err = v.Validate(window, set)
+	if err == nil {
+		t.Errorf("Validator did not return expected error")
+	}
+
+	if !slices.Contains(tags.vs, "Validate: a") {
+		t.Errorf("Validator did not call validate on first validator")
+	}
+	if !slices.Contains(tags.vs, "Validate: b") {
+		t.Errorf("Validator did not call validate on second validator")
+	}
+}

+ 22 - 0
core/pkg/heartbeat/exporter/controller.go

@@ -0,0 +1,22 @@
+package exporter
+
+import (
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/heartbeat"
+	"github.com/opencost/opencost/core/pkg/storage"
+)
+
+// NewHeartbeatExportController creates a new EventExportController for Heartbeat events.
+// A HeartbeatMetadataProvider can optionally be provided to append metadata to the Heartbeat payload.
+func NewHeartbeatExportController(
+	clusterId string,
+	applicationName string,
+	version string,
+	store storage.Storage,
+	provider HeartbeatMetadataProvider,
+) *exporter.EventExportController[heartbeat.Heartbeat] {
+	return exporter.NewEventExportController(
+		NewHeartbeatSource(applicationName, version, provider),
+		NewHeartbeatExporter(clusterId, applicationName, store),
+	)
+}

+ 11 - 0
core/pkg/heartbeat/exporter/encoder.go

@@ -0,0 +1,11 @@
+package exporter
+
+import (
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/heartbeat"
+)
+
+// NewHeartbeatEncoder returns a JSON encoder used to encode Heartbeat events.
+func NewHeartbeatEncoder() exporter.Encoder[heartbeat.Heartbeat] {
+	return exporter.NewJSONEncoder[heartbeat.Heartbeat]()
+}

+ 24 - 0
core/pkg/heartbeat/exporter/exporter.go

@@ -0,0 +1,24 @@
+package exporter
+
+import (
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
+	"github.com/opencost/opencost/core/pkg/heartbeat"
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/storage"
+)
+
+// NewHeartbeatExporter creates a new `StorageExporter[Heartbeat]` instance for exporting Heartbeat events.
+func NewHeartbeatExporter(clusterId string, applicationName string, storage storage.Storage) exporter.EventExporter[heartbeat.Heartbeat] {
+	pathing, err := pathing.NewEventStoragePathFormatter("federated", clusterId, heartbeat.HeartbeatEventName, applicationName)
+	if err != nil {
+		log.Errorf("failed to create pathing formatter: %v", err)
+		return nil
+	}
+
+	return exporter.NewEventStorageExporter(
+		pathing,
+		NewHeartbeatEncoder(),
+		storage,
+	)
+}

+ 87 - 0
core/pkg/heartbeat/exporter/heartbeat_test.go

@@ -0,0 +1,87 @@
+package exporter
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+	"path/filepath"
+	"slices"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/heartbeat"
+	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/sliceutil"
+)
+
+const (
+	MockClusterId       = "mock-cluster-1"
+	MockApplicationName = "mock-agent"
+	MockVersion         = "1.0.0"
+)
+
+type MockHeartbeatMetadataProvider struct{}
+
+func NewMockHeartbeatMetadataProvider() *MockHeartbeatMetadataProvider {
+	return &MockHeartbeatMetadataProvider{}
+}
+
+func (m *MockHeartbeatMetadataProvider) GetMetadata() map[string]any {
+	return map[string]any{
+		"cluster_id": MockClusterId,
+	}
+}
+
+func TestHeartbeatExporter(t *testing.T) {
+	t.Parallel()
+
+	mdp := NewMockHeartbeatMetadataProvider()
+	store := storage.NewMemoryStorage()
+
+	controller := NewHeartbeatExportController(MockClusterId, MockApplicationName, MockVersion, store, mdp)
+
+	if !controller.Start(time.Second) {
+		t.Fatal("Failed to start controller")
+	}
+
+	time.Sleep(10 * time.Second)
+	controller.Stop()
+
+	files, _ := store.List(path.Join("federated", MockClusterId, heartbeat.HeartbeatEventName, MockApplicationName))
+	if len(files) == 0 {
+		t.Fatal("No files found in storage")
+	}
+
+	fileNames := sliceutil.Map(files, func(stat *storage.StorageInfo) string {
+		return stat.Name
+	})
+
+	slices.Sort(fileNames)
+
+	lastCheck := time.Time{}
+
+	for _, f := range fileNames {
+		fpath := filepath.Join("federated", MockClusterId, "heartbeat", MockApplicationName, f)
+		data, err := store.Read(fpath)
+		if err != nil {
+			t.Fatalf("Failed to read file %s: %v", fpath, err)
+		}
+
+		hb := new(heartbeat.Heartbeat)
+		if err := json.Unmarshal(data, hb); err != nil {
+			t.Fatalf("Failed to unmarshal heartbeat data: %v", err)
+		}
+
+		fmt.Printf("%s: %d bytes\n%s\n\n", f, len(data), string(data))
+
+		if hb.Metadata["cluster_id"] != MockClusterId {
+			t.Fatalf("Expected cluster ID %s, got %s", MockClusterId, hb.Metadata["cluster_id"])
+		}
+
+		if hb.Timestamp.Before(lastCheck) {
+			t.Fatalf("Expected timestamp %s to be after %s", hb.Timestamp, lastCheck)
+		}
+		lastCheck = hb.Timestamp
+
+	}
+}

+ 80 - 0
core/pkg/heartbeat/exporter/source.go

@@ -0,0 +1,80 @@
+package exporter
+
+import (
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/opencost/opencost/core/pkg/clusters"
+	"github.com/opencost/opencost/core/pkg/heartbeat"
+)
+
+// HeartbeatMetadataProvider is an interface that provides metadata for heartbeat instances. It can be used to inject
+// custom metadata into a generic `Heartbeat` payload.
+type HeartbeatMetadataProvider interface {
+	// GetMetadata returns the metadata for new heartbeat instances.
+	GetMetadata() map[string]any
+}
+
+// ClusterInfoMetadataProvider is a `HeartbeatMetadataProvider` implementation that provides metadata about the cluster
+// leveraging a `ClusterInfoProvider` implementation.
+type ClusterInfoMetadataProvider struct {
+	clusterInfoProvider clusters.ClusterInfoProvider
+}
+
+// NewClusterInfoMetadataProvider creates a new `ClusterInfoMetadataProvider` instance. The `provider` parameter is used to
+// inject custom metadata, but can be set to `nil` if no metadata is needed.
+func NewClusterInfoMetadataProvider(provider clusters.ClusterInfoProvider) *ClusterInfoMetadataProvider {
+	return &ClusterInfoMetadataProvider{
+		clusterInfoProvider: provider,
+	}
+}
+
+// GetMetadata returns the metadata for new heartbeat instances. It uses the `ClusterInfoProvider` to get the cluster
+// information and injects it into the metadata map.
+func (c *ClusterInfoMetadataProvider) GetMetadata() map[string]any {
+	m := c.clusterInfoProvider.GetClusterInfo()
+	metadata := make(map[string]any, len(m))
+
+	for k, v := range m {
+		metadata[k] = v
+	}
+
+	return metadata
+}
+
+// HeartbeatSource is an `export.ExportSource` implementation that provides the basic data for a `Heartbeat` payload, and
+// leverages a `HeartbeatMetadataProvider` to inject custom metadata.
+type HeartbeatSource struct {
+	startTime        time.Time
+	applicationName  string
+	version          string
+	metadataProvider HeartbeatMetadataProvider
+}
+
+// NewHeartbeatSource creates a new `HeartbeatSource` instance. The `provider` parameter is used to inject custom metadata,
+// but can be set to `nil` if no metadata is needed.
+func NewHeartbeatSource(applicationName string, version string, provider HeartbeatMetadataProvider) *HeartbeatSource {
+	return &HeartbeatSource{
+		startTime:        time.Now().UTC(),
+		applicationName:  applicationName,
+		version:          version,
+		metadataProvider: provider,
+	}
+}
+
+// Make creates a new `Heartbeat` instance with the provided current time.
+func (h *HeartbeatSource) Make(t time.Time) *heartbeat.Heartbeat {
+	id := uuid.Must(uuid.NewV7()).String()
+	uptime := uint64(t.Sub(h.startTime).Minutes())
+
+	var metadata map[string]any
+	if h.metadataProvider != nil {
+		metadata = h.metadataProvider.GetMetadata()
+	}
+
+	return heartbeat.NewHeartbeat(id, t, uptime, h.applicationName, h.version, metadata)
+}
+
+func (h *HeartbeatSource) Name() string {
+	return heartbeat.HeartbeatEventName + "-source"
+}

+ 59 - 0
core/pkg/heartbeat/exporter/source_test.go

@@ -0,0 +1,59 @@
+package exporter
+
+import (
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/clusters"
+)
+
+type MockClusterInfoProvider struct{}
+
+func NewMockClusterInfoProvider() clusters.ClusterInfoProvider {
+	return new(MockClusterInfoProvider)
+}
+func (m *MockClusterInfoProvider) GetClusterInfo() map[string]string {
+	return map[string]string{
+		clusters.ClusterInfoIdKey:       "test-cluster-id",
+		clusters.ClusterInfoNameKey:     "test-cluster-name",
+		clusters.ClusterInfoVersionKey:  "test-cluster-version",
+		clusters.ClusterInfoRegionKey:   "test-cluster-region",
+		clusters.ClusterInfoProviderKey: "test-cluster-provider",
+	}
+}
+
+func TestClusterInfoProvider(t *testing.T) {
+	t.Parallel()
+
+	provider := NewMockClusterInfoProvider()
+	clusterInfoMetaDataProvider := NewClusterInfoMetadataProvider(provider)
+
+	heartbeatSrc := NewHeartbeatSource("test-app", "v0.0.1", clusterInfoMetaDataProvider)
+
+	hb := heartbeatSrc.Make(time.Now().UTC().Truncate(time.Second))
+
+	md := hb.Metadata
+	if md == nil {
+		t.Errorf("Expected metadata to be non-nil, got nil")
+	}
+
+	if md[clusters.ClusterInfoIdKey] != "test-cluster-id" {
+		t.Errorf("Expected cluster ID to be 'test-cluster-id', got '%s'", md[clusters.ClusterInfoIdKey])
+	}
+	if md[clusters.ClusterInfoNameKey] != "test-cluster-name" {
+		t.Errorf("Expected cluster name to be 'test-cluster-name', got '%s'", md[clusters.ClusterInfoNameKey])
+	}
+	if md[clusters.ClusterInfoVersionKey] != "test-cluster-version" {
+		t.Errorf("Expected cluster version to be 'test-cluster-version', got '%s'", md[clusters.ClusterInfoVersionKey])
+	}
+	if md[clusters.ClusterInfoRegionKey] != "test-cluster-region" {
+		t.Errorf("Expected cluster region to be 'test-cluster-region', got '%s'", md[clusters.ClusterInfoRegionKey])
+	}
+	if md[clusters.ClusterInfoProviderKey] != "test-cluster-provider" {
+		t.Errorf("Expected cluster provider to be 'test-cluster-provider', got '%s'", md[clusters.ClusterInfoProviderKey])
+	}
+
+	if heartbeatSrc.Name() != "heartbeat-source" {
+		t.Errorf("Expected source name to be 'heartbeat-source', got '%s'", heartbeatSrc.Name())
+	}
+}

+ 34 - 0
core/pkg/heartbeat/heartbeat.go

@@ -0,0 +1,34 @@
+package heartbeat
+
+import (
+	"time"
+)
+
+// HeartbeatEventName is used to represent the name of the heartbeat pipeline event to categorize for storage.
+const HeartbeatEventName string = "heartbeat"
+
+// Heartbeat is a payload struct that contains custom information and the timestamp of the heartbeat.
+type Heartbeat struct {
+	Id          string         `json:"id"`
+	Timestamp   time.Time      `json:"timestamp"`
+	Uptime      uint64         `json:"uptime"`
+	Application string         `json:"application"`
+	Version     string         `json:"version"`
+	Metadata    map[string]any `json:"metadata,omitempty"`
+}
+
+// NewHeartbeat creates a new Heartbeat instance with the provided parameters.
+// The `id` is a unique identifier for the heartbeat, `timestamp` is the time of the heartbeat,
+// `uptime` is the uptime in seconds, `version` is the version of the heartbeat, and `metadata`
+// is a pointer to a generic type that can hold any additional information. Metadata _can_ be omitted
+// by passing `nil`.
+func NewHeartbeat(id string, timestamp time.Time, uptime uint64, application string, version string, metadata map[string]any) *Heartbeat {
+	return &Heartbeat{
+		Id:          id,
+		Timestamp:   timestamp,
+		Uptime:      uptime,
+		Application: application,
+		Version:     version,
+		Metadata:    metadata,
+	}
+}

+ 0 - 0
pkg/kubeconfig/loader.go → core/pkg/kubeconfig/loader.go


+ 41 - 0
core/pkg/nodestats/config.go

@@ -0,0 +1,41 @@
+package nodestats
+
+import (
+	"net/http"
+)
+
+type NodeClientProxyConfig struct {
+	ForceKubeProxy bool
+	LocalProxy     string
+}
+
+func (nac NodeClientProxyConfig) IsLocalProxy() bool {
+	return nac.LocalProxy != ""
+}
+
+type NodeClientConfig struct {
+	ClusterId         string
+	ConcurrentPollers int
+	Transport         *http.Transport
+	CertFile          string
+	KeyFile           string
+	ProxyConfig       NodeClientProxyConfig
+}
+
+func NewNodeClientConfig(
+	clusterId string,
+	concurrentPollers int,
+	transport *http.Transport,
+	certFile string,
+	keyFile string,
+	proxyConfig NodeClientProxyConfig,
+) *NodeClientConfig {
+	return &NodeClientConfig{
+		ClusterId:         clusterId,
+		ConcurrentPollers: concurrentPollers,
+		Transport:         transport,
+		CertFile:          certFile,
+		KeyFile:           keyFile,
+		ProxyConfig:       proxyConfig,
+	}
+}

+ 59 - 0
core/pkg/nodestats/formatter.go

@@ -0,0 +1,59 @@
+package nodestats
+
+import (
+	"fmt"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+)
+
+// NodeEndpointFormatter is an interface that defines a method to format node endpoints.
+type NodeEndpointFormatter interface {
+	FormatEndpoint(s string) string
+}
+
+// DirectNodeFormatter is an implementation of a NodeEndpointFormatter that formats endpoints for direct node access.
+type DirectNodeFormatter struct {
+	ip   string
+	port int64
+}
+
+// NewDirectNodeFormatterFrom creates a new DirectNodeFormatter from a Node instance.
+func NewDirectNodeFormatterFrom(n *clustercache.Node) (*DirectNodeFormatter, error) {
+	if n == nil {
+		return nil, fmt.Errorf("node cannot be nil")
+	}
+
+	ip, port, err := NodeAddress(n)
+	if err != nil {
+		return nil, fmt.Errorf("problem getting node address: %s", err)
+	}
+
+	return &DirectNodeFormatter{
+		ip:   ip,
+		port: int64(port),
+	}, nil
+}
+
+// FormatEndpoint formats the endpoint URL for direct node access.
+func (dnf *DirectNodeFormatter) FormatEndpoint(s string) string {
+	return fmt.Sprintf("https://%s:%v/%s", dnf.ip, dnf.port, s)
+}
+
+// NodeProxyFormatter is an implementation of a NodeEndpointFormatter that formats endpoints for a node proxy request.
+type NodeProxyFormatter struct {
+	clusterHostUrl string
+	nodeName       string
+}
+
+// NewNodeProxyFormatter creates a new NodeProxyFormatter with the given cluster host URL and node name.
+func NewNodeProxyFormatter(clusterHostUrl, nodeName string) *NodeProxyFormatter {
+	return &NodeProxyFormatter{
+		clusterHostUrl: clusterHostUrl,
+		nodeName:       nodeName,
+	}
+}
+
+// FormatEndpoint formats the endpoint URL for a node proxy request.
+func (npf *NodeProxyFormatter) FormatEndpoint(s string) string {
+	return fmt.Sprintf("%s/api/v1/nodes/%s/proxy/%s", npf.clusterHostUrl, npf.nodeName, s)
+}

+ 132 - 0
core/pkg/nodestats/nodes_test.go

@@ -0,0 +1,132 @@
+package nodestats
+
+import (
+	"context"
+	"crypto/tls"
+	"net/http"
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/opencost/opencost/core/pkg/kubeconfig"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+func TestNodeSummaryStuff(t *testing.T) {
+	t.Skip("Skipping live test for node summary client")
+
+	client, err := kubeconfig.LoadKubeClient("")
+	if err != nil {
+		t.Fatalf("failed to load kube client: %v", err)
+	}
+
+	clusterConfig, err := kubeconfig.LoadKubeconfig("")
+	if err != nil {
+		t.Fatalf("failed to load kubeconfig: %v", err)
+	}
+
+	cache := NewTestClusterCache(client)
+
+	transport := &http.Transport{
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true,
+		},
+	}
+
+	config := NewNodeClientConfig("cluster-one", 10, transport, "", "", NodeClientProxyConfig{
+		ForceKubeProxy: true,
+		LocalProxy:     "http://localhost:8080",
+	})
+
+	statsClient := NewNodeStatsSummaryClient(cache, config, clusterConfig)
+
+	summary, err := statsClient.GetNodeData()
+	if err != nil {
+		t.Fatalf("failed to get node data: %v", err)
+	}
+
+	for _, s := range summary {
+		if s == nil {
+			t.Error("received nil summary data")
+			continue
+		}
+		t.Logf("Node Summary: %+v", s)
+	}
+}
+
+type NodesOnlyClusterCache struct {
+	k8sClient kubernetes.Interface
+}
+
+func NewTestClusterCache(k8sClient kubernetes.Interface) *NodesOnlyClusterCache {
+	return &NodesOnlyClusterCache{
+		k8sClient: k8sClient,
+	}
+}
+
+// Run starts the watcher processes
+func (tcc *NodesOnlyClusterCache) Run() {}
+
+// Stops the watcher processes
+func (tcc *NodesOnlyClusterCache) Stop() {}
+
+// GetAllNamespaces returns all the cached namespaces
+func (tcc *NodesOnlyClusterCache) GetAllNamespaces() []*clustercache.Namespace { return nil }
+
+// GetAllNodes returns all the cached nodes
+func (tcc *NodesOnlyClusterCache) GetAllNodes() []*clustercache.Node {
+	nodes, err := tcc.k8sClient.CoreV1().Nodes().List(context.Background(), v1.ListOptions{})
+	if err != nil {
+		return nil
+	}
+
+	var nodeList []*clustercache.Node
+	for _, n := range nodes.Items {
+		nodeList = append(nodeList, clustercache.TransformNode(&n))
+	}
+	return nodeList
+}
+
+// GetAllPods returns all the cached pods
+func (tcc *NodesOnlyClusterCache) GetAllPods() []*clustercache.Pod { return nil }
+
+// GetAllServices returns all the cached services
+func (tcc *NodesOnlyClusterCache) GetAllServices() []*clustercache.Service { return nil }
+
+// GetAllDaemonSets returns all the cached DaemonSets
+func (tcc *NodesOnlyClusterCache) GetAllDaemonSets() []*clustercache.DaemonSet { return nil }
+
+// GetAllDeployments returns all the cached deployments
+func (tcc *NodesOnlyClusterCache) GetAllDeployments() []*clustercache.Deployment { return nil }
+
+// GetAllStatfulSets returns all the cached StatefulSets
+func (tcc *NodesOnlyClusterCache) GetAllStatefulSets() []*clustercache.StatefulSet { return nil }
+
+// GetAllReplicaSets returns all the cached ReplicaSets
+func (tcc *NodesOnlyClusterCache) GetAllReplicaSets() []*clustercache.ReplicaSet { return nil }
+
+// GetAllPersistentVolumes returns all the cached persistent volumes
+func (tcc *NodesOnlyClusterCache) GetAllPersistentVolumes() []*clustercache.PersistentVolume {
+	return nil
+}
+
+// GetAllPersistentVolumeClaims returns all the cached persistent volume claims
+func (tcc *NodesOnlyClusterCache) GetAllPersistentVolumeClaims() []*clustercache.PersistentVolumeClaim {
+	return nil
+}
+
+// GetAllStorageClasses returns all the cached storage classes
+func (tcc *NodesOnlyClusterCache) GetAllStorageClasses() []*clustercache.StorageClass { return nil }
+
+// GetAllJobs returns all the cached jobs
+func (tcc *NodesOnlyClusterCache) GetAllJobs() []*clustercache.Job { return nil }
+
+// GetAllPodDisruptionBudgets returns all cached pod disruption budgets
+func (tcc *NodesOnlyClusterCache) GetAllPodDisruptionBudgets() []*clustercache.PodDisruptionBudget {
+	return nil
+}
+
+// GetAllReplicationControllers returns all cached replication controllers
+func (tcc *NodesOnlyClusterCache) GetAllReplicationControllers() []*clustercache.ReplicationController {
+	return nil
+}

+ 216 - 0
core/pkg/nodestats/nodestats.go

@@ -0,0 +1,216 @@
+package nodestats
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"os"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/util/worker"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/rest"
+	stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
+)
+
+type StatSummaryClient interface {
+	GetNodeData() ([]*stats.Summary, error)
+}
+
+// NodeStatsSummaryClient is a client used to retrieve node and container stats summaries from a Kubernetes cluster,
+// via communicating with the kubelet API on each node.
+type NodeStatsSummaryClient struct {
+	config          *NodeClientConfig
+	directClient    *NodeHttpClient
+	proxyClient     *NodeHttpClient
+	cache           clustercache.ClusterCache
+	endpoint        string
+	clusterHostUrl  string
+	bearerTokenFile string
+}
+
+// NewNodeStatsSummaryClient creates a new NodeStatsSummaryClient with the provided configuration and in-cluster config.
+func NewNodeStatsSummaryClient(cache clustercache.ClusterCache, config *NodeClientConfig, inClusterConfig *rest.Config) *NodeStatsSummaryClient {
+	return &NodeStatsSummaryClient{
+		config:          config,
+		directClient:    NewNodeHttpClient(&http.Client{Transport: config.Transport}),
+		proxyClient:     NewNodeHttpClient(&http.Client{Transport: config.Transport}),
+		cache:           cache,
+		endpoint:        "stats/summary",
+		clusterHostUrl:  inClusterConfig.Host,
+		bearerTokenFile: inClusterConfig.BearerTokenFile,
+	}
+}
+
+// GetNodeData creates a number of goroutines that attempt to access a specified endpoint and return the
+// corresponding stats data in slice of interfaces which can be converted into a stricter format.
+func (nssc *NodeStatsSummaryClient) GetNodeData() ([]*stats.Summary, error) {
+	var bearerToken string
+	if !nssc.config.ProxyConfig.IsLocalProxy() {
+		token, err := nssc.loadBearerToken()
+		if err != nil {
+			return nil, err
+		}
+		bearerToken = token
+	}
+
+	size := nssc.config.ConcurrentPollers
+	nodes := getReadyNodes(nssc.cache)
+
+	work := func(n *clustercache.Node) *stats.Summary {
+		if n.SpecProviderID == "" {
+			log.Warnf("node ProviderID not set, skipping for %s", n.Name)
+			return nil
+		}
+
+		connections := nssc.connectionOptions(n)
+
+		resp, err := requestNodeData(connections, nssc.endpoint, bearerToken)
+		if err != nil {
+			log.Warnf("error retrieving node data: %s", err)
+			return nil
+		}
+
+		data, err := nodeResponseToStatSummary(resp)
+		if err != nil {
+			log.Warnf("error converting node data: %s", err)
+			return nil
+		}
+
+		return data
+	}
+
+	return worker.ConcurrentCollectWith(size, work, nodes), nil
+}
+
+// connectionOptions returns the connection methods that are allowed for this node based on config
+// settings and cluster composition
+func (nssc *NodeStatsSummaryClient) connectionOptions(n *clustercache.Node) []*NodeHttpConnection {
+	var connections []*NodeHttpConnection
+
+	clusterHostURL := nssc.clusterHostUrl
+	if nssc.config.ProxyConfig.IsLocalProxy() {
+		clusterHostURL = nssc.config.ProxyConfig.LocalProxy
+	}
+
+	proxyFormatter := NewNodeProxyFormatter(clusterHostURL, n.Name)
+	connections = append(connections, NewNodeHttpConnection(nssc.proxyClient, proxyFormatter))
+
+	// Do not allow direct connection to fargate nodes
+	if !nssc.config.ProxyConfig.ForceKubeProxy && !isFargateNode(n) {
+		directFormatter, err := NewDirectNodeFormatterFrom(n)
+		if err != nil {
+			log.Warnf("error reaching direct node api %s", err)
+		} else {
+			connections = append(connections, NewNodeHttpConnection(nssc.directClient, directFormatter))
+		}
+	}
+
+	return connections
+}
+
+// Note: These functions are client-independent and can be reused within another function
+// for a different datasource using the same config
+type nodeFetchData struct {
+	nodeName       string
+	ClusterHostURL string
+}
+
+// requestNodeData fetches summary and container data for the node
+func requestNodeData(connections []*NodeHttpConnection, endpoint string, bearerToken string) (*http.Response, error) {
+	var errs []error
+
+	// Fail after trying all connections the alloted number of retries
+	for _, connection := range connections {
+		data, err := connection.AttemptEndPoint(http.MethodGet, endpoint, bearerToken)
+		if err == nil {
+			return data, err
+		}
+
+		// otherwise, append the error to the list
+		errs = append(errs, fmt.Errorf("error retrieving node data from %s: %w", connection.formatter.FormatEndpoint(endpoint), err))
+	}
+
+	return nil, fmt.Errorf("problem getting node address: %v\n%w", endpoint, errors.Join(errs...))
+}
+
+// isFargateNode detects if it is a fargate node, disallowing direct connections
+func isFargateNode(n *clustercache.Node) bool {
+	v := n.Labels["eks.amazonaws.com/compute-type"]
+	if v == "fargate" {
+		log.Warnf("Fargate node found: %s", n.Name)
+		return true
+	}
+	return false
+}
+
+// getReadyNodes returns all nodes from a cache that have the ready status
+func getReadyNodes(cache clustercache.ClusterCache) []*clustercache.Node {
+	nodes := cache.GetAllNodes()
+
+	var readyNodes []*clustercache.Node
+	for _, n := range nodes {
+		nc := getNodeCondition(&n.Status, v1.NodeReady)
+		if nc != nil && nc.Type == v1.NodeReady {
+			readyNodes = append(readyNodes, n)
+		}
+	}
+
+	if len(readyNodes) == 0 {
+		log.Warnf("no ready nodes were found")
+		return nil
+	}
+
+	numReadyNodes := len(readyNodes)
+	numTotalNodes := len(nodes)
+	if numReadyNodes != numTotalNodes {
+		log.Warnf("%v out of %v were in a not ready state when retrieving nodes", numTotalNodes-numReadyNodes, numTotalNodes)
+	}
+
+	return readyNodes
+}
+
+// getNodeCondition extracts the provided condition from the given status and returns that, nil if not present.
+func getNodeCondition(status *v1.NodeStatus, conditionType v1.NodeConditionType) *v1.NodeCondition {
+	if status == nil {
+		return nil
+	}
+	for i := range status.Conditions {
+		if status.Conditions[i].Type == conditionType {
+			return &status.Conditions[i]
+		}
+	}
+	return nil
+}
+
+// NodeAddress returns the internal IP address and kubelet port of a given node
+func NodeAddress(node *clustercache.Node) (string, int32, error) {
+	// adapted from k8s.io/kubernetes/pkg/util/node
+	for _, addr := range node.Status.Addresses {
+		if addr.Type == v1.NodeInternalIP {
+			return addr.Address, node.Status.DaemonEndpoints.KubeletEndpoint.Port, nil
+		}
+	}
+	return "", 0, fmt.Errorf("could not find internal IP address for node %s ", node.Name)
+}
+
+func nodeResponseToStatSummary(resp *http.Response) (*stats.Summary, error) {
+	data := &stats.Summary{}
+	err := json.NewDecoder(resp.Body).Decode(&data)
+	if err == nil {
+		return data, nil
+	}
+
+	return nil, err
+}
+
+// loadBearerToken reads the service account token
+func (nssc *NodeStatsSummaryClient) loadBearerToken() (string, error) {
+	token, err := os.ReadFile(nssc.bearerTokenFile)
+	if err != nil {
+		return "", fmt.Errorf("could not read bearer token from file")
+	}
+	return string(token), nil
+}

+ 90 - 0
core/pkg/nodestats/request.go

@@ -0,0 +1,90 @@
+package nodestats
+
+import (
+	"fmt"
+	"math"
+	"net/http"
+	"strconv"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/log"
+)
+
+// HttpClient is an interface that captures the Do method of the http.Client. We use this interface to allow
+// mocking in tests.
+type HttpClient interface {
+	Do(req *http.Request) (*http.Response, error)
+}
+
+type NodeHttpClient struct {
+	client HttpClient
+}
+
+// NewNodeHttpClient creates a new NodeHttpClient with the provided HttpClient.
+func NewNodeHttpClient(client HttpClient) *NodeHttpClient {
+	return &NodeHttpClient{
+		client: client,
+	}
+}
+
+// AttemptEndPoint will hit a specified endpoint with as many retries as it is allotted.
+func (c *NodeHttpClient) AttemptEndPoint(method string, URL string, bearerToken string) (*http.Response, error) {
+	attempts := uint(1)
+
+	for i := uint(0); i < attempts; i++ {
+		if i > 0 {
+			time.Sleep(time.Duration(int64(math.Pow(2, float64(i)))) * time.Second)
+		}
+
+		data, err := c.makeRequest(method, URL, bearerToken)
+		if err == nil {
+			return data, nil
+		}
+		log.Warnf("Error making request to %s: %s", URL, err)
+	}
+
+	return nil, fmt.Errorf("requests to %v failed", URL)
+}
+
+// makeRequest will call out to an endpoint and attempt to decode the body into an existing
+// data type.
+func (c *NodeHttpClient) makeRequest(method string, URL string, bearerToken string) (*http.Response, error) {
+	request, err := http.NewRequest(method, URL, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if bearerToken != "" {
+		request.Header.Add("Authorization", "bearer "+bearerToken)
+	}
+
+	resp, err := c.client.Do(request)
+	if err != nil {
+		return nil, err
+	}
+
+	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
+		return nil, fmt.Errorf("invalid response %s", strconv.Itoa(resp.StatusCode))
+	}
+
+	return resp, nil
+}
+
+// NodeHttpConnect is a struct that represents a connection to a node using an http client and endpoint formatter.
+type NodeHttpConnection struct {
+	formatter NodeEndpointFormatter
+	client    *NodeHttpClient
+}
+
+// NewNodeHttpConnection creates a new HttpConnection with the provided NodeHttpClient and NodeEndpointFormatter.
+func NewNodeHttpConnection(client *NodeHttpClient, formatter NodeEndpointFormatter) *NodeHttpConnection {
+	return &NodeHttpConnection{
+		formatter: formatter,
+		client:    client,
+	}
+}
+
+// AttemptEndPoint will hit a specified endpoint leveraging the internal http client and formatter for the endpoint.
+func (nhc *NodeHttpConnection) AttemptEndPoint(method string, url string, bearerToken string) (*http.Response, error) {
+	return nhc.client.AttemptEndPoint(method, nhc.formatter.FormatEndpoint(url), bearerToken)
+}

+ 15 - 15
core/pkg/opencost/allocation_test.go

@@ -3469,7 +3469,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(nil)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "unknown" {
+	} else if name != "unknown" {
 		t.Fatalf("determineSharingName: expected \"unknown\"; actual \"%s\"", name)
 	}
 
@@ -3478,7 +3478,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "unknown" {
+	} else if name != "unknown" {
 		t.Fatalf("determineSharingName: expected \"unknown\"; actual \"%s\"", name)
 	}
 
@@ -3487,7 +3487,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "namespace1" {
+	} else if name != "namespace1" {
 		t.Fatalf("determineSharingName: expected \"namespace1\"; actual \"%s\"", name)
 	}
 
@@ -3496,7 +3496,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "unknown" {
+	} else if name != "unknown" {
 		t.Fatalf("determineSharingName: expected \"unknown\"; actual \"%s\"", name)
 	}
 
@@ -3508,7 +3508,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "app1" {
+	} else if name != "app1" {
 		t.Fatalf("determineSharingName: expected \"app1\"; actual \"%s\"", name)
 	}
 
@@ -3519,7 +3519,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "app1" {
+	} else if name != "app1" {
 		t.Fatalf("determineSharingName: expected \"app1\"; actual \"%s\"", name)
 	}
 
@@ -3530,7 +3530,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "unknown" {
+	} else if name != "unknown" {
 		t.Fatalf("determineSharingName: expected \"unknown\"; actual \"%s\"", name)
 	}
 
@@ -3542,7 +3542,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "app1" {
+	} else if name != "app1" {
 		t.Fatalf("determineSharingName: expected \"app1\"; actual \"%s\"", name)
 	}
 
@@ -3554,7 +3554,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "app1" {
+	} else if name != "app1" {
 		t.Fatalf("determineSharingName: expected \"app1\"; actual \"%s\"", name)
 	}
 
@@ -3566,7 +3566,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "unknown" {
+	} else if name != "unknown" {
 		t.Fatalf("determineSharingName: expected \"unknown\"; actual \"%s\"", name)
 	}
 
@@ -3583,7 +3583,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "app1" {
+	} else if name != "app1" {
 		t.Fatalf("determineSharingName: expected \"app1\"; actual \"%s\"", name)
 	}
 
@@ -3600,7 +3600,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "env1" {
+	} else if name != "env1" {
 		t.Fatalf("determineSharingName: expected \"env1\"; actual \"%s\"", name)
 	}
 
@@ -3611,7 +3611,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "namespace1" {
+	} else if name != "namespace1" {
 		t.Fatalf("determineSharingName: expected \"namespace1\"; actual \"%s\"", name)
 	}
 
@@ -3622,7 +3622,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "namespace2" {
+	} else if name != "namespace2" {
 		t.Fatalf("determineSharingName: expected \"namespace2\"; actual \"%s\"", name)
 	}
 
@@ -3632,7 +3632,7 @@ func Test_DetermineSharingName(t *testing.T) {
 	name, err = alloc.determineSharingName(options)
 	if err != nil {
 		t.Fatalf("determineSharingName: expected no error; actual \"%s\"", err)
-	} else if err != nil || name != "unknown" {
+	} else if name != "unknown" {
 		t.Fatalf("determineSharingName: expected \"unknown\"; actual \"%s\"", name)
 	}
 }

+ 0 - 16
core/pkg/opencost/common.go

@@ -1,16 +0,0 @@
-package opencost
-
-// Pair is a generic struct containing a pair of instances, one of each type similar to std::pair
-type Pair[T any, U any] struct {
-	First  T
-	Second U
-}
-
-// Creates a new pair struct containing the provided parameters. This is useful for creating types
-// capable of representing common paired types (result, error), (result, bool), etc...
-func NewPair[T any, U any](first T, second U) Pair[T, U] {
-	return Pair[T, U]{
-		First:  first,
-		Second: second,
-	}
-}

+ 43 - 0
core/pkg/opencost/exporter/allocation/source.go

@@ -0,0 +1,43 @@
+package allocation
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/pipelines"
+)
+
+type AllocationSource interface {
+	ComputeAllocation(start, end time.Time, resolution time.Duration) (*opencost.AllocationSet, error)
+}
+
+type AllocationComputeSource struct {
+	src AllocationSource
+}
+
+// NewAllocationComputeSource creates an `exporter.ComputeSource[opencost.AllocationSet]` implementation
+func NewAllocationComputeSource(src AllocationSource) exporter.ComputeSource[opencost.AllocationSet] {
+	return &AllocationComputeSource{
+		src: src,
+	}
+}
+
+// CanCompute should return true iff the ComputeSource can effectively act as
+// a source of T data for the given time range. For example, a ComputeSource
+// with two-day coverage cannot fulfill a range from three days ago, and should
+// not be left to return an error in Compute. Instead, it should report that is
+// cannot compute and allow another Source to handle the computation.
+func (acs *AllocationComputeSource) CanCompute(start, end time.Time) bool {
+	return true
+}
+
+// Compute should compute a single T for the given time range, optionally using the given resolution.
+func (acs *AllocationComputeSource) Compute(start, end time.Time, resolution time.Duration) (*opencost.AllocationSet, error) {
+	return acs.src.ComputeAllocation(start, end, resolution)
+}
+
+// Name returns the name of the ComputeSource
+func (acs *AllocationComputeSource) Name() string {
+	return pipelines.AllocationPipelineName
+}

+ 43 - 0
core/pkg/opencost/exporter/asset/source.go

@@ -0,0 +1,43 @@
+package asset
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/pipelines"
+)
+
+type AssetSource interface {
+	ComputeAssets(start, end time.Time) (*opencost.AssetSet, error)
+}
+
+type AssetsComputeSource struct {
+	src AssetSource
+}
+
+// NewAssetsComputeSource creates an `exporter.ComputeSource[opencost.AssetSet]` implementation
+func NewAssetsComputeSource(src AssetSource) exporter.ComputeSource[opencost.AssetSet] {
+	return &AssetsComputeSource{
+		src: src,
+	}
+}
+
+// CanCompute should return true iff the ComputeSource can effectively act as
+// a source of T data for the given time range. For example, a ComputeSource
+// with two-day coverage cannot fulfill a range from three days ago, and should
+// not be left to return an error in Compute. Instead, it should report that is
+// cannot compute and allow another Source to handle the computation.
+func (acs *AssetsComputeSource) CanCompute(start, end time.Time) bool {
+	return true
+}
+
+// Compute should compute a single T for the given time range, optionally using the given resolution.
+func (acs *AssetsComputeSource) Compute(start, end time.Time, resolution time.Duration) (*opencost.AssetSet, error) {
+	return acs.src.ComputeAssets(start, end)
+}
+
+// Name returns the name of the ComputeSource
+func (acs *AssetsComputeSource) Name() string {
+	return pipelines.AssetsPipelineName
+}

+ 151 - 0
core/pkg/opencost/exporter/controllers.go

@@ -0,0 +1,151 @@
+package exporter
+
+import (
+	"time"
+
+	export "github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/opencost/exporter/allocation"
+	"github.com/opencost/opencost/core/pkg/opencost/exporter/asset"
+	"github.com/opencost/opencost/core/pkg/opencost/exporter/networkinsight"
+	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
+)
+
+// ComputePipelineSource is an interface that defines methods for computing all pipeline data.
+// For all intents and purposes, this represents costmodel.CostModel. To interface allows tests to
+// mock the costmodel.CostModel and return a different source for the pipeline.
+type ComputePipelineSource interface {
+	allocation.AllocationSource
+	asset.AssetSource
+	networkinsight.NetworkInsightSource
+
+	GetDataSource() source.OpenCostDataSource
+}
+
+// PipelinesExportConfig is a configuration struct that contains the export resolutions for
+// allocation, assets, and network insights pipelines.
+type PipelinesExportConfig struct {
+	AllocationPiplineResolutions      []time.Duration
+	AssetPipelineResolutons           []time.Duration
+	NetworkInsightPipelineResolutions []time.Duration
+}
+
+// defaultPipelineExportResolutions returns the default export configuration for the pipeline
+// which is set to export hourly and daily.
+func defaultPipelineExportResolutions() []time.Duration {
+	return []time.Duration{
+		time.Hour,
+		24 * time.Hour,
+	}
+}
+
+// DefaultPipelinesExportConfig returns the default export configuration for all pipelines
+// which is set to export hourly and daily for allocations, assets, and network insights.
+func DefaultPipelinesExportConfig() *PipelinesExportConfig {
+	return &PipelinesExportConfig{
+		AllocationPiplineResolutions:      defaultPipelineExportResolutions(),
+		AssetPipelineResolutons:           defaultPipelineExportResolutions(),
+		NetworkInsightPipelineResolutions: defaultPipelineExportResolutions(),
+	}
+}
+
+// PipelineExportControllers is a facade that contains the export controllers for allocations, assets, and network insights.
+type PipelineExportControllers struct {
+	AllocationExportController     *export.ComputeExportControllerGroup[opencost.AllocationSet]
+	AssetExportController          *export.ComputeExportControllerGroup[opencost.AssetSet]
+	NetworkInsightExportController *export.ComputeExportControllerGroup[opencost.NetworkInsightSet]
+}
+
+// NewPipelineExportControllers creates a new PipelineExportControllers instance with the given cluster ID, storage implementation, cost model, and configuration.
+// Setting the config to nil will use the default hourly and daily export resolutions for each pipeline.
+func NewPipelineExportControllers(clusterId string, store storage.Storage, cm ComputePipelineSource, config *PipelinesExportConfig) *PipelineExportControllers {
+	if config == nil {
+		config = DefaultPipelinesExportConfig()
+	}
+
+	mins := int(cm.GetDataSource().Resolution().Minutes())
+	if mins <= 0 {
+		mins = 1
+	}
+
+	// minimum source/query resolution
+	sourceResolution := time.Duration(mins) * time.Minute
+
+	// allocation sources and exporters
+	allocSource := allocation.NewAllocationComputeSource(cm)
+	allocExportControllers := []*export.ComputeExportController[opencost.AllocationSet]{}
+
+	for _, res := range config.AllocationPiplineResolutions {
+		if res < sourceResolution {
+			log.Warnf("Configured allocation pipeline resolution %dm is less than source resolution %dm. Not configuring the exporter for this resolution.", int64(res.Minutes()), int64(sourceResolution.Minutes()))
+			continue
+		}
+
+		allocController, err := NewComputePipelineExportController(clusterId, store, allocSource, res, sourceResolution)
+		if err != nil {
+			log.Errorf("Failed to create allocation export controller for resolution: %s - %v", timeutil.DurationString(res), err)
+			continue
+		}
+
+		allocExportControllers = append(allocExportControllers, allocController)
+	}
+
+	// asset sources and exporters
+	assetSource := asset.NewAssetsComputeSource(cm)
+	assetExportControllers := []*export.ComputeExportController[opencost.AssetSet]{}
+
+	for _, res := range config.AssetPipelineResolutons {
+		if res < sourceResolution {
+			log.Warnf("Configured asset pipeline resolution %dm is less than source resolution %dm. Not configuring the exporter for this resolution.", int64(res.Minutes()), int64(sourceResolution.Minutes()))
+			continue
+		}
+
+		assetController, err := NewComputePipelineExportController(clusterId, store, assetSource, res, sourceResolution)
+		if err != nil {
+			log.Errorf("Failed to create asset export controller for resolution: %s - %v", timeutil.DurationString(res), err)
+			continue
+		}
+
+		assetExportControllers = append(assetExportControllers, assetController)
+	}
+
+	// network insights sources and exporters
+	networkInsightSource := networkinsight.NewNetworkInsightsComputeSource(cm)
+	networkInsightExportControllers := []*export.ComputeExportController[opencost.NetworkInsightSet]{}
+
+	for _, res := range config.NetworkInsightPipelineResolutions {
+		if res < sourceResolution {
+			log.Warnf("Configured network insight pipeline resolution %dm is less than source resolution %dm. Not configuring the exporter for this resolution.", int64(res.Minutes()), int64(sourceResolution.Minutes()))
+			continue
+		}
+
+		networkInsightController, err := NewComputePipelineExportController(clusterId, store, networkInsightSource, res, sourceResolution)
+		if err != nil {
+			log.Errorf("Failed to create network insight export controller for resolution: %s - %v", timeutil.DurationString(res), err)
+			continue
+		}
+
+		networkInsightExportControllers = append(networkInsightExportControllers, networkInsightController)
+	}
+
+	return &PipelineExportControllers{
+		AllocationExportController:     export.NewComputeExportControllerGroup(allocExportControllers...),
+		AssetExportController:          export.NewComputeExportControllerGroup(assetExportControllers...),
+		NetworkInsightExportController: export.NewComputeExportControllerGroup(networkInsightExportControllers...),
+	}
+}
+
+func (pec *PipelineExportControllers) Start(interval time.Duration) {
+	pec.AllocationExportController.Start(interval)
+	pec.AssetExportController.Start(interval)
+	pec.NetworkInsightExportController.Start(interval)
+}
+
+func (pec *PipelineExportControllers) Stop() {
+	pec.AllocationExportController.Stop()
+	pec.AssetExportController.Stop()
+	pec.NetworkInsightExportController.Stop()
+}

+ 397 - 0
core/pkg/opencost/exporter/exporter_test.go

@@ -0,0 +1,397 @@
+package exporter
+
+import (
+	"testing"
+	"time"
+
+	"github.com/julienschmidt/httprouter"
+	"github.com/opencost/opencost/core/pkg/clusters"
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/pipelines"
+	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/storage"
+)
+
+const (
+	TestClusterId  = "test-cluster"
+	TestResolution = 24 * time.Hour
+)
+
+type GenerateMockSet[T any] func(start, end time.Time) *T
+
+type MockSource[T any] struct {
+	generate GenerateMockSet[T]
+}
+
+func (ms *MockSource[T]) CanCompute(start, end time.Time) bool {
+	return true
+}
+func (ms *MockSource[T]) Compute(start, end time.Time, resolution time.Duration) (*T, error) {
+	return ms.generate(start, end), nil
+}
+func (ms *MockSource[T]) Name() string {
+	return pipelines.NameFor[T]()
+}
+
+func NewMockAllocationSource() exporter.ComputeSource[opencost.AllocationSet] {
+	return &MockSource[opencost.AllocationSet]{
+		generate: func(start, end time.Time) *opencost.AllocationSet { return opencost.GenerateMockAllocationSet(start) },
+	}
+}
+
+func NewMockAssetSource() exporter.ComputeSource[opencost.AssetSet] {
+	return &MockSource[opencost.AssetSet]{
+		generate: func(start, end time.Time) *opencost.AssetSet {
+			return opencost.GenerateMockAssetSet(start, TestResolution)
+		},
+	}
+}
+
+func NewMockNetworkInsightSource() exporter.ComputeSource[opencost.NetworkInsightSet] {
+	return &MockSource[opencost.NetworkInsightSet]{
+		generate: func(start, end time.Time) *opencost.NetworkInsightSet {
+			return opencost.GenerateMockNetworkInsightSet(start, end)
+		},
+	}
+}
+
+type MockDataSource struct {
+	resolution time.Duration
+}
+
+func NewMockDataSource() *MockDataSource {
+	return NewMockDataSourceWith(time.Minute)
+}
+
+func NewMockDataSourceWith(resolution time.Duration) *MockDataSource {
+	return &MockDataSource{
+		resolution: resolution,
+	}
+}
+
+func (mds *MockDataSource) RegisterEndPoints(router *httprouter.Router)                   {}
+func (mds *MockDataSource) RegisterDiagnostics(diagService diagnostics.DiagnosticService) {}
+func (mds *MockDataSource) Metrics() source.MetricsQuerier                                { return nil }
+func (mds *MockDataSource) ClusterMap() clusters.ClusterMap                               { return nil }
+func (mds *MockDataSource) ClusterInfo() clusters.ClusterInfoProvider                     { return nil }
+func (mds *MockDataSource) BatchDuration() time.Duration                                  { return time.Hour * 20000 }
+func (mds *MockDataSource) Resolution() time.Duration                                     { return mds.resolution }
+
+type MockPipelineComputeSource struct {
+	allocSource exporter.ComputeSource[opencost.AllocationSet]
+	assetSource exporter.ComputeSource[opencost.AssetSet]
+	netSource   exporter.ComputeSource[opencost.NetworkInsightSet]
+	ds          *MockDataSource
+}
+
+func NewMockPipelineComputeSource() *MockPipelineComputeSource {
+	return &MockPipelineComputeSource{
+		allocSource: NewMockAllocationSource(),
+		assetSource: NewMockAssetSource(),
+		netSource:   NewMockNetworkInsightSource(),
+		ds:          NewMockDataSource(),
+	}
+}
+
+func NewMockPipelineComputeSourceWith(srcResolution time.Duration) *MockPipelineComputeSource {
+	return &MockPipelineComputeSource{
+		allocSource: NewMockAllocationSource(),
+		assetSource: NewMockAssetSource(),
+		netSource:   NewMockNetworkInsightSource(),
+		ds:          NewMockDataSourceWith(srcResolution),
+	}
+}
+
+func (mpcs *MockPipelineComputeSource) ComputeAllocation(start, end time.Time, resolution time.Duration) (*opencost.AllocationSet, error) {
+	return mpcs.allocSource.Compute(start, end, resolution)
+}
+func (mpcs *MockPipelineComputeSource) ComputeAssets(start, end time.Time) (*opencost.AssetSet, error) {
+	return mpcs.assetSource.Compute(start, end, TestResolution)
+}
+func (mpcs *MockPipelineComputeSource) ComputeNetworkInsights(start, end time.Time, resolution time.Duration) (*opencost.NetworkInsightSet, error) {
+	return mpcs.netSource.Compute(start, end, resolution)
+}
+func (mpcs *MockPipelineComputeSource) GetDataSource() source.OpenCostDataSource {
+	return mpcs.ds
+}
+
+type UnknownSet struct{}
+
+func (u *UnknownSet) MarshalBinary() ([]byte, error) {
+	return []byte{}, nil
+}
+func (u *UnknownSet) UnmarshalBinary(data []byte) error {
+	return nil
+}
+func (u *UnknownSet) IsEmpty() bool {
+	return false
+}
+
+type PipelineData[T any] interface {
+	UnmarshalBinary(data []byte) error
+	IsEmpty() bool
+	*T
+}
+
+func ptr[T any](v T) *T {
+	return &v
+}
+
+func TestExporters(t *testing.T) {
+	t.Run("allocation exporter", func(t *testing.T) {
+		allocSource := NewMockAllocationSource()
+		memStore := storage.NewMemoryStorage()
+		p, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AllocationPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
+		allocExporter, err := NewComputePipelineExporter[opencost.AllocationSet](TestClusterId, TestResolution, memStore)
+		if err != nil {
+			t.Fatalf("failed to create allocation exporter: %v", err)
+		}
+
+		end := time.Now().UTC().Truncate(TestResolution)
+		start := end.Add(-TestResolution)
+
+		data, err := allocSource.Compute(start, end, TestResolution)
+		if err != nil {
+			t.Fatalf("failed to compute allocation data: %v", err)
+		}
+
+		err = allocExporter.Export(opencost.NewClosedWindow(start, end), data)
+		if err != nil {
+			t.Fatalf("failed to export allocation data: %v", err)
+		}
+
+		validateFileCreation[opencost.AllocationSet](t, memStore, p, start, end)
+	})
+
+	t.Run("asset exporter", func(t *testing.T) {
+		assetSource := NewMockAssetSource()
+		memStore := storage.NewMemoryStorage()
+		p, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AssetsPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
+		assetExporter, err := NewComputePipelineExporter[opencost.AssetSet](TestClusterId, TestResolution, memStore)
+		if err != nil {
+			t.Fatalf("failed to create allocation exporter: %v", err)
+		}
+
+		end := time.Now().UTC().Truncate(TestResolution)
+		start := end.Add(-TestResolution)
+
+		data, err := assetSource.Compute(start, end, TestResolution)
+		if err != nil {
+			t.Fatalf("failed to compute asset data: %v", err)
+		}
+
+		err = assetExporter.Export(opencost.NewClosedWindow(start, end), data)
+		if err != nil {
+			t.Fatalf("failed to export asset data: %v", err)
+		}
+
+		validateFileCreation[opencost.AssetSet](t, memStore, p, start, end)
+	})
+
+	t.Run("network insight exporter", func(t *testing.T) {
+		netInsightSource := NewMockNetworkInsightSource()
+		memStore := storage.NewMemoryStorage()
+		p, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
+		netInsightExporter, err := NewComputePipelineExporter[opencost.NetworkInsightSet](TestClusterId, TestResolution, memStore)
+		if err != nil {
+			t.Fatalf("failed to create net insights exporter: %v", err)
+		}
+
+		end := time.Now().UTC().Truncate(TestResolution)
+		start := end.Add(-TestResolution)
+
+		data, err := netInsightSource.Compute(start, end, TestResolution)
+		if err != nil {
+			t.Fatalf("failed to compute net insights data: %v", err)
+		}
+
+		err = netInsightExporter.Export(opencost.NewClosedWindow(start, end), data)
+		if err != nil {
+			t.Fatalf("failed to export net insights data: %v", err)
+		}
+
+		validateFileCreation[opencost.NetworkInsightSet](t, memStore, p, start, end)
+	})
+
+	t.Run("unknown exporter", func(t *testing.T) {
+		memStore := storage.NewMemoryStorage()
+
+		// Invalid pipeline
+		_, err := NewComputePipelineExporter[UnknownSet](TestClusterId, TestResolution, memStore)
+		if err == nil {
+			t.Fatalf("expected error creating unknown pipeline exporter, got nil")
+		}
+
+		// Invalid cluster id
+		_, err = NewComputePipelineExporter[opencost.AllocationSet]("", TestResolution, memStore)
+		if err == nil {
+			t.Fatalf("expected error creating allocation pipeline exporter with empty cluster id, got nil")
+		}
+	})
+}
+
+func TestPipelineExportControllers(t *testing.T) {
+	t.Run("with custom export config", func(t *testing.T) {
+		pipelineComputeSource := NewMockPipelineComputeSource()
+		memStore := storage.NewMemoryStorage()
+
+		exportControllers := NewPipelineExportControllers(TestClusterId, memStore, pipelineComputeSource, &PipelinesExportConfig{
+			AllocationPiplineResolutions:      []time.Duration{TestResolution},
+			AssetPipelineResolutons:           []time.Duration{TestResolution},
+			NetworkInsightPipelineResolutions: []time.Duration{TestResolution},
+		})
+
+		start := time.Now().UTC().Truncate(TestResolution)
+		end := start.Add(TestResolution)
+
+		// allow a single export to occur
+		exportControllers.Start(time.Second)
+		time.Sleep(time.Second + (750 * time.Millisecond))
+		exportControllers.Stop()
+
+		allocPath, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AllocationPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create allocations path formatter: %v", err)
+		}
+		assetPath, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AssetsPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create assets path formatter: %v", err)
+		}
+		netPath, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create net insights path formatter: %v", err)
+		}
+
+		validateFileCreation[opencost.AllocationSet](t, memStore, allocPath, start, end)
+		validateFileCreation[opencost.AssetSet](t, memStore, assetPath, start, end)
+		validateFileCreation[opencost.NetworkInsightSet](t, memStore, netPath, start, end)
+	})
+
+	t.Run("with auto-set to minute resolution", func(t *testing.T) {
+		pipelineComputeSource := NewMockPipelineComputeSourceWith(30 * time.Second)
+		memStore := storage.NewMemoryStorage()
+
+		exportControllers := NewPipelineExportControllers(TestClusterId, memStore, pipelineComputeSource, &PipelinesExportConfig{
+			AllocationPiplineResolutions:      []time.Duration{TestResolution},
+			AssetPipelineResolutons:           []time.Duration{TestResolution},
+			NetworkInsightPipelineResolutions: []time.Duration{TestResolution},
+		})
+
+		start := time.Now().UTC().Truncate(TestResolution)
+		end := start.Add(TestResolution)
+
+		// allow a single export to occur
+		exportControllers.Start(time.Second)
+		time.Sleep(time.Second + (750 * time.Millisecond))
+		exportControllers.Stop()
+
+		allocPath, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AllocationPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create allocations path formatter: %v", err)
+		}
+		assetPath, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.AssetsPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create assets path formatter: %v", err)
+		}
+		netPath, err := pathing.NewBingenStoragePathFormatter("federated", TestClusterId, pipelines.NetworkInsightPipelineName, ptr(TestResolution))
+		if err != nil {
+			t.Fatalf("failed to create net insights path formatter: %v", err)
+		}
+
+		validateFileCreation[opencost.AllocationSet](t, memStore, allocPath, start, end)
+		validateFileCreation[opencost.AssetSet](t, memStore, assetPath, start, end)
+		validateFileCreation[opencost.NetworkInsightSet](t, memStore, netPath, start, end)
+	})
+
+	t.Run("with default export config", func(t *testing.T) {
+		pipelineComputeSource := NewMockPipelineComputeSource()
+		memStore := storage.NewMemoryStorage()
+
+		exportControllers := NewPipelineExportControllers(TestClusterId, memStore, pipelineComputeSource, nil)
+
+		if len(exportControllers.AllocationExportController.Resolutions()) != 2 {
+			t.Fatalf("expected 2 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
+		}
+		if len(exportControllers.AssetExportController.Resolutions()) != 2 {
+			t.Fatalf("expected 2 asset resolutions, got %d", len(exportControllers.AssetExportController.Resolutions()))
+		}
+		if len(exportControllers.NetworkInsightExportController.Resolutions()) != 2 {
+			t.Fatalf("expected 2 network insight resolutions, got %d", len(exportControllers.NetworkInsightExportController.Resolutions()))
+		}
+	})
+
+	t.Run("with 2day source resolution", func(t *testing.T) {
+		// make compute source use a source resolution of 48 hours
+		pipelineComputeSource := NewMockPipelineComputeSourceWith(48 * time.Hour)
+		memStore := storage.NewMemoryStorage()
+
+		exportControllers := NewPipelineExportControllers(TestClusterId, memStore, pipelineComputeSource, nil)
+
+		if len(exportControllers.AllocationExportController.Resolutions()) != 0 {
+			t.Fatalf("expected 0 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
+		}
+		if len(exportControllers.AssetExportController.Resolutions()) != 0 {
+			t.Fatalf("expected 0 asset resolutions, got %d", len(exportControllers.AssetExportController.Resolutions()))
+		}
+		if len(exportControllers.NetworkInsightExportController.Resolutions()) != 0 {
+			t.Fatalf("expected 0 network insight resolutions, got %d", len(exportControllers.NetworkInsightExportController.Resolutions()))
+		}
+	})
+
+	t.Run("with empty cluster id", func(t *testing.T) {
+		pipelineComputeSource := NewMockPipelineComputeSource()
+		memStore := storage.NewMemoryStorage()
+
+		exportControllers := NewPipelineExportControllers("", memStore, pipelineComputeSource, nil)
+
+		if len(exportControllers.AllocationExportController.Resolutions()) != 0 {
+			t.Fatalf("expected 0 allocation resolutions, got %d", len(exportControllers.AllocationExportController.Resolutions()))
+		}
+		if len(exportControllers.AssetExportController.Resolutions()) != 0 {
+			t.Fatalf("expected 0 asset resolutions, got %d", len(exportControllers.AssetExportController.Resolutions()))
+		}
+		if len(exportControllers.NetworkInsightExportController.Resolutions()) != 0 {
+			t.Fatalf("expected 0 network insight resolutions, got %d", len(exportControllers.NetworkInsightExportController.Resolutions()))
+		}
+	})
+}
+
+// test helper function that will load a path from a storage implementation and ensure that the file is not empty and can be decoded, etc...
+func validateFileCreation[T any, U PipelineData[T]](t *testing.T, memStore storage.Storage, p pathing.StoragePathFormatter[opencost.Window], start, end time.Time) {
+	t.Helper()
+
+	expectedPath := p.ToFullPath("", opencost.NewClosedWindow(start, end), "")
+
+	fileContents, err := memStore.Read(expectedPath)
+	if err != nil {
+		t.Fatalf("failed to read file %s: %v", expectedPath, err)
+	}
+	if len(fileContents) == 0 {
+		t.Fatalf("file %s is empty", expectedPath)
+	}
+
+	var set U = new(T)
+	err = set.UnmarshalBinary(fileContents)
+	if err != nil {
+		t.Fatalf("failed to unmarshal data: %v", err)
+	}
+
+	if set.IsEmpty() {
+		t.Fatalf("data set is empty")
+	}
+}

+ 55 - 0
core/pkg/opencost/exporter/exporters.go

@@ -0,0 +1,55 @@
+package exporter
+
+import (
+	"fmt"
+	"time"
+
+	export "github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/exporter/pathing"
+	"github.com/opencost/opencost/core/pkg/exporter/validator"
+	"github.com/opencost/opencost/core/pkg/pipelines"
+	"github.com/opencost/opencost/core/pkg/storage"
+	"github.com/opencost/opencost/core/pkg/util/typeutil"
+)
+
+// NewComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
+// by window for a specific pipeline.
+func NewComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	clusterId string,
+	resolution time.Duration,
+	store storage.Storage,
+) (export.ComputeExporter[T], error) {
+	pipelineName := pipelines.NameFor[T]()
+	if pipelineName == "" {
+		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
+	}
+
+	pathing, err := pathing.NewBingenStoragePathFormatter("federated", clusterId, pipelineName, &resolution)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create path formatter: %w", err)
+	}
+
+	return export.NewComputeStorageExporter(
+		pathing,
+		export.NewBingenEncoder[T, U](),
+		store,
+		validator.NewSetValidator[T, S](resolution),
+	), nil
+}
+
+// NewComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to export computed data
+// using the provided source, storage, resolution, and source resolution.
+func NewComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	clusterId string,
+	store storage.Storage,
+	source export.ComputeSource[T],
+	resolution time.Duration,
+	sourceResolution time.Duration,
+) (*export.ComputeExportController[T], error) {
+	exporter, err := NewComputePipelineExporter[T, U, S](clusterId, resolution, store)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
+	}
+
+	return export.NewComputeExportController(source, exporter, resolution, sourceResolution), nil
+}

+ 43 - 0
core/pkg/opencost/exporter/networkinsight/source.go

@@ -0,0 +1,43 @@
+package networkinsight
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/exporter"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/pipelines"
+)
+
+type NetworkInsightSource interface {
+	ComputeNetworkInsights(start, end time.Time, resolution time.Duration) (*opencost.NetworkInsightSet, error)
+}
+
+type NetworkInsightsComputeSource struct {
+	src NetworkInsightSource
+}
+
+// NewNetworkInsightsComputeSource creates an `exporter.ComputeSource[opencost.NetworkInsightSet]` implementation
+func NewNetworkInsightsComputeSource(src NetworkInsightSource) exporter.ComputeSource[opencost.NetworkInsightSet] {
+	return &NetworkInsightsComputeSource{
+		src: src,
+	}
+}
+
+// CanCompute should return true iff the ComputeSource can effectively act as
+// a source of T data for the given time range. For example, a ComputeSource
+// with two-day coverage cannot fulfill a range from three days ago, and should
+// not be left to return an error in Compute. Instead, it should report that is
+// cannot compute and allow another Source to handle the computation.
+func (acs *NetworkInsightsComputeSource) CanCompute(start, end time.Time) bool {
+	return true
+}
+
+// Compute should compute a single T for the given time range, optionally using the given resolution.
+func (acs *NetworkInsightsComputeSource) Compute(start, end time.Time, resolution time.Duration) (*opencost.NetworkInsightSet, error) {
+	return acs.src.ComputeNetworkInsights(start, end, resolution)
+}
+
+// Name returns the name of the ComputeSource
+func (acs *NetworkInsightsComputeSource) Name() string {
+	return pipelines.NetworkInsightPipelineName
+}

+ 0 - 36
core/pkg/opencost/window.go

@@ -15,12 +15,6 @@ import (
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 )
 
-const (
-	minutesPerDay  = 60 * 24
-	minutesPerHour = 60
-	hoursPerDay    = 24
-)
-
 var (
 	durationRegex       = regexp.MustCompile(`^(\d+)(m|h|d|w)$`)
 	durationOffsetRegex = regexp.MustCompile(`^(\d+)(m|h|d|w) offset (\d+)(m|h|d|w)$`)
@@ -29,40 +23,10 @@ var (
 	rfcRegex            = regexp.MustCompile(fmt.Sprintf(`(%s),(%s)`, rfc3339, rfc3339))
 	timestampPairRegex  = regexp.MustCompile(`^(\d+)[,|-](\d+)$`)
 
-	tOffsetLock sync.Mutex
-	tOffset     *time.Duration
-
 	utcOffsetLock sync.Mutex
 	utcOffsetDur  *time.Duration
 )
 
-// get and cache the thanos offset duration.
-// TODO: Due to dependencies here, we have to drag a non-core config option into
-// TOOD: core scope. Any solution here would be a one-off until we can generalize
-// TODO: global configuration options.
-func thanosOffset() time.Duration {
-	tOffsetLock.Lock()
-	defer tOffsetLock.Unlock()
-
-	if tOffset == nil {
-		d, err := time.ParseDuration(env.Get("THANOS_QUERY_OFFSET", "3h"))
-		if err != nil {
-			d = 0
-		}
-
-		tOffset = &d
-	}
-
-	return *tOffset
-}
-
-// returns true if thanos is enabled
-// TODO: Same note as thanosOffset above - temporary work-around until more
-// TODO: generalized global configuration.
-func isThanosEnabled() bool {
-	return env.GetBool("THANOS_ENABLED", false)
-}
-
 // returns the configured utc offset as a duration
 // TODO: Same as the above options -- we should provide a one-time initialization configuration
 // TODO: for these values, or deprecate their use.

+ 1 - 1
core/pkg/opencost/window_test.go

@@ -1166,7 +1166,7 @@ func TestMarshalUnmarshal(t *testing.T) {
 			}
 
 			if diff := cmp.Diff(c.w, unmarshaledW); len(diff) > 0 {
-				t.Errorf(diff)
+				t.Errorf("%s", diff)
 			}
 		})
 	}

+ 46 - 0
core/pkg/pipelines/name.go

@@ -0,0 +1,46 @@
+package pipelines
+
+import (
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/util/typeutil"
+)
+
+const (
+	AllocationPipelineName     string = "allocations"
+	AssetsPipelineName         string = "assets"
+	CloudCostsPipelineName     string = "cloudcosts"
+	NetworkInsightPipelineName string = "networkinsights"
+	CustomCostsPipelineName    string = "customcosts"
+)
+
+var nameByType map[string]string
+
+// initializes the package, creates type -> pipeline mapping
+func init() {
+	allocSetKey := typeutil.TypeOf[opencost.AllocationSet]()
+	allocKey := typeutil.TypeOf[opencost.Allocation]()
+
+	assetSetKey := typeutil.TypeOf[opencost.AssetSet]()
+	assetKey := typeutil.TypeOf[opencost.Asset]()
+
+	cloudCostsSetKey := typeutil.TypeOf[opencost.CloudCostSet]()
+	cloudCostKey := typeutil.TypeOf[opencost.CloudCost]()
+
+	networkInsightSetKey := typeutil.TypeOf[opencost.NetworkInsightSet]()
+	networkInsightKey := typeutil.TypeOf[opencost.NetworkInsight]()
+
+	nameByType = map[string]string{
+		allocSetKey:          AllocationPipelineName,
+		allocKey:             AllocationPipelineName,
+		assetSetKey:          AssetsPipelineName,
+		assetKey:             AssetsPipelineName,
+		cloudCostsSetKey:     CloudCostsPipelineName,
+		cloudCostKey:         CloudCostsPipelineName,
+		networkInsightSetKey: NetworkInsightPipelineName,
+		networkInsightKey:    NetworkInsightPipelineName,
+	}
+}
+
+func NameFor[T any]() string {
+	return nameByType[typeutil.TypeOf[T]()]
+}

+ 142 - 0
core/pkg/source/datasource.go

@@ -0,0 +1,142 @@
+package source
+
+import (
+	"time"
+
+	"github.com/julienschmidt/httprouter"
+	"github.com/opencost/opencost/core/pkg/clusters"
+	"github.com/opencost/opencost/core/pkg/diagnostics"
+)
+
+type MetricsQuerier interface {
+	// Cluster Disks
+	QueryPVActiveMinutes(start, end time.Time) *Future[PVActiveMinutesResult]
+	QueryPVUsedAverage(start, end time.Time) *Future[PVUsedAvgResult]
+	QueryPVUsedMax(start, end time.Time) *Future[PVUsedMaxResult]
+
+	// Local Cluster Disks
+	QueryLocalStorageActiveMinutes(start, end time.Time) *Future[LocalStorageActiveMinutesResult]
+	QueryLocalStorageCost(start, end time.Time) *Future[LocalStorageCostResult]
+	QueryLocalStorageUsedCost(start, end time.Time) *Future[LocalStorageUsedCostResult]
+	QueryLocalStorageUsedAvg(start, end time.Time) *Future[LocalStorageUsedAvgResult]
+	QueryLocalStorageUsedMax(start, end time.Time) *Future[LocalStorageUsedMaxResult]
+	QueryLocalStorageBytes(start, end time.Time) *Future[LocalStorageBytesResult]
+
+	// Nodes
+	QueryNodeActiveMinutes(start, end time.Time) *Future[NodeActiveMinutesResult]
+	QueryNodeCPUCoresCapacity(start, end time.Time) *Future[NodeCPUCoresCapacityResult]
+	QueryNodeCPUCoresAllocatable(start, end time.Time) *Future[NodeCPUCoresAllocatableResult]
+	QueryNodeRAMBytesCapacity(start, end time.Time) *Future[NodeRAMBytesCapacityResult]
+	QueryNodeRAMBytesAllocatable(start, end time.Time) *Future[NodeRAMBytesAllocatableResult]
+	QueryNodeGPUCount(start, end time.Time) *Future[NodeGPUCountResult]
+	QueryNodeCPUModeTotal(start, end time.Time) *Future[NodeCPUModeTotalResult]
+	QueryNodeIsSpot(start, end time.Time) *Future[NodeIsSpotResult]
+	QueryNodeRAMSystemPercent(start, end time.Time) *Future[NodeRAMSystemPercentResult]
+	QueryNodeRAMUserPercent(start, end time.Time) *Future[NodeRAMUserPercentResult]
+
+	// Load Balancers
+	QueryLBActiveMinutes(start, end time.Time) *Future[LBActiveMinutesResult]
+	QueryLBPricePerHr(start, end time.Time) *Future[LBPricePerHrResult]
+
+	// Cluster Management
+	QueryClusterManagementDuration(start, end time.Time) *Future[ClusterManagementDurationResult]
+	QueryClusterManagementPricePerHr(start, end time.Time) *Future[ClusterManagementPricePerHrResult]
+
+	// Pods
+	QueryPods(start, end time.Time) *Future[PodsResult]
+	QueryPodsUID(start, end time.Time) *Future[PodsResult]
+
+	// RAM
+	QueryRAMBytesAllocated(start, end time.Time) *Future[RAMBytesAllocatedResult]
+	QueryRAMRequests(start, end time.Time) *Future[RAMRequestsResult]
+	QueryRAMUsageAvg(start, end time.Time) *Future[RAMUsageAvgResult]
+	QueryRAMUsageMax(start, end time.Time) *Future[RAMUsageMaxResult]
+	QueryNodeRAMPricePerGiBHr(start, end time.Time) *Future[NodeRAMPricePerGiBHrResult]
+
+	// CPU
+	QueryCPUCoresAllocated(start, end time.Time) *Future[CPUCoresAllocatedResult]
+	QueryCPURequests(start, end time.Time) *Future[CPURequestsResult]
+	QueryCPUUsageAvg(start, end time.Time) *Future[CPUUsageAvgResult]
+	QueryCPUUsageMax(start, end time.Time) *Future[CPUUsageMaxResult]
+	QueryNodeCPUPricePerHr(start, end time.Time) *Future[NodeCPUPricePerHrResult]
+
+	// GPU
+	QueryGPUsAllocated(start, end time.Time) *Future[GPUsAllocatedResult]
+	QueryGPUsRequested(start, end time.Time) *Future[GPUsRequestedResult]
+	QueryGPUsUsageAvg(start, end time.Time) *Future[GPUsUsageAvgResult]
+	QueryGPUsUsageMax(start, end time.Time) *Future[GPUsUsageMaxResult]
+	QueryNodeGPUPricePerHr(start, end time.Time) *Future[NodeGPUPricePerHrResult]
+	QueryGPUInfo(start, end time.Time) *Future[GPUInfoResult]
+	QueryIsGPUShared(start, end time.Time) *Future[IsGPUSharedResult]
+
+	// PVC
+	QueryPodPVCAllocation(start, end time.Time) *Future[PodPVCAllocationResult]
+	QueryPVCBytesRequested(start, end time.Time) *Future[PVCBytesRequestedResult]
+	QueryPVCInfo(start, end time.Time) *Future[PVCInfoResult]
+
+	// PV
+	QueryPVBytes(start, end time.Time) *Future[PVBytesResult]
+	QueryPVPricePerGiBHour(start, end time.Time) *Future[PVPricePerGiBHourResult]
+	QueryPVInfo(start, end time.Time) *Future[PVInfoResult]
+
+	// Network Egress
+	QueryNetZoneGiB(start, end time.Time) *Future[NetZoneGiBResult]
+	QueryNetZonePricePerGiB(start, end time.Time) *Future[NetZonePricePerGiBResult]
+	QueryNetRegionGiB(start, end time.Time) *Future[NetRegionGiBResult]
+	QueryNetRegionPricePerGiB(start, end time.Time) *Future[NetRegionPricePerGiBResult]
+	QueryNetInternetGiB(start, end time.Time) *Future[NetInternetGiBResult]
+	QueryNetInternetPricePerGiB(start, end time.Time) *Future[NetInternetPricePerGiBResult]
+	QueryNetInternetServiceGiB(start, end time.Time) *Future[NetInternetServiceGiBResult]
+	QueryNetTransferBytes(start, end time.Time) *Future[NetTransferBytesResult]
+
+	// Network Ingress
+	QueryNetZoneIngressGiB(start, end time.Time) *Future[NetZoneIngressGiBResult]
+	QueryNetRegionIngressGiB(start, end time.Time) *Future[NetRegionIngressGiBResult]
+	QueryNetInternetIngressGiB(start, end time.Time) *Future[NetInternetIngressGiBResult]
+	QueryNetInternetServiceIngressGiB(start, end time.Time) *Future[NetInternetServiceIngressGiBResult]
+	QueryNetReceiveBytes(start, end time.Time) *Future[NetReceiveBytesResult]
+
+	// Annotations
+	QueryNamespaceAnnotations(start, end time.Time) *Future[NamespaceAnnotationsResult]
+	QueryPodAnnotations(start, end time.Time) *Future[PodAnnotationsResult]
+
+	// Labels
+	QueryNodeLabels(start, end time.Time) *Future[NodeLabelsResult]
+	QueryNamespaceLabels(start, end time.Time) *Future[NamespaceLabelsResult]
+	QueryPodLabels(start, end time.Time) *Future[PodLabelsResult]
+	QueryServiceLabels(start, end time.Time) *Future[ServiceLabelsResult]
+	QueryDeploymentLabels(start, end time.Time) *Future[DeploymentLabelsResult]
+	QueryStatefulSetLabels(start, end time.Time) *Future[StatefulSetLabelsResult]
+	QueryDaemonSetLabels(start, end time.Time) *Future[DaemonSetLabelsResult]
+	QueryJobLabels(start, end time.Time) *Future[JobLabelsResult]
+
+	// ReplicaSet -> Controller mapping
+	QueryPodsWithReplicaSetOwner(start, end time.Time) *Future[PodsWithReplicaSetOwnerResult]
+	QueryReplicaSetsWithoutOwners(start, end time.Time) *Future[ReplicaSetsWithoutOwnersResult]
+	QueryReplicaSetsWithRollout(start, end time.Time) *Future[ReplicaSetsWithRolloutResult]
+
+	// Data Coverage Query
+	QueryDataCoverage(limitDays int) (time.Time, time.Time, error)
+}
+
+type OpenCostDataSource interface {
+	// RegisterEndPoints registers any custom endpoints that can be used for diagnostics or debug purposes.
+	RegisterEndPoints(router *httprouter.Router)
+
+	// RegisterDiagnostics registers any custom data source diagnostics with the `DiagnosticService` that can
+	// be used to report externally.
+	RegisterDiagnostics(diagService diagnostics.DiagnosticService)
+
+	// Metrics returns a MetricsQuerier that can be used to query historical metrics data from the data source.
+	Metrics() MetricsQuerier
+
+	// ClusterMap returns a mapping of cluster identifier to ClusterInfo for all known clusters (local only for
+	// single cluster deployments).
+	ClusterMap() clusters.ClusterMap
+
+	// ClusterInfo returns the ClusterInfoProvider for the local cluster.
+	ClusterInfo() clusters.ClusterInfoProvider
+
+	BatchDuration() time.Duration
+	Resolution() time.Duration
+}

+ 1340 - 0
core/pkg/source/decoders.go

@@ -0,0 +1,1340 @@
+package source
+
+import (
+	"github.com/opencost/opencost/core/pkg/util"
+)
+
+const (
+	ClusterIDLabel       = "cluster_id"
+	NamespaceLabel       = "namespace"
+	NodeLabel            = "node"
+	InstanceLabel        = "instance"
+	InstanceTypeLabel    = "instance_type"
+	ContainerLabel       = "container"
+	PodLabel             = "pod"
+	PodNameLabel         = "pod_name"
+	ProviderIDLabel      = "provider_id"
+	DeviceLabel          = "device"
+	PVCLabel             = "persistentvolumeclaim"
+	PVLabel              = "persistentvolume"
+	StorageClassLabel    = "storageclass"
+	VolumeNameLabel      = "volumename"
+	ServiceLabel         = "service"
+	ServiceNameLabel     = "service_name"
+	IngressIPLabel       = "ingress_ip"
+	ProvisionerNameLabel = "provisioner_name"
+	UIDLabel             = "uid"
+	KubernetesNodeLabel  = "kubernetes_node"
+	ModeLabel            = "mode"
+	ModelNameLabel       = "modelName"
+	UUIDLabel            = "UUID"
+	ResourceLabel        = "resource"
+	DeploymentLabel      = "deployment"
+	StatefulSetLabel     = "statefulSet"
+	ReplicaSetLabel      = "replicaset"
+	OwnerNameLabel       = "owner_name"
+	OwnerKindLabel       = "owner_kind"
+	UnitLabel            = "unit"
+	InternetLabel        = "internet"
+	SameZoneLabel        = "same_zone"
+	SameRegionLabel      = "same_region"
+)
+
+type PVResult struct {
+	Cluster          string
+	PersistentVolume string
+}
+
+type PVUsedAvgResult struct {
+	Cluster               string
+	Namespace             string
+	PersistentVolumeClaim string
+
+	Data []*util.Vector
+}
+
+func DecodePVUsedAvgResult(result *QueryResult) *PVUsedAvgResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pvc, _ := result.GetString(PVCLabel)
+
+	return &PVUsedAvgResult{
+		Cluster:               cluster,
+		Namespace:             namespace,
+		PersistentVolumeClaim: pvc,
+		Data:                  result.Values,
+	}
+}
+
+type PVActiveMinutesResult struct {
+	Cluster          string
+	PersistentVolume string
+
+	Data []*util.Vector
+}
+
+func DecodePVActiveMinutesResult(result *QueryResult) *PVActiveMinutesResult {
+	cluster, _ := result.GetCluster()
+	pv, _ := result.GetString(PVLabel)
+
+	return &PVActiveMinutesResult{
+		Cluster:          cluster,
+		PersistentVolume: pv,
+		Data:             result.Values,
+	}
+}
+
+type PVUsedMaxResult struct {
+	Cluster               string
+	Namespace             string
+	PersistentVolumeClaim string
+	Data                  []*util.Vector
+}
+
+func DecodePVUsedMaxResult(result *QueryResult) *PVUsedMaxResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pvc, _ := result.GetString(PVCLabel)
+
+	return &PVUsedMaxResult{
+		Cluster:               cluster,
+		Namespace:             namespace,
+		PersistentVolumeClaim: pvc,
+		Data:                  result.Values,
+	}
+}
+
+type LocalStorageActiveMinutesResult struct {
+	Cluster    string
+	Node       string
+	ProviderID string
+
+	Data []*util.Vector
+}
+
+func DecodeLocalStorageActiveMinutesResult(result *QueryResult) *LocalStorageActiveMinutesResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	if node == "" {
+		node, _ = result.GetInstance()
+	}
+	providerId, _ := result.GetProviderID()
+
+	return &LocalStorageActiveMinutesResult{
+		Cluster:    cluster,
+		Node:       node,
+		ProviderID: providerId,
+		Data:       result.Values,
+	}
+}
+
+type LocalStorageCostResult struct {
+	Cluster  string
+	Instance string
+	Device   string
+
+	Data []*util.Vector
+}
+
+func DecodeLocalStorageCostResult(result *QueryResult) *LocalStorageCostResult {
+	cluster, _ := result.GetCluster()
+	instance, _ := result.GetInstance()
+	device, _ := result.GetDevice()
+
+	return &LocalStorageCostResult{
+		Cluster:  cluster,
+		Instance: instance,
+		Device:   device,
+		Data:     result.Values,
+	}
+}
+
+type LocalStorageUsedCostResult struct {
+	Cluster  string
+	Instance string
+	Device   string
+	Data     []*util.Vector
+}
+
+func DecodeLocalStorageUsedCostResult(result *QueryResult) *LocalStorageUsedCostResult {
+	cluster, _ := result.GetCluster()
+	instance, _ := result.GetInstance()
+	device, _ := result.GetDevice()
+
+	return &LocalStorageUsedCostResult{
+		Cluster:  cluster,
+		Instance: instance,
+		Device:   device,
+		Data:     result.Values,
+	}
+}
+
+type LocalStorageUsedAvgResult struct {
+	Cluster  string
+	Instance string
+	Device   string
+	Data     []*util.Vector
+}
+
+func DecodeLocalStorageUsedAvgResult(result *QueryResult) *LocalStorageUsedAvgResult {
+	cluster, _ := result.GetCluster()
+	instance, _ := result.GetInstance()
+	device, _ := result.GetDevice()
+
+	return &LocalStorageUsedAvgResult{
+		Cluster:  cluster,
+		Instance: instance,
+		Device:   device,
+		Data:     result.Values,
+	}
+}
+
+type LocalStorageUsedMaxResult struct {
+	Cluster  string
+	Instance string
+	Device   string
+	Data     []*util.Vector
+}
+
+func DecodeLocalStorageUsedMaxResult(result *QueryResult) *LocalStorageUsedMaxResult {
+	cluster, _ := result.GetCluster()
+	instance, _ := result.GetInstance()
+	device, _ := result.GetDevice()
+
+	return &LocalStorageUsedMaxResult{
+		Cluster:  cluster,
+		Instance: instance,
+		Device:   device,
+		Data:     result.Values,
+	}
+}
+
+type LocalStorageBytesResult struct {
+	Cluster  string
+	Instance string
+	Device   string
+	Data     []*util.Vector
+}
+
+func DecodeLocalStorageBytesResult(result *QueryResult) *LocalStorageBytesResult {
+	cluster, _ := result.GetCluster()
+	instance, _ := result.GetInstance()
+	device, _ := result.GetDevice()
+
+	return &LocalStorageBytesResult{
+		Cluster:  cluster,
+		Instance: instance,
+		Device:   device,
+		Data:     result.Values,
+	}
+}
+
+type NodeActiveMinutesResult struct {
+	Cluster    string
+	Node       string
+	ProviderID string
+	Data       []*util.Vector
+}
+
+func DecodeNodeActiveMinutesResult(result *QueryResult) *NodeActiveMinutesResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	providerId, _ := result.GetProviderID()
+
+	return &NodeActiveMinutesResult{
+		Cluster:    cluster,
+		Node:       node,
+		ProviderID: providerId,
+		Data:       result.Values,
+	}
+}
+
+type NodeCPUCoresCapacityResult struct {
+	Cluster string
+	Node    string
+	Data    []*util.Vector
+}
+
+func DecodeNodeCPUCoresCapacityResult(result *QueryResult) *NodeCPUCoresCapacityResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+
+	return &NodeCPUCoresCapacityResult{
+		Cluster: cluster,
+		Node:    node,
+		Data:    result.Values,
+	}
+}
+
+type NodeCPUCoresAllocatableResult = NodeCPUCoresCapacityResult
+
+func DecodeNodeCPUCoresAllocatableResult(result *QueryResult) *NodeCPUCoresAllocatableResult {
+	return DecodeNodeCPUCoresCapacityResult(result)
+}
+
+type NodeRAMBytesCapacityResult struct {
+	Cluster string
+	Node    string
+	Data    []*util.Vector
+}
+
+func DecodeNodeRAMBytesCapacityResult(result *QueryResult) *NodeRAMBytesCapacityResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+
+	return &NodeRAMBytesCapacityResult{
+		Cluster: cluster,
+		Node:    node,
+		Data:    result.Values,
+	}
+}
+
+type NodeRAMBytesAllocatableResult = NodeRAMBytesCapacityResult
+
+func DecodeNodeRAMBytesAllocatableResult(result *QueryResult) *NodeRAMBytesAllocatableResult {
+	return DecodeNodeRAMBytesCapacityResult(result)
+}
+
+type NodeGPUCountResult struct {
+	Cluster    string
+	Node       string
+	ProviderID string
+
+	Data []*util.Vector
+}
+
+func DecodeNodeGPUCountResult(result *QueryResult) *NodeGPUCountResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	providerId, _ := result.GetProviderID()
+
+	return &NodeGPUCountResult{
+		Cluster:    cluster,
+		Node:       node,
+		ProviderID: providerId,
+		Data:       result.Values,
+	}
+}
+
+type NodeCPUModeTotalResult struct {
+	Cluster string
+	Node    string
+	Mode    string
+	Data    []*util.Vector
+}
+
+func DecodeNodeCPUModeTotalResult(result *QueryResult) *NodeCPUModeTotalResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetString(KubernetesNodeLabel)
+	mode, _ := result.GetString(ModeLabel)
+
+	return &NodeCPUModeTotalResult{
+		Cluster: cluster,
+		Node:    node,
+		Mode:    mode,
+		Data:    result.Values,
+	}
+}
+
+type NodeIsSpotResult struct {
+	Cluster    string
+	Node       string
+	ProviderID string
+	Data       []*util.Vector
+}
+
+func DecodeNodeIsSpotResult(result *QueryResult) *NodeIsSpotResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	providerId, _ := result.GetProviderID()
+
+	return &NodeIsSpotResult{
+		Cluster:    cluster,
+		Node:       node,
+		ProviderID: providerId,
+		Data:       result.Values,
+	}
+}
+
+type NodeRAMSystemPercentResult struct {
+	Cluster  string
+	Instance string
+	Data     []*util.Vector
+}
+
+func DecodeNodeRAMSystemPercentResult(result *QueryResult) *NodeRAMSystemPercentResult {
+	cluster, _ := result.GetCluster()
+	instance, _ := result.GetInstance()
+
+	return &NodeRAMSystemPercentResult{
+		Cluster:  cluster,
+		Instance: instance,
+		Data:     result.Values,
+	}
+}
+
+type NodeRAMUserPercentResult = NodeRAMSystemPercentResult
+
+func DecodeNodeRAMUserPercentResult(result *QueryResult) *NodeRAMUserPercentResult {
+	return DecodeNodeRAMSystemPercentResult(result)
+}
+
+type LBActiveMinutesResult struct {
+	Cluster   string
+	Namespace string
+	Service   string
+	IngressIP string
+
+	Data []*util.Vector
+}
+
+func DecodeLBActiveMinutesResult(result *QueryResult) *LBActiveMinutesResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	service, _ := result.GetString(ServiceNameLabel)
+	ingressIp, _ := result.GetString(IngressIPLabel)
+
+	return &LBActiveMinutesResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Service:   service,
+		IngressIP: ingressIp,
+		Data:      result.Values,
+	}
+}
+
+type LBPricePerHrResult = LBActiveMinutesResult
+
+func DecodeLBPricePerHrResult(result *QueryResult) *LBPricePerHrResult {
+	return DecodeLBActiveMinutesResult(result)
+}
+
+type ClusterManagementDurationResult struct {
+	Cluster     string
+	Provisioner string
+	Data        []*util.Vector
+}
+
+func DecodeClusterManagementDurationResult(result *QueryResult) *ClusterManagementDurationResult {
+	cluster, _ := result.GetCluster()
+	provisioner, _ := result.GetString(ProvisionerNameLabel)
+
+	return &ClusterManagementDurationResult{
+		Cluster:     cluster,
+		Provisioner: provisioner,
+		Data:        result.Values,
+	}
+}
+
+type ClusterManagementPricePerHrResult = ClusterManagementDurationResult
+
+func DecodeClusterManagementPricePerHrResult(result *QueryResult) *ClusterManagementPricePerHrResult {
+	return DecodeClusterManagementDurationResult(result)
+}
+
+type PodsResult struct {
+	UID       string
+	Cluster   string
+	Namespace string
+	Pod       string
+
+	Data []*util.Vector
+}
+
+func DecodePodsResult(result *QueryResult) *PodsResult {
+	uid, _ := result.GetString(UIDLabel)
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+
+	return &PodsResult{
+		UID:       uid,
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Data:      result.Values,
+	}
+}
+
+type ContainerMetricResult struct {
+	Cluster   string
+	Node      string
+	Instance  string
+	Namespace string
+	Pod       string
+	Container string
+
+	Data []*util.Vector
+}
+
+func DecodeContainerMetricResult(result *QueryResult) *ContainerMetricResult {
+	cluster, _ := result.GetCluster()
+
+	node, _ := result.GetNode()
+	instance, _ := result.GetInstance()
+
+	// NOTE: this addresses cases where the node isn't set, but the instance is,
+	// NOTE: we just inherit the instance as the node
+	if node == "" {
+		node = instance
+	}
+
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+
+	return &ContainerMetricResult{
+		Cluster:   cluster,
+		Node:      node,
+		Instance:  instance,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Data:      result.Values,
+	}
+}
+
+type RAMBytesAllocatedResult = ContainerMetricResult
+
+func DecodeRAMBytesAllocatedResult(result *QueryResult) *RAMBytesAllocatedResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type RAMRequestsResult = ContainerMetricResult
+
+func DecodeRAMRequestsResult(result *QueryResult) *RAMRequestsResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type RAMUsageAvgResult = ContainerMetricResult
+
+func DecodeRAMUsageAvgResult(result *QueryResult) *RAMUsageAvgResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type RAMUsageMaxResult = ContainerMetricResult
+
+func DecodeRAMUsageMaxResult(result *QueryResult) *RAMUsageMaxResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type NodeRAMPricePerGiBHrResult struct {
+	Cluster      string
+	Node         string
+	InstanceType string
+	ProviderID   string
+	Data         []*util.Vector
+}
+
+func DecodeNodeRAMPricePerGiBHrResult(result *QueryResult) *NodeRAMPricePerGiBHrResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	instanceType, _ := result.GetInstanceType()
+	providerId, _ := result.GetProviderID()
+
+	return &NodeRAMPricePerGiBHrResult{
+		Cluster:      cluster,
+		Node:         node,
+		InstanceType: instanceType,
+		ProviderID:   providerId,
+		Data:         result.Values,
+	}
+}
+
+type CPUCoresAllocatedResult = ContainerMetricResult
+
+func DecodeCPUCoresAllocatedResult(result *QueryResult) *CPUCoresAllocatedResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type CPURequestsResult = ContainerMetricResult
+
+func DecodeCPURequestsResult(result *QueryResult) *CPURequestsResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type CPUUsageAvgResult = ContainerMetricResult
+
+func DecodeCPUUsageAvgResult(result *QueryResult) *CPUUsageAvgResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type CPUUsageMaxResult = ContainerMetricResult
+
+func DecodeCPUUsageMaxResult(result *QueryResult) *CPUUsageMaxResult {
+	return DecodeContainerMetricResult(result)
+}
+
+type NodeCPUPricePerHrResult struct {
+	Cluster      string
+	Node         string
+	InstanceType string
+	ProviderID   string
+	Data         []*util.Vector
+}
+
+func DecodeNodeCPUPricePerHrResult(result *QueryResult) *NodeCPUPricePerHrResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	instanceType, _ := result.GetInstanceType()
+	providerId, _ := result.GetProviderID()
+
+	return &NodeCPUPricePerHrResult{
+		Cluster:      cluster,
+		Node:         node,
+		InstanceType: instanceType,
+		ProviderID:   providerId,
+		Data:         result.Values,
+	}
+}
+
+// type alias requested result to allocated result, as you can only request a full GPU
+type GPUsRequestedResult = GPUsAllocatedResult
+
+func DecodeGPUsRequestedResult(result *QueryResult) *GPUsRequestedResult {
+	return DecodeGPUsAllocatedResult(result)
+}
+
+type GPUsAllocatedResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+	Data      []*util.Vector
+}
+
+func DecodeGPUsAllocatedResult(result *QueryResult) *GPUsAllocatedResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+
+	return &GPUsAllocatedResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Data:      result.Values,
+	}
+}
+
+type GPUsUsageAvgResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+
+	Data []*util.Vector
+}
+
+func DecodeGPUsUsageAvgResult(result *QueryResult) *GPUsUsageAvgResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+
+	return &GPUsUsageAvgResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Data:      result.Values,
+	}
+}
+
+type GPUsUsageMaxResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+	Data      []*util.Vector
+}
+
+func DecodeGPUsUsageMaxResult(result *QueryResult) *GPUsUsageMaxResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+
+	return &GPUsUsageMaxResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Data:      result.Values,
+	}
+}
+
+type NodeGPUPricePerHrResult struct {
+	Cluster      string
+	Node         string
+	InstanceType string
+	ProviderID   string
+	Data         []*util.Vector
+}
+
+func DecodeNodeGPUPricePerHrResult(result *QueryResult) *NodeGPUPricePerHrResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	instanceType, _ := result.GetInstanceType()
+	providerId, _ := result.GetProviderID()
+
+	return &NodeGPUPricePerHrResult{
+		Cluster:      cluster,
+		Node:         node,
+		InstanceType: instanceType,
+		ProviderID:   providerId,
+		Data:         result.Values,
+	}
+}
+
+type GPUInfoResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+	Device    string
+	ModelName string
+	UUID      string
+	Data      []*util.Vector
+}
+
+func DecodeGPUInfoResult(result *QueryResult) *GPUInfoResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+	device, _ := result.GetString(DeviceLabel)
+	modelName, _ := result.GetString(ModelNameLabel)
+	uuid, _ := result.GetString(UUIDLabel)
+
+	return &GPUInfoResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Device:    device,
+		ModelName: modelName,
+		UUID:      uuid,
+		Data:      result.Values,
+	}
+}
+
+type IsGPUSharedResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+	Resource  string
+	Data      []*util.Vector
+}
+
+func DecodeIsGPUSharedResult(result *QueryResult) *IsGPUSharedResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+	resource, _ := result.GetString(ResourceLabel)
+
+	return &IsGPUSharedResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Resource:  resource,
+		Data:      result.Values,
+	}
+}
+
+type PodPVCAllocationResult struct {
+	Cluster               string
+	Namespace             string
+	Pod                   string
+	PersistentVolume      string
+	PersistentVolumeClaim string
+	Data                  []*util.Vector
+}
+
+func DecodePodPVCAllocationResult(result *QueryResult) *PodPVCAllocationResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	pv, _ := result.GetString(PVLabel)
+	pvc, _ := result.GetString(PVCLabel)
+
+	return &PodPVCAllocationResult{
+		Cluster:               cluster,
+		Namespace:             namespace,
+		Pod:                   pod,
+		PersistentVolume:      pv,
+		PersistentVolumeClaim: pvc,
+		Data:                  result.Values,
+	}
+}
+
+type PVCBytesRequestedResult struct {
+	Cluster               string
+	Namespace             string
+	PersistentVolumeClaim string
+
+	Data []*util.Vector
+}
+
+func DecodePVCBytesRequestedResult(result *QueryResult) *PVCBytesRequestedResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pvc, _ := result.GetString(PVCLabel)
+
+	return &PVCBytesRequestedResult{
+		Cluster:               cluster,
+		Namespace:             namespace,
+		PersistentVolumeClaim: pvc,
+		Data:                  result.Values,
+	}
+}
+
+type PVCInfoResult struct {
+	Cluster               string
+	Namespace             string
+	VolumeName            string
+	PersistentVolumeClaim string
+	StorageClass          string
+
+	Data []*util.Vector
+}
+
+func DecodePVCInfoResult(result *QueryResult) *PVCInfoResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	volumeName, _ := result.GetString(VolumeNameLabel)
+	pvc, _ := result.GetString(PVCLabel)
+	storageClass, _ := result.GetString(StorageClassLabel)
+
+	return &PVCInfoResult{
+		Cluster:               cluster,
+		Namespace:             namespace,
+		VolumeName:            volumeName,
+		PersistentVolumeClaim: pvc,
+		StorageClass:          storageClass,
+		Data:                  result.Values,
+	}
+}
+
+type PVBytesResult struct {
+	Cluster          string
+	PersistentVolume string
+
+	Data []*util.Vector
+}
+
+func DecodePVBytesResult(result *QueryResult) *PVBytesResult {
+	cluster, _ := result.GetCluster()
+	pv, _ := result.GetString(PVLabel)
+
+	return &PVBytesResult{
+		Cluster:          cluster,
+		PersistentVolume: pv,
+		Data:             result.Values,
+	}
+}
+
+type PVPricePerGiBHourResult struct {
+	Cluster          string
+	VolumeName       string
+	PersistentVolume string
+	ProviderID       string
+
+	Data []*util.Vector
+}
+
+func DecodePVPricePerGiBHourResult(result *QueryResult) *PVPricePerGiBHourResult {
+	cluster, _ := result.GetCluster()
+	volumeName, _ := result.GetString(VolumeNameLabel)
+	pv, _ := result.GetString(PVLabel)
+	providerId, _ := result.GetProviderID()
+
+	return &PVPricePerGiBHourResult{
+		Cluster:          cluster,
+		VolumeName:       volumeName,
+		PersistentVolume: pv,
+		ProviderID:       providerId,
+
+		Data: result.Values,
+	}
+}
+
+type PVInfoResult struct {
+	Cluster          string
+	PersistentVolume string
+	StorageClass     string
+	ProviderID       string
+
+	Data []*util.Vector
+}
+
+func DecodePVInfoResult(result *QueryResult) *PVInfoResult {
+	cluster, _ := result.GetCluster()
+	storageClass, _ := result.GetString(StorageClassLabel)
+	providerId, _ := result.GetProviderID()
+	pv, _ := result.GetString(PVLabel)
+
+	return &PVInfoResult{
+		Cluster:          cluster,
+		PersistentVolume: pv,
+		StorageClass:     storageClass,
+		ProviderID:       providerId,
+		Data:             result.Values,
+	}
+}
+
+// Base type for network usage results
+type NetworkGiBResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Service   string
+
+	Data []*util.Vector
+}
+
+func DecodeNetworkGiBResult(result *QueryResult) *NetworkGiBResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	service, _ := result.GetString(ServiceLabel)
+
+	return &NetworkGiBResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Service:   service,
+		Data:      result.Values,
+	}
+}
+
+// Base type for network price results
+type NetworkPricePerGiBResult struct {
+	Cluster string
+
+	Data []*util.Vector
+}
+
+func DecodeNetworkPricePerGiBResult(result *QueryResult) *NetworkPricePerGiBResult {
+	cluster, _ := result.GetCluster()
+
+	return &NetworkPricePerGiBResult{
+		Cluster: cluster,
+		Data:    result.Values,
+	}
+}
+
+// Type alias the specific network subclassification results AND price results
+type NetZoneGiBResult = NetworkGiBResult
+type NetZonePricePerGiBResult = NetworkPricePerGiBResult
+
+type NetRegionGiBResult = NetworkGiBResult
+type NetRegionPricePerGiBResult = NetworkPricePerGiBResult
+
+type NetInternetGiBResult = NetworkGiBResult
+type NetInternetPricePerGiBResult = NetworkPricePerGiBResult
+
+type NetInternetServiceGiBResult = NetworkGiBResult
+
+type NetZoneIngressGiBResult = NetworkGiBResult
+type NetRegionIngressGiBResult = NetworkGiBResult
+type NetInternetIngressGiBResult = NetworkGiBResult
+type NetInternetServiceIngressGiBResult = NetworkGiBResult
+
+func DecodeNetZoneGiBResult(result *QueryResult) *NetZoneGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetZonePricePerGiBResult(result *QueryResult) *NetZonePricePerGiBResult {
+	return DecodeNetworkPricePerGiBResult(result)
+}
+
+func DecodeNetRegionGiBResult(result *QueryResult) *NetRegionGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetRegionPricePerGiBResult(result *QueryResult) *NetRegionPricePerGiBResult {
+	return DecodeNetworkPricePerGiBResult(result)
+}
+
+func DecodeNetInternetGiBResult(result *QueryResult) *NetInternetGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetInternetPricePerGiBResult(result *QueryResult) *NetInternetPricePerGiBResult {
+	return DecodeNetworkPricePerGiBResult(result)
+}
+
+func DecodeNetInternetServiceGiBResult(result *QueryResult) *NetInternetServiceGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetZoneIngressGiBResult(result *QueryResult) *NetZoneIngressGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetRegionIngressGiBResult(result *QueryResult) *NetRegionIngressGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetInternetIngressGiBResult(result *QueryResult) *NetInternetIngressGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+func DecodeNetInternetServiceIngressGiBResult(result *QueryResult) *NetInternetServiceIngressGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
+type NetReceiveBytesResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+	Data      []*util.Vector
+}
+
+func DecodeNetReceiveBytesResult(result *QueryResult) *NetReceiveBytesResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+
+	return &NetReceiveBytesResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Data:      result.Values,
+	}
+}
+
+type NetTransferBytesResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Container string
+
+	Data []*util.Vector
+}
+
+func DecodeNetTransferBytesResult(result *QueryResult) *NetTransferBytesResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	container, _ := result.GetContainer()
+
+	return &NetTransferBytesResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Container: container,
+		Data:      result.Values,
+	}
+}
+
+type NamespaceAnnotationsResult struct {
+	Cluster     string
+	Namespace   string
+	Annotations map[string]string
+
+	Data []*util.Vector
+}
+
+func DecodeNamespaceAnnotationsResult(result *QueryResult) *NamespaceAnnotationsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	annotations := result.GetAnnotations()
+
+	return &NamespaceAnnotationsResult{
+		Cluster:     cluster,
+		Namespace:   namespace,
+		Annotations: annotations,
+		Data:        result.Values,
+	}
+}
+
+type PodAnnotationsResult struct {
+	Cluster     string
+	Namespace   string
+	Pod         string
+	Annotations map[string]string
+
+	Data []*util.Vector
+}
+
+func DecodePodAnnotationsResult(result *QueryResult) *PodAnnotationsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	annotations := result.GetAnnotations()
+
+	return &PodAnnotationsResult{
+		Cluster:     cluster,
+		Namespace:   namespace,
+		Pod:         pod,
+		Annotations: annotations,
+		Data:        result.Values,
+	}
+}
+
+type NodeLabelsResult struct {
+	Cluster string
+	Node    string
+	Labels  map[string]string
+	Data    []*util.Vector
+}
+
+func DecodeNodeLabelsResult(result *QueryResult) *NodeLabelsResult {
+	cluster, _ := result.GetCluster()
+	node, _ := result.GetNode()
+	labels := result.GetLabels()
+
+	return &NodeLabelsResult{
+		Cluster: cluster,
+		Node:    node,
+		Labels:  labels,
+		Data:    result.Values,
+	}
+}
+
+type NamespaceLabelsResult struct {
+	Cluster   string
+	Namespace string
+	Labels    map[string]string
+	Data      []*util.Vector
+}
+
+func DecodeNamespaceLabelsResult(result *QueryResult) *NamespaceLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	labels := result.GetLabels()
+
+	return &NamespaceLabelsResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Labels:    labels,
+		Data:      result.Values,
+	}
+}
+
+type PodLabelsResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Labels    map[string]string
+	Data      []*util.Vector
+}
+
+func DecodePodLabelsResult(result *QueryResult) *PodLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	labels := result.GetLabels()
+
+	return &PodLabelsResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Labels:    labels,
+		Data:      result.Values,
+	}
+}
+
+type ServiceLabelsResult struct {
+	Cluster   string
+	Namespace string
+	Service   string
+	Labels    map[string]string
+
+	Data []*util.Vector
+}
+
+func DecodeServiceLabelsResult(result *QueryResult) *ServiceLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	service, _ := result.GetString(ServiceLabel)
+	labels := result.GetLabels()
+
+	return &ServiceLabelsResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Service:   service,
+		Labels:    labels,
+		Data:      result.Values,
+	}
+}
+
+type DeploymentLabelsResult struct {
+	Cluster    string
+	Namespace  string
+	Deployment string
+	Labels     map[string]string
+	Data       []*util.Vector
+}
+
+func DecodeDeploymentLabelsResult(result *QueryResult) *DeploymentLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	deployment, _ := result.GetString(DeploymentLabel)
+	labels := result.GetLabels()
+
+	return &DeploymentLabelsResult{
+		Cluster:    cluster,
+		Namespace:  namespace,
+		Deployment: deployment,
+		Labels:     labels,
+		Data:       result.Values,
+	}
+}
+
+type StatefulSetLabelsResult struct {
+	Cluster     string
+	Namespace   string
+	StatefulSet string
+	Labels      map[string]string
+	Data        []*util.Vector
+}
+
+func DecodeStatefulSetLabelsResult(result *QueryResult) *StatefulSetLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	statefulSet, _ := result.GetString(StatefulSetLabel)
+	labels := result.GetLabels()
+
+	return &StatefulSetLabelsResult{
+		Cluster:     cluster,
+		Namespace:   namespace,
+		StatefulSet: statefulSet,
+		Labels:      labels,
+		Data:        result.Values,
+	}
+}
+
+type DaemonSetLabelsResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	DaemonSet string
+	Labels    map[string]string
+	Data      []*util.Vector
+}
+
+func DecodeDaemonSetLabelsResult(result *QueryResult) *DaemonSetLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	daemonSet, _ := result.GetString(OwnerNameLabel)
+	labels := result.GetLabels()
+
+	return &DaemonSetLabelsResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		DaemonSet: daemonSet,
+		Labels:    labels,
+		Data:      result.Values,
+	}
+}
+
+type JobLabelsResult struct {
+	Cluster   string
+	Namespace string
+	Pod       string
+	Job       string
+	Labels    map[string]string
+	Data      []*util.Vector
+}
+
+func DecodeJobLabelsResult(result *QueryResult) *JobLabelsResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	pod, _ := result.GetPod()
+	job, _ := result.GetString(OwnerNameLabel)
+	labels := result.GetLabels()
+
+	return &JobLabelsResult{
+		Cluster:   cluster,
+		Namespace: namespace,
+		Pod:       pod,
+		Job:       job,
+		Labels:    labels,
+		Data:      result.Values,
+	}
+}
+
+type PodsWithReplicaSetOwnerResult struct {
+	Cluster    string
+	Namespace  string
+	Pod        string
+	ReplicaSet string
+
+	Data []*util.Vector
+}
+
+func DecodePodsWithReplicaSetOwnerResult(result *QueryResult) *PodsWithReplicaSetOwnerResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	replicaSet, _ := result.GetString(OwnerNameLabel)
+	pod, _ := result.GetPod()
+
+	return &PodsWithReplicaSetOwnerResult{
+		Cluster:    cluster,
+		Namespace:  namespace,
+		Pod:        pod,
+		ReplicaSet: replicaSet,
+		Data:       result.Values,
+	}
+}
+
+type ReplicaSetsWithoutOwnersResult struct {
+	Cluster    string
+	Namespace  string
+	ReplicaSet string
+
+	Data []*util.Vector
+}
+
+func DecodeReplicaSetsWithoutOwnersResult(result *QueryResult) *ReplicaSetsWithoutOwnersResult {
+	return &ReplicaSetsWithoutOwnersResult{
+		Data: result.Values,
+	}
+}
+
+type ReplicaSetsWithRolloutResult struct {
+	Cluster    string
+	Namespace  string
+	ReplicaSet string
+	OwnerName  string
+	OwnerKind  string
+	Data       []*util.Vector
+}
+
+func DecodeReplicaSetsWithRolloutResult(result *QueryResult) *ReplicaSetsWithRolloutResult {
+	cluster, _ := result.GetCluster()
+	namespace, _ := result.GetNamespace()
+	replicaSet, _ := result.GetString(ReplicaSetLabel)
+	ownerName, _ := result.GetString(OwnerNameLabel)
+	ownerKind, _ := result.GetString(OwnerKindLabel)
+
+	return &ReplicaSetsWithRolloutResult{
+		Cluster:    cluster,
+		Namespace:  namespace,
+		ReplicaSet: replicaSet,
+		OwnerName:  ownerName,
+		OwnerKind:  ownerKind,
+		Data:       result.Values,
+	}
+}
+
+func DecodeAll[T any](results []*QueryResult, decode ResultDecoder[T]) []*T {
+	decoded := make([]*T, 0, len(results))
+	for _, result := range results {
+		decoded = append(decoded, decode(result))
+	}
+
+	return decoded
+}

+ 46 - 3
pkg/prom/error.go → core/pkg/source/error.go

@@ -1,6 +1,7 @@
-package prom
+package source
 
 import (
+	"errors"
 	"fmt"
 	"reflect"
 	"strings"
@@ -23,7 +24,33 @@ func IsNoStoreAPIWarning(warning string) bool {
 }
 
 //--------------------------------------------------------------------------
-//  Prometheus Error Collection
+//  Help Retry Error
+//--------------------------------------------------------------------------
+
+// HelpRetryError is a wrapper error type which indicates an error should induce a retry, and
+// is non-fatal
+type HelpRetryError struct {
+	wrapped error
+}
+
+func (h *HelpRetryError) Unwrap() error {
+	return h.wrapped
+}
+
+func (h *HelpRetryError) Error() string {
+	return h.wrapped.Error()
+}
+
+func NewHelpRetryError(cause error) error {
+	return &HelpRetryError{wrapped: cause}
+}
+
+func IsRetryable(err error) bool {
+	return errors.Is(err, &HelpRetryError{})
+}
+
+//--------------------------------------------------------------------------
+//  Error Collection
 //--------------------------------------------------------------------------
 
 type QueryError struct {
@@ -89,6 +116,22 @@ type QueryErrorCollector struct {
 	warnings []*QueryWarning
 }
 
+// Appends a QueryError to the errors list
+func (ec *QueryErrorCollector) AppendError(err *QueryError) {
+	ec.m.Lock()
+	defer ec.m.Unlock()
+
+	ec.errors = append(ec.errors, err)
+}
+
+// Appends a QueryWarning to the warnings list
+func (ec *QueryErrorCollector) AppendWarning(warn *QueryWarning) {
+	ec.m.Lock()
+	defer ec.m.Unlock()
+
+	ec.warnings = append(ec.warnings, warn)
+}
+
 // Reports an error to the collector. Ignores if the error is nil and the warnings
 // are empty
 func (ec *QueryErrorCollector) Report(query string, warnings []string, requestError error, parseError error) {
@@ -273,7 +316,7 @@ func IsCommError(err error) bool {
 
 // Error prints the error as a string
 func (pce CommError) Error() string {
-	return fmt.Sprintf("Prometheus communication error: %s", strings.Join(pce.messages, ": "))
+	return fmt.Sprintf("Communication error: %s", strings.Join(pce.messages, ": "))
 }
 
 // Wrap wraps the error with the given message, but persists the error type.

+ 1 - 1
pkg/prom/error_test.go → core/pkg/source/error_test.go

@@ -1,4 +1,4 @@
-package prom
+package source
 
 import (
 	"errors"

+ 68 - 0
core/pkg/source/future.go

@@ -0,0 +1,68 @@
+package source
+
+// ResultDecoder[T] is a function that decodes a `QueryResult` into a `*T` type.
+type ResultDecoder[T any] func(*QueryResult) *T
+
+// Future[T] is a async future/promise/task that will resolve into a []*T return or
+// error when awaited. This type provides a type-safe way of interfacing with `QueryResultsChan`
+// via a `ResultDecoder[T]` function.
+type Future[T any] struct {
+	decoder     ResultDecoder[T]
+	resultsChan QueryResultsChan
+
+	// results is set when we use a passthrough
+	results []*T
+}
+
+// NewFuture Creates a new `Future[T]` with the given `ResultDecoder[T]` and `QueryResultsChan`.
+func NewFuture[T any](decoder ResultDecoder[T], resultsChan QueryResultsChan) *Future[T] {
+	return &Future[T]{
+		decoder:     decoder,
+		resultsChan: resultsChan,
+	}
+}
+
+// NewFutureFrom accepts a result set to wrap in the a Future implementation for passthrough.
+func NewFutureFrom[T any](results []*T) *Future[T] {
+	return &Future[T]{
+		results: results,
+	}
+}
+
+// awaitWith allows internal callers to pass an error collector for grouping futures
+func (f *Future[T]) awaitWith(errorCollector *QueryErrorCollector) ([]*T, error) {
+	if f.results != nil {
+		return f.results, nil
+	}
+
+	defer close(f.resultsChan)
+	result := <-f.resultsChan
+
+	q := result.Query
+	err := result.Error
+
+	if err != nil {
+		errorCollector.AppendError(&QueryError{Query: q, Error: err})
+		return nil, err
+	}
+
+	decoded := DecodeAll(result.Results, f.decoder)
+	return decoded, nil
+}
+
+// Await blocks and waits for the `Future` to resolve, and returns the results if successful, or an error
+// otherwise.
+func (f *Future[T]) Await() ([]*T, error) {
+	// in the event that we have a resolved future, we can return the results directly
+	if f.results != nil {
+		return f.results, nil
+	}
+
+	results, err := f.resultsChan.Await()
+	if err != nil {
+		return nil, err
+	}
+
+	decoded := DecodeAll(results, f.decoder)
+	return decoded, nil
+}

+ 110 - 0
core/pkg/source/querygroup.go

@@ -0,0 +1,110 @@
+package source
+
+// QueryGroupAsyncResult is a representation of a single async query in a group.
+type QueryGroupAsyncResult struct {
+	errorCollector *QueryErrorCollector
+	resultsChan    QueryResultsChan
+}
+
+// newQueryGroupAsyncResult creates a new QueryGroupAsyncResult with the given error collector and results channel.
+func newQueryGroupAsyncResult(collector *QueryErrorCollector, resultsChan QueryResultsChan) *QueryGroupAsyncResult {
+	return &QueryGroupAsyncResult{
+		errorCollector: collector,
+		resultsChan:    resultsChan,
+	}
+}
+
+// Await blocks and waits for the `QueryGroupAsyncResult` to resolve, and returns a slice of generic `QueryResult`
+// instances if successful, or an error otherwise.
+func (qgar *QueryGroupAsyncResult) Await() ([]*QueryResult, error) {
+	defer close(qgar.resultsChan)
+	result := <-qgar.resultsChan
+
+	q := result.Query
+	err := result.Error
+
+	if err != nil {
+		qgar.errorCollector.AppendError(&QueryError{Query: q, Error: err})
+		return nil, err
+	}
+
+	return result.Results, nil
+}
+
+// QueryGroupFuture[T] is a representation of a single async query in a group with a typed result.
+type QueryGroupFuture[T any] struct {
+	errorCollector *QueryErrorCollector
+	future         *Future[T]
+}
+
+// WithGroup creates a new QueryGroupFuture[T] instance with the given QueryGroup and Future instances.
+// This is the specific way to add a typed `Future[T]` to a `QueryGroup`.
+func WithGroup[T any](g *QueryGroup, f *Future[T]) *QueryGroupFuture[T] {
+	return &QueryGroupFuture[T]{
+		errorCollector: g.errorCollector,
+		future:         f,
+	}
+}
+
+// Await blocks and waits for the `QueryGroupFuture[T]` to resolve, and returns a slice of `*T` instances if successful,
+// or an error otherwise.
+func (qgf *QueryGroupFuture[T]) Await() ([]*T, error) {
+	return qgf.future.awaitWith(qgf.errorCollector)
+}
+
+// QueryGroup is a representation of multiple async queries. It provides a shared error collector
+// for all queries in the group.
+//
+// Example:
+//
+//	grp := NewQueryGroup()
+//	q1 := WithGroup(grp, QueryFoo())
+//	q2 := WithGroup(grp, QueryBar())
+//
+//	results1, _ := q1.Await()
+//	results2, _ := q2.Await()
+//
+//	if grp.HasErrors() {
+//		return grp.Error() // <-- error return type
+//	}
+type QueryGroup struct {
+	errorCollector *QueryErrorCollector
+}
+
+// NewQueryGroup creates a new QueryGroup instance which can be used to group non-typed async queries with
+// the `With(QueryResultsChan)` instance method, or with the package function `WithGroup[T](*QueryGroup, *Future[T])`
+func NewQueryGroup() *QueryGroup {
+	var errorCollector QueryErrorCollector
+
+	return &QueryGroup{
+		errorCollector: &errorCollector,
+	}
+}
+
+// With adds the given `QueryResultsChan` to the QueryGroup instance and returns a `QueryGroupAsyncResult` instance to be
+// awaited
+func (qg *QueryGroup) With(resultsChan QueryResultsChan) *QueryGroupAsyncResult {
+	return newQueryGroupAsyncResult(qg.errorCollector, resultsChan)
+}
+
+// HasErrors returns true if any of the async queries in the group have errored. Note that all results must be awaited
+// in order to be checked for errors.
+func (qg *QueryGroup) HasErrors() bool {
+	return qg.errorCollector.IsError()
+}
+
+// Error returns nil if there were no errors in the group. Otherwise, it returns all of the errors that occurred grouped
+// into a single error.
+func (qg *QueryGroup) Error() error {
+	if !qg.errorCollector.IsError() {
+		var err error
+		return err
+	}
+
+	return qg.errorCollector
+}
+
+// Errors returns all of individual errors that occurred in the group.
+func (qg *QueryGroup) Errors() []*QueryError {
+	return qg.errorCollector.Errors()
+}

+ 241 - 0
core/pkg/source/queryresult.go

@@ -0,0 +1,241 @@
+package source
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/util"
+)
+
+// QueryResultsChan is a channel of query results
+type QueryResultsChan chan *QueryResults
+
+// Await returns query results, blocking until they are made available, and
+// deferring the closure of the underlying channel
+func (qrc QueryResultsChan) Await() ([]*QueryResult, error) {
+	defer close(qrc)
+
+	results := <-qrc
+	if results.Error != nil {
+		return nil, results.Error
+	}
+
+	return results.Results, nil
+}
+
+// ResultKeys is a "configuration" struct that contains the keys/labels used to resolve labeled query
+// results. ResultKeys can be defined with every QueryResults instance if necessary, and alter the keys
+// used to fetch results when calling the following methods on QueryResults:
+//
+//	GetCluster()
+//	GetNamespace()
+//	GetNode()
+//	GetInstance()
+//	GetInstanceType()
+//	GetContainer()
+//	GetPod()
+//	GetProviderID()
+//	GetDevice()
+type ResultKeys struct {
+	ClusterKey      string
+	NamespaceKey    string
+	NodeKey         string
+	InstanceKey     string
+	InstanceTypeKey string
+	ContainerKey    string
+	PodKey          string
+	ProviderIDKey   string
+	DeviceKey       string
+}
+
+// DefaultResultKeys returns a new ResultKeys instance with typical default values.
+func DefaultResultKeys() *ResultKeys {
+	return &ResultKeys{
+		ClusterKey:      ClusterIDLabel,
+		NamespaceKey:    NamespaceLabel,
+		NodeKey:         NodeLabel,
+		InstanceKey:     InstanceLabel,
+		InstanceTypeKey: InstanceTypeLabel,
+		ContainerKey:    ContainerLabel,
+		PodKey:          PodLabel,
+		ProviderIDKey:   ProviderIDLabel,
+		DeviceKey:       DeviceLabel,
+	}
+}
+
+// ClusterKeyWithDefaults returns a new ResultKeys instance with the provided cluster key and the
+// rest of the keys set to their default values.
+func ClusterKeyWithDefaults(clusterKey string) *ResultKeys {
+	keys := DefaultResultKeys()
+	keys.ClusterKey = clusterKey
+	return keys
+}
+
+// QueryResults contains all of the query results and the source query string.
+type QueryResults struct {
+	Query   string
+	Error   error
+	Results []*QueryResult
+}
+
+func NewQueryResults(query string) *QueryResults {
+	return &QueryResults{
+		Query: query,
+	}
+}
+
+// QueryResult contains a single result from a prometheus query. It's common
+// to refer to query results as a slice of QueryResult
+type QueryResult struct {
+	Metric map[string]interface{} `json:"metric"`
+	Values []*util.Vector         `json:"values"`
+
+	keys *ResultKeys
+}
+
+func NewQueryResult(metrics map[string]any, values []*util.Vector, keys *ResultKeys) *QueryResult {
+	if keys == nil {
+		keys = DefaultResultKeys()
+	}
+
+	return &QueryResult{
+		Metric: metrics,
+		Values: values,
+		keys:   keys,
+	}
+}
+
+func (qr *QueryResult) GetCluster() (string, error) {
+	return qr.GetString(qr.keys.ClusterKey)
+}
+
+func (qr *QueryResult) GetNamespace() (string, error) {
+	return qr.GetString(qr.keys.NamespaceKey)
+}
+
+func (qr *QueryResult) GetNode() (string, error) {
+	return qr.GetString(qr.keys.NodeKey)
+}
+
+func (qr *QueryResult) GetInstance() (string, error) {
+	return qr.GetString(qr.keys.InstanceKey)
+}
+
+func (qr *QueryResult) GetInstanceType() (string, error) {
+	return qr.GetString(qr.keys.InstanceTypeKey)
+}
+
+func (qr *QueryResult) GetContainer() (string, error) {
+	value, err := qr.GetString(qr.keys.ContainerKey)
+	if value == "" || err != nil {
+		alternate, e := qr.GetString(qr.keys.ContainerKey + "_name")
+		if alternate == "" || e != nil {
+			return "", fmt.Errorf("'%s' and '%s' fields do not exist in data result vector", qr.keys.ContainerKey, qr.keys.ContainerKey+"_name")
+		}
+		return alternate, nil
+	}
+	return value, nil
+}
+
+func (qr *QueryResult) GetPod() (string, error) {
+	value, err := qr.GetString(qr.keys.PodKey)
+	if value == "" || err != nil {
+		alternate, e := qr.GetString(qr.keys.PodKey + "_name")
+		if alternate == "" || e != nil {
+			return "", fmt.Errorf("'%s' and '%s' fields do not exist in data result vector", qr.keys.PodKey, qr.keys.PodKey+"_name")
+		}
+		return alternate, nil
+	}
+	return value, nil
+}
+
+func (qr *QueryResult) GetProviderID() (string, error) {
+	return qr.GetString(qr.keys.ProviderIDKey)
+}
+
+func (qr *QueryResult) GetDevice() (string, error) {
+	return qr.GetString(qr.keys.DeviceKey)
+}
+
+// GetString returns the requested field, or an error if it does not exist
+func (qr *QueryResult) GetString(field string) (string, error) {
+	f, ok := qr.Metric[field]
+	if !ok {
+		return "", fmt.Errorf("'%s' field does not exist in data result vector", field)
+	}
+
+	strField, ok := f.(string)
+	if !ok {
+		return "", fmt.Errorf("'%s' field is improperly formatted and cannot be converted to string", field)
+	}
+
+	return strField, nil
+}
+
+// GetStrings returns the requested fields, or an error if it does not exist
+func (qr *QueryResult) GetStrings(fields ...string) (map[string]string, error) {
+	values := map[string]string{}
+
+	for _, field := range fields {
+		f, ok := qr.Metric[field]
+		if !ok {
+			return nil, fmt.Errorf("'%s' field does not exist in data result vector", field)
+		}
+
+		value, ok := f.(string)
+		if !ok {
+			return nil, fmt.Errorf("'%s' field is improperly formatted and cannot be converted to string", field)
+		}
+
+		values[field] = value
+	}
+
+	return values, nil
+}
+
+// GetLabels returns all labels and their values from the query result
+func (qr *QueryResult) GetLabels() map[string]string {
+	result := make(map[string]string)
+
+	// Find All keys with prefix label_, remove prefix, add to labels
+	for k, v := range qr.Metric {
+		if !strings.HasPrefix(k, "label_") {
+			continue
+		}
+
+		label := strings.TrimPrefix(k, "label_")
+		value, ok := v.(string)
+		if !ok {
+			log.Warnf("Failed to parse label value for label: '%s'", label)
+			continue
+		}
+
+		result[label] = value
+	}
+
+	return result
+}
+
+// GetAnnotations returns all annotations and their values from the query result
+func (qr *QueryResult) GetAnnotations() map[string]string {
+	result := make(map[string]string)
+
+	// Find All keys with prefix annotation_, remove prefix, add to annotations
+	for k, v := range qr.Metric {
+		if !strings.HasPrefix(k, "annotation_") {
+			continue
+		}
+
+		annotations := strings.TrimPrefix(k, "annotation_")
+		value, ok := v.(string)
+		if !ok {
+			log.Warnf("Failed to parse label value for label: '%s'", annotations)
+			continue
+		}
+
+		result[annotations] = value
+	}
+
+	return result
+}

+ 0 - 0
pkg/storage/azurestorage.go → core/pkg/storage/azurestorage.go


+ 0 - 0
pkg/storage/bucketstorage.go → core/pkg/storage/bucketstorage.go


+ 0 - 0
pkg/storage/bucketstorage_test.go → core/pkg/storage/bucketstorage_test.go


+ 0 - 0
pkg/storage/filestorage.go → core/pkg/storage/filestorage.go


+ 0 - 0
pkg/storage/filestorage_test.go → core/pkg/storage/filestorage_test.go


+ 0 - 0
pkg/storage/gcsstorage.go → core/pkg/storage/gcsstorage.go


+ 124 - 0
core/pkg/storage/memfile/memfile.go

@@ -0,0 +1,124 @@
+package memfile
+
+import (
+	"iter"
+	"maps"
+	"time"
+)
+
+// MemoryFile represents a file in memory storage. It's part of the directory tree
+// structure used to look up files by path.
+type MemoryFile struct {
+	Name     string
+	Contents []byte
+	ModTime  time.Time
+
+	directory *MemoryDirectory
+}
+
+// Size returns the size of the file in bytes.
+func (mf *MemoryFile) Size() int64 {
+	return int64(len(mf.Contents))
+}
+
+// NewMemoryFile creates a new MemoryFile instance with the provided name and and byte contents.
+func NewMemoryFile(name string, contents []byte) *MemoryFile {
+	return &MemoryFile{
+		Name:      name,
+		Contents:  contents,
+		ModTime:   time.Now().UTC(),
+		directory: nil,
+	}
+}
+
+// MemoryDirectory represents a directory in memory storage. It is the root of the file system
+// tree structure used to look up files by path.
+type MemoryDirectory struct {
+	Name    string
+	ModTime time.Time
+
+	dirs      map[string]*MemoryDirectory
+	files     map[string]*MemoryFile
+	directory *MemoryDirectory
+}
+
+// NewMemoryDirectory creates a new Directory instance with the provided path name.
+func NewMemoryDirectory(name string) *MemoryDirectory {
+	return &MemoryDirectory{
+		Name:  name,
+		dirs:  make(map[string]*MemoryDirectory),
+		files: make(map[string]*MemoryFile),
+	}
+}
+
+// Size returns the size of all subdirectories and files within this directory.
+func (d *MemoryDirectory) Size() int64 {
+	var size int64
+	for _, f := range d.files {
+		size += f.Size()
+	}
+	for _, subdir := range d.dirs {
+		size += subdir.Size()
+	}
+	return size
+}
+
+// AddFile adds a file to the directory. Note that files can only exist within a single directory
+// at a time.
+func (d *MemoryDirectory) AddFile(f *MemoryFile) {
+	if f.directory != nil {
+		f.directory.RemoveFile(f.Name)
+		f.directory = nil
+	}
+
+	d.files[f.Name] = f
+	d.ModTime = time.Now().UTC()
+	f.directory = d
+}
+
+// AddDirectory adds a subdirectory to the parent directory. Note that directories can only exist within a single directory.
+func (d *MemoryDirectory) AddDirectory(subdir *MemoryDirectory) {
+	if subdir.directory != nil {
+		subdir.directory.RemoveDirectory(subdir.Name)
+		subdir.directory = nil
+	}
+
+	d.dirs[subdir.Name] = subdir
+	d.ModTime = time.Now().UTC()
+	subdir.directory = d
+}
+
+// RemoveFile removes a file from the directoory tree.
+func (d *MemoryDirectory) RemoveFile(name string) {
+	if _, ok := d.files[name]; ok {
+		delete(d.files, name)
+		d.ModTime = time.Now().UTC()
+	}
+}
+
+// RemoveDirectory remove a subdirectory from the directory tree.
+func (d *MemoryDirectory) RemoveDirectory(name string) {
+	if _, ok := d.dirs[name]; ok {
+		delete(d.dirs, name)
+		d.ModTime = time.Now().UTC()
+	}
+}
+
+// FileCount returns the total number of files in this directory.
+func (d *MemoryDirectory) FileCount() int {
+	return len(d.files)
+}
+
+// DirCount returns the total number of subdirectories in this directory.
+func (d *MemoryDirectory) DirCount() int {
+	return len(d.dirs)
+}
+
+// Files returns a slice of files located within this directory.
+func (d *MemoryDirectory) Files() iter.Seq[*MemoryFile] {
+	return maps.Values(d.files)
+}
+
+func (d *MemoryDirectory) Directories() iter.Seq[*MemoryDirectory] {
+	return maps.Values(d.dirs)
+}

+ 57 - 0
core/pkg/storage/memfile/util.go

@@ -0,0 +1,57 @@
+package memfile
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+)
+
+// SplitPaths splits the directory path into a slice of directory names.
+func SplitPaths(path string) []string {
+	path = filepath.Clean(path)
+	if path[len(path)-1] == filepath.Separator {
+		path = path[:len(path)-1]
+	}
+	return strings.Split(path, string(filepath.Separator))
+}
+
+// Split splits the path into a slice of directory names and the file name.
+func Split(path string) ([]string, string) {
+	path = filepath.Clean(path)
+	pDir, pFile := filepath.Split(path)
+	pDir = filepath.Dir(pDir)
+
+	return strings.Split(pDir, string(filepath.Separator)), pFile
+}
+
+// CreateSubdirectory creates the necessary subdirectories within the provided MemoryDirectory.
+func CreateSubdirectory(d *MemoryDirectory, paths []string) *MemoryDirectory {
+	currentDir := d
+
+	for i := 0; i < len(paths); i++ {
+		dirName := paths[i]
+		if _, ok := currentDir.dirs[dirName]; !ok {
+			currentDir.AddDirectory(NewMemoryDirectory(dirName))
+		}
+		currentDir = currentDir.dirs[dirName]
+	}
+
+	return currentDir
+}
+
+// FindSubdirectory searches through the provided path slice starting with the provided directory,
+// and returns the correct MemoryDirectory if it exists. If the directory does not exist, an error is
+// returned containing the path where the find failed.
+func FindSubdirectory(d *MemoryDirectory, paths []string) (*MemoryDirectory, error) {
+	currentDir := d
+
+	for i := 0; i < len(paths); i++ {
+		dirName := paths[i]
+		if _, ok := currentDir.dirs[dirName]; !ok {
+			return nil, fmt.Errorf("directory %s not found", filepath.Join(paths[:i+1]...))
+		}
+		currentDir = currentDir.dirs[dirName]
+	}
+
+	return currentDir, nil
+}

+ 171 - 0
core/pkg/storage/memorystorage.go

@@ -0,0 +1,171 @@
+package storage
+
+import (
+	"fmt"
+	"path/filepath"
+	"sync"
+
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/storage/memfile"
+)
+
+// MemoryStorage is a thread-safe in-memory file system storage implementation. It can be used for testing storage.Storage dependents
+// or to serve as a lightweight storage implementation within a production system.
+type MemoryStorage struct {
+	lock        sync.Mutex
+	directPaths map[string]*memfile.MemoryFile
+	fileTree    *memfile.MemoryDirectory
+}
+
+// NewMemoryStorage creates a new in-memory file system storage implementation.
+func NewMemoryStorage() *MemoryStorage {
+	return &MemoryStorage{
+		directPaths: make(map[string]*memfile.MemoryFile),
+		fileTree:    memfile.NewMemoryDirectory(""),
+	}
+}
+
+// StorageType returns a string identifier for the type of storage used by the implementation.
+func (ms *MemoryStorage) StorageType() StorageType {
+	return StorageTypeMemory
+}
+
+// FullPath returns the storage working path combined with the path provided
+func (ms *MemoryStorage) FullPath(path string) string {
+	return path
+}
+
+// Stat returns the StorageStats for the specific path.
+func (ms *MemoryStorage) Stat(path string) (*StorageInfo, error) {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	path = filepath.Clean(path)
+	if file, ok := ms.directPaths[path]; ok {
+		return &StorageInfo{
+			Name:    file.Name,
+			Size:    file.Size(),
+			ModTime: file.ModTime,
+		}, nil
+	}
+
+	return nil, fmt.Errorf("file not found: %s - %w", path, DoesNotExistError)
+}
+
+// Read uses the relative path of the storage combined with the provided path to
+// read the contents.
+func (ms *MemoryStorage) Read(path string) ([]byte, error) {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	path = filepath.Clean(path)
+
+	if file, ok := ms.directPaths[path]; ok {
+		return file.Contents, nil
+	}
+
+	return nil, fmt.Errorf("file not found: %s - %w", path, DoesNotExistError)
+}
+
+// Write uses the relative path of the storage combined with the provided path
+// to write a new file or overwrite an existing file.
+func (ms *MemoryStorage) Write(path string, data []byte) error {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	paths, pFile := memfile.Split(path)
+
+	f := memfile.NewMemoryFile(pFile, data)
+	currentDir := memfile.CreateSubdirectory(ms.fileTree, paths)
+
+	currentDir.AddFile(f)
+	ms.directPaths[path] = f
+	return nil
+}
+
+// Remove uses the relative path of the storage combined with the provided path to
+// remove a file from storage permanently.
+func (ms *MemoryStorage) Remove(path string) error {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	path = filepath.Clean(path)
+	paths, pFile := memfile.Split(path)
+
+	currentDir, err := memfile.FindSubdirectory(ms.fileTree, paths)
+	if err != nil {
+		return fmt.Errorf("file not found: %s - %w", path, DoesNotExistError)
+	}
+
+	currentDir.RemoveFile(pFile)
+
+	delete(ms.directPaths, path)
+	return nil
+}
+
+// Exists uses the relative path of the storage combined with the provided path to
+// determine if the file exists.
+func (ms *MemoryStorage) Exists(path string) (bool, error) {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	path = filepath.Clean(path)
+
+	_, ok := ms.directPaths[path]
+	return ok, nil
+}
+
+// List uses the relative path of the storage combined with the provided path to return
+// storage information for the files.
+func (ms *MemoryStorage) List(path string) ([]*StorageInfo, error) {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	paths := memfile.SplitPaths(path)
+	currentDir, err := memfile.FindSubdirectory(ms.fileTree, paths)
+	if err != nil {
+		// contract for bucket storages returns an empty list in this case
+		// so just log a warning, and return an empty list
+		log.Warnf("failed to resolve path: %s - %s", path, err)
+		return []*StorageInfo{}, nil
+	}
+
+	storageInfos := make([]*StorageInfo, 0, currentDir.FileCount())
+	for f := range currentDir.Files() {
+		storageInfos = append(storageInfos, &StorageInfo{
+			Name:    f.Name,
+			Size:    f.Size(),
+			ModTime: f.ModTime,
+		})
+	}
+
+	return storageInfos, nil
+}
+
+// ListDirectories uses the relative path of the storage combined with the provided path
+// to return storage information for only directories contained along the path. This
+// functions as List, but returns storage information for only directories.
+func (ms *MemoryStorage) ListDirectories(path string) ([]*StorageInfo, error) {
+	ms.lock.Lock()
+	defer ms.lock.Unlock()
+
+	paths := memfile.SplitPaths(path)
+	currentDir, err := memfile.FindSubdirectory(ms.fileTree, paths)
+	if err != nil {
+		// contract for bucket storages returns an empty list in this case
+		// so just log a warning, and return an empty list
+		log.Warnf("failed to resolve path: %s - %s", path, err)
+		return []*StorageInfo{}, nil
+	}
+
+	storageInfos := make([]*StorageInfo, 0, currentDir.DirCount())
+	for d := range currentDir.Directories() {
+		storageInfos = append(storageInfos, &StorageInfo{
+			Name:    filepath.Join(append(paths, d.Name)...) + "/",
+			Size:    d.Size(),
+			ModTime: d.ModTime,
+		})
+	}
+
+	return storageInfos, nil
+}

+ 382 - 0
core/pkg/storage/memorystorage_test.go

@@ -0,0 +1,382 @@
+package storage
+
+import (
+	"path"
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/util/json"
+)
+
+func TestMemoryStorage_List(t *testing.T) {
+	store := NewMemoryStorage()
+	testName := "list"
+
+	fileNames := []string{
+		"/file0.json",
+		"/file1.json",
+		"/dir0/file2.json",
+		"/dir0/file3.json",
+	}
+
+	err := createFiles(fileNames, testName, store)
+	if err != nil {
+		t.Errorf("failed to create files: %s", err)
+	}
+
+	defer func() {
+		err = cleanupFiles(fileNames, testName, store)
+		if err != nil {
+			t.Errorf("failed to clean up files: %s", err)
+		}
+	}()
+
+	testCases := map[string]struct {
+		path      string
+		expected  []string
+		expectErr bool
+	}{
+		"base dir files": {
+			path: path.Join(testpath, testName),
+			expected: []string{
+				"file0.json",
+				"file1.json",
+			},
+			expectErr: false,
+		},
+		"single nested dir files": {
+			path: path.Join(testpath, testName, "dir0"),
+			expected: []string{
+				"file2.json",
+				"file3.json",
+			},
+			expectErr: false,
+		},
+		"nonexistent dir files": {
+			path:      path.Join(testpath, testName, "dir1"),
+			expected:  []string{},
+			expectErr: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			fileList, err := store.List(tc.path)
+			if tc.expectErr == (err == nil) {
+				if tc.expectErr {
+					t.Errorf("expected error was not thrown")
+					return
+				}
+				t.Errorf("unexpected error: %s", err.Error())
+				return
+			}
+
+			if len(fileList) != len(tc.expected) {
+				t.Errorf("file list length does not match expected length, actual: %d, expected: %d", len(fileList), len(tc.expected))
+			}
+
+			expectedSet := map[string]struct{}{}
+			for _, expName := range tc.expected {
+				expectedSet[expName] = struct{}{}
+			}
+
+			for _, file := range fileList {
+				_, ok := expectedSet[file.Name]
+				if !ok {
+					t.Errorf("unexpect file in list %s", file.Name)
+				}
+
+				if file.Size == 0 {
+					t.Errorf("file size is not set")
+				}
+
+				if file.ModTime.IsZero() {
+					t.Errorf("file mod time is not set")
+				}
+			}
+		})
+	}
+}
+
+func TestMemoryStorage_ListDirectories(t *testing.T) {
+	store := NewMemoryStorage()
+	testName := "list_directories"
+
+	fileNames := []string{
+		"/file0.json",
+		"/dir0/file2.json",
+		"/dir0/file3.json",
+		"/dir0/dir1/file4.json",
+		"/dir0/dir2/file5.json",
+	}
+
+	err := createFiles(fileNames, testName, store)
+	if err != nil {
+		t.Errorf("failed to create files: %s", err)
+	}
+
+	defer func() {
+		err = cleanupFiles(fileNames, testName, store)
+		if err != nil {
+			t.Errorf("failed to clean up files: %s", err)
+		}
+	}()
+
+	testCases := map[string]struct {
+		path      string
+		expected  []string
+		expectErr bool
+	}{
+		"base dir dir": {
+			path: path.Join(testpath, testName),
+			expected: []string{
+				path.Join(testpath, testName, "dir0") + "/",
+			},
+			expectErr: false,
+		},
+		"single nested dir files": {
+			path: path.Join(testpath, testName, "dir0"),
+			expected: []string{
+				path.Join(testpath, testName, "dir0", "dir1") + "/",
+				path.Join(testpath, testName, "dir0", "dir2") + "/",
+			},
+			expectErr: false,
+		},
+		"dir with no sub dirs": {
+			path:      path.Join(testpath, testName, "dir0/dir1"),
+			expected:  []string{},
+			expectErr: false,
+		},
+		"non-existent dir": {
+			path:      path.Join(testpath, testName, "dir1"),
+			expected:  []string{},
+			expectErr: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			dirList, err := store.ListDirectories(tc.path)
+			if tc.expectErr == (err == nil) {
+				if tc.expectErr {
+					t.Errorf("expected error was not thrown")
+					return
+				}
+				t.Errorf("unexpected error: %s", err.Error())
+				return
+			}
+
+			if len(dirList) != len(tc.expected) {
+				t.Errorf("dir list length does not match expected length, actual: %d, expected: %d", len(dirList), len(tc.expected))
+			}
+
+			expectedSet := map[string]struct{}{}
+			for _, expName := range tc.expected {
+				expectedSet[expName] = struct{}{}
+			}
+
+			for _, dir := range dirList {
+				_, ok := expectedSet[dir.Name]
+				if !ok {
+					t.Errorf("unexpect dir: %s in list %s", dir.Name, tc.path)
+				}
+			}
+		})
+	}
+}
+
+func TestMemoryStorage_Exists(t *testing.T) {
+	store := NewMemoryStorage()
+	testName := "exists"
+	fileNames := []string{
+		"/file0.json",
+	}
+
+	err := createFiles(fileNames, testName, store)
+	if err != nil {
+		t.Errorf("failed to create files: %s", err)
+	}
+
+	defer func() {
+		err = cleanupFiles(fileNames, testName, store)
+		if err != nil {
+			t.Errorf("failed to clean up files: %s", err)
+		}
+	}()
+
+	testCases := map[string]struct {
+		path      string
+		expected  bool
+		expectErr bool
+	}{
+		"file exists": {
+			path:      path.Join(testpath, testName, "file0.json"),
+			expected:  true,
+			expectErr: false,
+		},
+		"file does not exist": {
+			path:      path.Join(testpath, testName, "file1.json"),
+			expected:  false,
+			expectErr: false,
+		},
+		"dir does not exist": {
+			path:      path.Join(testpath, testName, "dir0/file.json"),
+			expected:  false,
+			expectErr: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			exists, err := store.Exists(tc.path)
+			if tc.expectErr == (err == nil) {
+				if tc.expectErr {
+					t.Errorf("expected error was not thrown")
+					return
+				}
+				t.Errorf("unexpected error: %s", err.Error())
+				return
+			}
+
+			if exists != tc.expected {
+				t.Errorf("file exists output did not match expected")
+			}
+		})
+	}
+}
+
+func TestMemoryStorage_Read(t *testing.T) {
+	store := NewMemoryStorage()
+	testName := "read"
+
+	fileNames := []string{
+		"/file0.json",
+	}
+
+	err := createFiles(fileNames, testName, store)
+	if err != nil {
+		t.Errorf("failed to create files: %s", err)
+	}
+
+	defer func() {
+		err = cleanupFiles(fileNames, testName, store)
+		if err != nil {
+			t.Errorf("failed to clean up files: %s", err)
+		}
+	}()
+
+	testCases := map[string]struct {
+		path      string
+		expectErr bool
+	}{
+		"file exists": {
+			path:      path.Join(testpath, testName, "file0.json"),
+			expectErr: false,
+		},
+		"file does not exist": {
+			path:      path.Join(testpath, testName, "file1.json"),
+			expectErr: true,
+		},
+		"dir does not exist": {
+			path:      path.Join(testpath, testName, "dir0/file.json"),
+			expectErr: true,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			b, err := store.Read(tc.path)
+			if tc.expectErr && err != nil {
+				return
+			}
+			if tc.expectErr == (err == nil) {
+				if tc.expectErr {
+					t.Errorf("expected error was not thrown")
+					return
+				}
+				t.Errorf("unexpected error: %s", err.Error())
+				return
+			}
+			var content testFileContent
+			err = json.Unmarshal(b, &content)
+			if err != nil {
+				t.Errorf("could not unmarshal file content")
+				return
+			}
+
+			if content != tfc {
+				t.Errorf("file content did not match writen value")
+			}
+		})
+	}
+}
+
+func TestMemoryStorage_Stat(t *testing.T) {
+	store := NewMemoryStorage()
+	testName := "stat"
+
+	fileNames := []string{
+		"/file0.json",
+	}
+
+	err := createFiles(fileNames, testName, store)
+	if err != nil {
+		t.Errorf("failed to create files: %s", err)
+	}
+
+	defer func() {
+		err = cleanupFiles(fileNames, testName, store)
+		if err != nil {
+			t.Errorf("failed to clean up files: %s", err)
+		}
+	}()
+
+	testCases := map[string]struct {
+		path      string
+		expected  *StorageInfo
+		expectErr bool
+	}{
+		"base dir": {
+			path: path.Join(testpath, testName, "file0.json"),
+			expected: &StorageInfo{
+				Name: "file0.json",
+				Size: 45,
+			},
+			expectErr: false,
+		},
+		"file does not exist": {
+			path:      path.Join(testpath, testName, "file1.json"),
+			expected:  nil,
+			expectErr: true,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			status, err := store.Stat(tc.path)
+			if tc.expectErr && err != nil {
+				return
+			}
+			if tc.expectErr == (err == nil) {
+				if tc.expectErr {
+					t.Errorf("expected error was not thrown")
+					return
+				}
+				t.Errorf("unexpected error: %s", err.Error())
+				return
+			}
+
+			if status.Name != tc.expected.Name {
+				t.Errorf("status name did name match expected, actual: %s, expected: %s", status.Name, tc.expected.Name)
+			}
+
+			if status.Size != tc.expected.Size {
+				t.Errorf("status name did size match expected, actual: %d, expected: %d", status.Size, tc.expected.Size)
+			}
+
+			if status.ModTime.IsZero() {
+				t.Errorf("status mod time is not set")
+			}
+
+		})
+	}
+}

+ 0 - 0
pkg/storage/prefixedbucketstorage.go → core/pkg/storage/prefixedbucketstorage.go


+ 15 - 0
pkg/storage/s3storage.go → core/pkg/storage/s3storage.go

@@ -618,6 +618,10 @@ func (a *awsAuth) Retrieve() (credentials.Value, error) {
 	}, nil
 }
 
+func (a *awsAuth) RetrieveWithCredContext(ctx *credentials.CredContext) (credentials.Value, error) {
+	return a.Retrieve()
+}
+
 // IsExpired returns if the credentials have been retrieved.
 func (a *awsAuth) IsExpired() bool {
 	return a.creds.Expired()
@@ -638,3 +642,14 @@ func (s *overrideSignerType) Retrieve() (credentials.Value, error) {
 	}
 	return v, nil
 }
+
+func (s *overrideSignerType) RetrieveWithCredContext(ctx *credentials.CredContext) (credentials.Value, error) {
+	v, err := s.Provider.RetrieveWithCredContext(ctx)
+	if err != nil {
+		return v, err
+	}
+	if !v.SignerType.IsAnonymous() {
+		v.SignerType = s.signerType
+	}
+	return v, nil
+}

+ 0 - 0
pkg/storage/storage.go → core/pkg/storage/storage.go


+ 6 - 0
pkg/storage/storagetypes.go → core/pkg/storage/storagetypes.go

@@ -18,6 +18,7 @@ import (
 type StorageType string
 
 const (
+	StorageTypeMemory      StorageType = "memory"
 	StorageTypeFile        StorageType = "file"
 	StorageTypeBucketS3    StorageType = "bucket|s3"
 	StorageTypeBucketGCS   StorageType = "bucket|gcs"
@@ -55,6 +56,11 @@ func (st *StorageType) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
+// IsMemoryStorage returns true if the StorageType is a memory storage type.
+func (st StorageType) IsMemoryStorage() bool {
+	return st.BackendType() == "memory"
+}
+
 // IsFileStorage returns true if the StorageType is a file storage type.
 func (st StorageType) IsFileStorage() bool {
 	return st.BackendType() == "file"

+ 0 - 0
pkg/storage/storagetypes_test.go → core/pkg/storage/storagetypes_test.go


+ 0 - 0
pkg/storage/tlsconfig.go → core/pkg/storage/tlsconfig.go


+ 21 - 0
core/pkg/util/atomic/atomicrunstate.go

@@ -11,6 +11,9 @@ type AtomicRunState struct {
 	stopping bool
 	stop     chan struct{}
 	reset    chan struct{}
+
+	// buffer contains channels that are returned before Start() is called.
+	stopBuffer []chan struct{}
 }
 
 // Start checks for an existing run state and returns false if the run state has already started. If
@@ -24,6 +27,18 @@ func (ars *AtomicRunState) Start() bool {
 	}
 
 	ars.stop = make(chan struct{})
+
+	// if there are any channels in the buffer, assign them a wait routine on
+	// the stop channel.
+	for _, ch := range ars.stopBuffer {
+		go func(ch chan struct{}) {
+			defer close(ch)
+
+			<-ars.stop
+		}(ch)
+	}
+	ars.stopBuffer = nil
+
 	return true
 }
 
@@ -34,6 +49,12 @@ func (ars *AtomicRunState) OnStop() <-chan struct{} {
 	ars.lock.Lock()
 	defer ars.lock.Unlock()
 
+	if ars.stop == nil {
+		ch := make(chan struct{})
+		ars.stopBuffer = append(ars.stopBuffer, ch)
+		return ch
+	}
+
 	return ars.stop
 }
 

+ 70 - 21
core/pkg/util/atomic/atomicrunstate_test.go

@@ -1,6 +1,7 @@
 package atomic
 
 import (
+	"fmt"
 	"sync"
 	"testing"
 	"time"
@@ -124,46 +125,49 @@ func TestContinuousConcurrentStartsAndStops(t *testing.T) {
 	// continuously try and start the ars on a tight loop
 	// throttled by OnStop and WaitForReset()
 	go func() {
-		defer func() {
-			if e := recover(); e != nil {
-				// sometimes the waitgroup will hit a negative value at the end of the test
-				// this is ok given the way the test behaves (chaos star/stop calls), so
-				// we can safely ignore.
-			}
-		}()
-
-		firstCycle := true
-		for {
+		c := cycles
+		for c > 0 {
 			ars.WaitForReset()
 			if ars.Start() {
 				t.Logf("Started")
-				if firstCycle {
-					firstCycle = false
+				if c == cycles {
 					started <- true
 				}
-				wg.Done()
+				c--
 			}
-
-			<-ars.OnStop()
-			t.Logf("Stopped")
 		}
 	}()
 
 	// wait for an initial start
 	<-started
 
-	// Loop Stop/Resets from other goroutines
+	// Loop Stop from other goroutines
 	go func() {
-		for {
+		c := cycles
+		for c > 0 {
 			time.Sleep(100 * time.Millisecond)
 			if ars.Stop() {
-				<-ars.OnStop()
-				time.Sleep(500 * time.Millisecond)
-				ars.Reset()
+				t.Logf("Wait for stop")
+				c--
 			}
 		}
 	}()
 
+	// Loop OnStop and Resets
+	go func() {
+		c := cycles
+
+		time.Sleep(150 * time.Millisecond)
+		for c > 0 {
+			<-ars.OnStop()
+			t.Logf("Stopped")
+			time.Sleep(500 * time.Millisecond)
+			ars.Reset()
+			c--
+			wg.Done()
+		}
+	}()
+
 	// Wait for full cycles
 	select {
 	case <-time.After(5 * time.Second):
@@ -172,3 +176,48 @@ func TestContinuousConcurrentStartsAndStops(t *testing.T) {
 		t.Logf("Completed!")
 	}
 }
+
+func TestStopChannelWhenStopped(t *testing.T) {
+	t.Parallel()
+
+	// This scenario is a bit odd, but there was a bug where waiting on `OnStop()`
+	// before the run state is started will indefinitely block. The problem is resolved by
+	// buffering the stop channel with intermediate channels until Start() is called.
+
+	var ars AtomicRunState
+
+	finished := make(chan struct{})
+	errors := make(chan error)
+
+	go func() {
+		<-ars.OnStop()
+		t.Logf("Stopped")
+		finished <- struct{}{}
+	}()
+
+	// wait a bit, then start and stop the run state -- the OnStop
+	// channel should complete.
+	go func() {
+		time.Sleep(1 * time.Second)
+		ars.WaitForReset()
+
+		if !ars.Start() {
+			errors <- fmt.Errorf("Failed to Start() AtomicRunState")
+		}
+		time.Sleep(500 * time.Millisecond)
+
+		if !ars.Stop() {
+			errors <- fmt.Errorf("Failed to Stop() AtomicRunState")
+		}
+	}()
+
+	select {
+	case <-time.After(5 * time.Second):
+		t.Fatalf("Didn't complete after 5 seconds")
+	case e := <-errors:
+		t.Fatalf("Received error from goroutine: %s", e)
+	case <-finished:
+		t.Logf("Completed!")
+	}
+
+}

+ 4 - 9
core/pkg/util/buffer.go

@@ -6,7 +6,6 @@ import (
 	"errors"
 	"io"
 	"math"
-	"reflect"
 	"unsafe"
 
 	"github.com/opencost/opencost/core/pkg/util/stringutil"
@@ -114,6 +113,7 @@ func (b *Buffer) WriteFloat64(i float64) {
 // WriteString writes the string's length as a uint16 followed by the string contents.
 func (b *Buffer) WriteString(i string) {
 	s := stringToBytes(i)
+
 	// string lengths are limited to uint16 - See ReadString()
 	if len(s) > math.MaxUint16 {
 		s = s[:math.MaxUint16]
@@ -401,7 +401,7 @@ func bytesToString(b []byte) string {
 	// cached string. If it does _not_ exist, then we use the passed func() string to allocate a new
 	// string and cache it. This will prevent us from allocating throw-away strings just to
 	// check our cache.
-	pinned := *(*string)(unsafe.Pointer(&b))
+	pinned := unsafe.String(unsafe.SliceData(b), len(b))
 
 	return stringutil.BankFunc(pinned, func() string {
 		return string(b)
@@ -409,11 +409,6 @@ func bytesToString(b []byte) string {
 }
 
 // Direct string to byte conversion that doesn't allocate.
-func stringToBytes(s string) (b []byte) {
-	strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
-	sh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
-	sh.Data = strh.Data
-	sh.Len = strh.Len
-	sh.Cap = strh.Len
-	return b
+func stringToBytes(s string) []byte {
+	return unsafe.Slice(unsafe.StringData(s), len(s))
 }

+ 57 - 0
core/pkg/util/buffer_test.go

@@ -4,7 +4,10 @@ import (
 	"bytes"
 	"math"
 	"math/rand"
+	"runtime"
+	"strings"
 	"testing"
+	"time"
 )
 
 func TestBufferReadWrite(t *testing.T) {
@@ -226,6 +229,60 @@ func generateRandomString(ln int) string {
 	return string(b)
 }
 
+func memMib() float64 {
+	var m runtime.MemStats
+	runtime.ReadMemStats(&m)
+	return float64(m.Alloc) / 1024.0 / 1024.0
+}
+
+func TestStringBytes(t *testing.T) {
+	baselineMem := memMib()
+
+	b := make([]byte, 10<<20)
+
+	afterMem := memMib()
+	delta := afterMem - baselineMem
+	t.Logf("Allocated %v MiB, Delta: %v MiB", afterMem, delta)
+
+	s := "Hello World!"
+	sl := b[512 : 512+len(s)]
+	copy(sl, stringToBytes(s))
+
+	afterMem = memMib()
+	delta = afterMem - baselineMem
+	t.Logf("Allocated %v MiB, Delta: %v MiB", afterMem, delta)
+
+	// this should pin the large backing array in memory, preventing it from being GC'd
+	newS := bytesToString(sl)
+
+	runtime.GC()
+	time.Sleep(time.Second)
+
+	afterMem = memMib()
+	delta = afterMem - baselineMem
+	t.Logf("S: %s, Allocated %v MiB, Delta: %v MiB", newS, afterMem, delta)
+
+	// copy the string into a new string and clear out pinned string
+	sCopy := strings.Clone(newS)
+	newS = ""
+
+	// Now that we've dropped the reference to the pinned backing array, it should be GC'd
+	runtime.GC()
+	time.Sleep(time.Second)
+
+	afterMem = memMib()
+	delta = afterMem - baselineMem
+	t.Logf("S: %s, Allocated %v MiB, Delta: %v MiB", sCopy, afterMem, delta)
+
+	if sCopy != s {
+		t.Errorf("Expected string to be %v, got %v", s, sCopy)
+	}
+
+	if delta > 0.5 {
+		t.Errorf("Expected memory delta to be less than 0.5 MiB, got %v MiB", delta)
+	}
+}
+
 func TestTooLargeStringTruncate(t *testing.T) {
 	normalStr := generateRandomString(100)
 	bigStr := generateRandomString(math.MaxUint16 + (math.MaxUint16 / 2))

+ 52 - 0
core/pkg/util/iterutil/iterutil.go

@@ -0,0 +1,52 @@
+package iterutil
+
+import "iter"
+
+// Combine takes two iterator sequences and combines them into a single iterator sequence of pairs.
+// This iterator will only yield as many values as the smallest of the two sequences.
+func Combine[T any, U any](seq1 iter.Seq[T], seq2 iter.Seq[U]) iter.Seq2[T, U] {
+	return func(yield func(T, U) bool) {
+		n1, s1 := iter.Pull(seq1)
+		n2, s2 := iter.Pull(seq2)
+
+		defer s1()
+		defer s2()
+
+		for {
+			first, fOk := n1()
+			if !fOk {
+				return
+			}
+
+			second, sOk := n2()
+			if !sOk {
+				return
+			}
+
+			if !yield(first, second) {
+				return
+			}
+		}
+	}
+}
+
+// Concat takes multiple iterator sequences and concatenates them into a single iterator sequence.
+// This iterator will yield all values from the first sequence, followed by all values from the second
+// sequence, and so on.
+func Concat[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
+	return func(yield func(T) bool) {
+		for _, seq := range seqs {
+			func() {
+				n, s := iter.Pull(seq)
+				defer s()
+
+				for {
+					v, ok := n()
+					if !ok || !yield(v) {
+						return
+					}
+				}
+			}()
+		}
+	}
+}

+ 140 - 0
core/pkg/util/iterutil/iterutil_test.go

@@ -0,0 +1,140 @@
+package iterutil
+
+import (
+	"iter"
+	"testing"
+)
+
+// toSeq maintains order in the sequence
+func toSeq[T any](s []T) iter.Seq[T] {
+	return func(yield func(T) bool) {
+		for _, v := range s {
+			if !yield(v) {
+				return
+			}
+		}
+	}
+}
+
+type pair struct {
+	first  int
+	second string
+}
+
+func TestCombine(t *testing.T) {
+	type testCase struct {
+		name     string
+		input1   []int
+		input2   []string
+		expected []pair
+	}
+
+	tests := []testCase{
+		{
+			name:     "empty slices",
+			input1:   []int{},
+			input2:   []string{},
+			expected: []pair{},
+		},
+		{
+			name:   "different string length slice",
+			input1: []int{1, 2, 3},
+			input2: []string{"a", "b"},
+			expected: []pair{
+				{1, "a"},
+				{2, "b"},
+			},
+		},
+		{
+			name:   "different int length slice",
+			input1: []int{1, 2},
+			input2: []string{"a", "b", "c"},
+			expected: []pair{
+				{1, "a"},
+				{2, "b"},
+			},
+		},
+		{
+			name:   "same length slices",
+			input1: []int{1, 2, 3},
+			input2: []string{"a", "b", "c"},
+			expected: []pair{
+				{1, "a"},
+				{2, "b"},
+				{3, "c"},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := Combine(toSeq(test.input1), toSeq(test.input2))
+			for f, s := range result {
+				if !isPairIn(test.expected, pair{f, s}) {
+					t.Errorf("expected %v, got %v", test.expected, pair{f, s})
+				}
+			}
+		})
+	}
+}
+
+func TestConcat(t *testing.T) {
+	type testCase struct {
+		name     string
+		input1   []int
+		input2   []int
+		expected []int
+	}
+
+	tests := []testCase{
+		{
+			name:     "empty slices",
+			input1:   []int{},
+			input2:   []int{},
+			expected: []int{},
+		},
+		{
+			name:     "non-empty first slice",
+			input1:   []int{1, 2, 3},
+			input2:   []int{},
+			expected: []int{1, 2, 3},
+		},
+		{
+			name:     "non-empty second slice",
+			input1:   []int{},
+			input2:   []int{4, 5, 6},
+			expected: []int{4, 5, 6},
+		},
+		{
+			name:     "non-empty both slices",
+			input1:   []int{1, 2, 3},
+			input2:   []int{4, 5, 6},
+			expected: []int{1, 2, 3, 4, 5, 6},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := Concat(toSeq(test.input1), toSeq(test.input2))
+
+			// it is safe to compare this way due to the way a slice sequence iterator
+			// obeys the ordering of the slice
+			index := 0
+			for result := range results {
+				if result != test.expected[index] {
+					t.Errorf("expected %v, got %v", test.expected[index], result)
+				}
+				index++
+			}
+		})
+	}
+}
+
+func isPairIn(pairs []pair, p pair) bool {
+	for _, pair := range pairs {
+		if pair.first == p.first && pair.second == p.second {
+			return true
+		}
+	}
+	return false
+}

+ 28 - 0
core/pkg/util/maputil/maputil.go

@@ -1,5 +1,7 @@
 package maputil
 
+import "iter"
+
 // Map applies a transformation function to each value within a map to get a new map containing the
 // transformed values.
 func Map[K comparable, V any, T any](m map[K]V, transform func(V) T) map[K]T {
@@ -9,3 +11,29 @@ func Map[K comparable, V any, T any](m map[K]V, transform func(V) T) map[K]T {
 	}
 	return result
 }
+
+// Flatten returns an iterator that will iterate over a nested map.
+func Flatten[Map ~map[T]Inner, Inner ~map[T]U, T comparable, U any](m Map) iter.Seq[U] {
+	return func(yield func(U) bool) {
+		for _, inner := range m {
+			for _, value := range inner {
+				if !yield(value) {
+					return
+				}
+			}
+		}
+	}
+}
+
+// FlatMap returns an iterator that will iterate over a nested map, and apply a transformation to a different type.
+func FlatMap[Map ~map[T]Inner, Inner ~map[T]U, T comparable, U any, V any](m Map, transform func(U) V) iter.Seq[V] {
+	return func(yield func(V) bool) {
+		for _, inner := range m {
+			for _, value := range inner {
+				if !yield(transform(value)) {
+					return
+				}
+			}
+		}
+	}
+}

+ 129 - 0
core/pkg/util/maputil/maputil_test.go

@@ -0,0 +1,129 @@
+package maputil
+
+import (
+	"testing"
+)
+
+type set[T comparable] struct {
+	m map[T]struct{}
+}
+
+func newSet[T comparable](values ...T) *set[T] {
+	s := &set[T]{
+		m: make(map[T]struct{}, len(values)),
+	}
+
+	for _, v := range values {
+		s.m[v] = struct{}{}
+	}
+
+	return s
+}
+
+func (s *set[T]) contains(value T) bool {
+	_, ok := s.m[value]
+	return ok
+}
+
+func (s *set[T]) remove(value T) {
+	delete(s.m, value)
+}
+
+func TestFlatten(t *testing.T) {
+	m := map[string]map[string]int{
+		"A": {
+			"b": 1,
+			"c": 2,
+			"d": 3,
+		},
+		"B": {
+			"e": 4,
+			"f": 5,
+		},
+		"C": {
+			"g": 6,
+			"h": 7,
+			"i": 8,
+			"j": 9,
+		},
+	}
+
+	expected := newSet(1, 2, 3, 4, 5, 6, 7, 8, 9)
+
+	flattened := Flatten(m)
+	for value := range flattened {
+		if !expected.contains(value) {
+			t.Errorf("expected values did not contain the value: %d", value)
+		}
+
+		expected.remove(value)
+	}
+}
+
+func TestAliasedMapFlatten(t *testing.T) {
+	type IntMap map[string]int
+	type StringIntMap map[string]IntMap
+
+	m := StringIntMap(map[string]IntMap{
+		"A": IntMap(map[string]int{
+			"b": 1,
+			"c": 2,
+			"d": 3,
+		}),
+		"B": IntMap(map[string]int{
+			"e": 4,
+			"f": 5,
+		}),
+		"C": IntMap(map[string]int{
+			"g": 6,
+			"h": 7,
+			"i": 8,
+			"j": 9,
+		}),
+	})
+
+	expected := newSet(1, 2, 3, 4, 5, 6, 7, 8, 9)
+
+	flattened := Flatten(m)
+	for value := range flattened {
+		if !expected.contains(value) {
+			t.Errorf("expected values did not contain the value: %d", value)
+		}
+
+		expected.remove(value)
+	}
+}
+
+func TestFlatMap(t *testing.T) {
+	m := map[string]map[string]int{
+		"A": {
+			"b": 1,
+			"c": 2,
+			"d": 3,
+		},
+		"B": {
+			"e": 4,
+			"f": 5,
+		},
+		"C": {
+			"g": 6,
+			"h": 7,
+			"i": 8,
+			"j": 9,
+		},
+	}
+
+	expected := newSet(2, 4, 6, 8, 10, 12, 14, 16, 18)
+
+	flatMap := FlatMap(m, func(value int) int {
+		return value * 2
+	})
+
+	for value := range flatMap {
+		if !expected.contains(value) {
+			t.Errorf("expected values did not contain the value: %d", value)
+		}
+
+		expected.remove(value)
+	}
+}

+ 66 - 1
core/pkg/util/mathutil/mathutil.go

@@ -1,6 +1,30 @@
 package mathutil
 
-import "math"
+import (
+	"math"
+	"sync"
+)
+
+// intSearch is a mechanism for searching integers in a specific direction
+// for a given distance.
+type intSearch struct {
+	value     int
+	distance  int
+	increment int
+}
+
+func newIntSearch(value int, increment int) *intSearch {
+	return &intSearch{
+		value:     value,
+		distance:  0,
+		increment: increment,
+	}
+}
+
+func (i *intSearch) advance() {
+	i.value += i.increment
+	i.distance++
+}
 
 func Approximately(exp, act float64) bool {
 	return ApproximatelyPct(exp, act, 0.0001) // within 0.1%
@@ -13,3 +37,44 @@ func ApproximatelyPct(exp, act, pct float64) bool {
 	}
 	return math.Abs(exp-act) < delta
 }
+
+// FindClosestDivisor finds the closest divisor into the `into` value starting with the `current` value.
+// It runs concurrent searches in both directions and returns the value that travels the least distance.
+// If the distances are equivalent, it returns the greater value.
+func FindClosestDivisor(current int, into int) int {
+	if isDivisibleBy(current, into) {
+		return current
+	}
+
+	// we run forward and backwards searches
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	// find just advances until it finds a number that can divide cleanly into
+	// the target int
+	find := func(res *intSearch) {
+		defer wg.Done()
+
+		for !isDivisibleBy(res.value, into) {
+			res.advance()
+		}
+	}
+
+	rev := newIntSearch(current, -1)
+	fwd := newIntSearch(current, 1)
+
+	go find(rev)
+	go find(fwd)
+
+	wg.Wait()
+
+	if rev.distance < fwd.distance {
+		return rev.value
+	}
+	return fwd.value
+}
+
+// is b divisible by a
+func isDivisibleBy(a, b int) bool {
+	return (a == 0 || b == 0) || (b%a == 0)
+}

+ 48 - 0
core/pkg/util/mathutil/mathutil_test.go

@@ -0,0 +1,48 @@
+package mathutil
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestClosestDivision(t *testing.T) {
+	tests := []struct {
+		current  int
+		into     int
+		expected int
+	}{
+		{0, 60, 0},
+		{1, 60, 1},
+		{2, 60, 2},
+		{8, 60, 10},
+		{7, 60, 6},
+		{11, 60, 12},
+		{41, 60, 30},
+		{42, 60, 30},
+		{43, 60, 30},
+		{44, 60, 30},
+		{45, 60, 60},
+		{46, 60, 60},
+		{47, 60, 60},
+		{48, 60, 60},
+		{49, 60, 60},
+		{50, 60, 60},
+	}
+
+	for _, test := range tests {
+		t.Run(fmt.Sprintf("TestClosestDivision[%d_%d]", test.current, test.into), func(t *testing.T) {
+			result := FindClosestDivisor(test.current, test.into)
+			if result != test.expected {
+				t.Errorf("Expected %d, got %d", test.expected, result)
+			}
+		})
+	}
+}
+
+func BenchmarkClosestDivision(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		for current := 0; current <= 60; current++ {
+			FindClosestDivisor(current, 60)
+		}
+	}
+}

+ 22 - 0
core/pkg/util/promutil/promutil_test.go

@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"reflect"
 	"testing"
+
+	"github.com/opencost/opencost/core/pkg/util/json"
 )
 
 func checkSlice(s1, s2 []string) error {
@@ -205,3 +207,23 @@ func TestSanitizeLabels(t *testing.T) {
 		})
 	}
 }
+
+func TestClusterInfoLabels(t *testing.T) {
+	expected := map[string]bool{"clusterprofile": true, "errorreporting": true, "id": true, "logcollection": true, "name": true, "productanalytics": true, "provider": true, "provisioner": true, "remotereadenabled": true, "thanosenabled": true, "valuesreporting": true, "version": true}
+	clusterInfo := `{"clusterProfile":"production","errorReporting":"true","id":"cluster-one","logCollection":"true","name":"bolt-3","productAnalytics":"true","provider":"GCP","provisioner":"GKE","remoteReadEnabled":"false","thanosEnabled":"false","valuesReporting":"true","version":"1.14+"}`
+
+	var m map[string]any
+	err := json.Unmarshal([]byte(clusterInfo), &m)
+	if err != nil {
+		t.Errorf("Error: %s", err)
+		return
+	}
+
+	labels := MapToLabels(m)
+	for k := range expected {
+		if _, ok := labels[k]; !ok {
+			t.Errorf("Failed to locate key: \"%s\" in labels.", k)
+			return
+		}
+	}
+}

+ 34 - 0
core/pkg/util/sliceutil/sliceutil.go

@@ -1,5 +1,10 @@
 package sliceutil
 
+import (
+	"iter"
+	"slices"
+)
+
 // Map accepts a slice of T and applies a transformation function to each index of a
 // slice, which are inserted into a new slice of type U.
 func Map[T any, U any](s []T, transform func(T) U) []U {
@@ -9,3 +14,32 @@ func Map[T any, U any](s []T, transform func(T) U) []U {
 	}
 	return result
 }
+
+// AsSeq converts a slice of T into an iterator sequence only yielding the values. This should be used
+// to convert a slice into an iterator sequence for APIs that accept iterators only.
+func AsSeq[T any](s []T) iter.Seq[T] {
+	return func(yield func(T) bool) {
+		for _, v := range s {
+			if !yield(v) {
+				return
+			}
+		}
+	}
+}
+
+// AsSeq2 converts a slice of T into an iterator sequence yielding the index and value. This should be used
+// to convert a slice into an iterator sequence for APIs that accept iterators only.
+func AsSeq2[T any](s []T) iter.Seq2[int, T] {
+	return func(yield func(int, T) bool) {
+		for i, v := range s {
+			if !yield(i, v) {
+				return
+			}
+		}
+	}
+}
+
+// SeqToSlice converts an iterator sequence into a slice of T.
+func SeqToSlice[T any](s iter.Seq[T]) []T {
+	return slices.Collect(s)
+}

+ 158 - 0
core/pkg/util/sliceutil/sliceutil_test.go

@@ -0,0 +1,158 @@
+package sliceutil
+
+import (
+	"maps"
+	"slices"
+	"testing"
+)
+
+func TestSliceMap(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []int
+		expected []int
+	}{
+		{
+			name:     "empty slice",
+			input:    []int{},
+			expected: []int{},
+		},
+		{
+			name:     "single element",
+			input:    []int{1},
+			expected: []int{2},
+		},
+		{
+			name:     "multiple elements",
+			input:    []int{1, 2, 3},
+			expected: []int{2, 4, 6},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := Map(test.input, func(i int) int { return i * 2 })
+			for i, v := range result {
+				if v != test.expected[i] {
+					t.Errorf("expected %v, got %v", test.expected[i], v)
+				}
+			}
+		})
+	}
+}
+
+type seqTestCase[T comparable] struct {
+	name  string
+	input []T
+}
+
+func runSeqTest[T comparable](test seqTestCase[T]) func(*testing.T) {
+	return func(t *testing.T) {
+		result := AsSeq(test.input)
+
+		i := 0
+		for v := range result {
+			if v != test.input[i] {
+				t.Errorf("expected %v, got %v", test.input[i], v)
+			}
+			i++
+		}
+	}
+}
+
+func runSeqTests[T comparable](t *testing.T, testCases []seqTestCase[T]) {
+	t.Helper()
+
+	for _, test := range testCases {
+		t.Run(test.name, runSeqTest(test))
+	}
+}
+
+func TestToSeq(t *testing.T) {
+	intTests := []seqTestCase[int]{
+		{
+			name:  "int empty slice",
+			input: []int{},
+		},
+		{
+			name:  "int single element",
+			input: []int{1},
+		},
+		{
+			name:  "int multiple elements",
+			input: []int{1, 2, 3},
+		},
+	}
+
+	floatTests := []seqTestCase[float64]{
+		{
+			name:  "float64 empty slice",
+			input: []float64{},
+		},
+		{
+			name:  "float64 single element",
+			input: []float64{1.54},
+		},
+		{
+			name:  "float64 multiple elements",
+			input: []float64{52.32, 23.12, 54.123},
+		},
+	}
+
+	stringTests := []seqTestCase[string]{
+		{
+			name:  "string empty slice",
+			input: []string{},
+		},
+		{
+			name:  "single single element",
+			input: []string{"foo"},
+		},
+		{
+			name:  "string multiple elements",
+			input: []string{"foo", "bar", "baz"},
+		},
+	}
+
+	runSeqTests(t, intTests)
+	runSeqTests(t, floatTests)
+	runSeqTests(t, stringTests)
+}
+
+func TestSeqToSlice(t *testing.T) {
+	keys := []string{
+		"a", "b", "c", "d", "e", "f", "g",
+	}
+	m := make(map[string]string, len(keys))
+	for _, k := range keys {
+		m[k] = "value-" + k
+	}
+
+	seqKeys := maps.Keys(m)
+	seqValues := maps.Values(m)
+
+	// These do *NOT* align on indexes!
+	keySlice := SeqToSlice(seqKeys)
+	valueSlice := SeqToSlice(seqValues)
+
+	for _, k := range keySlice {
+		if !slices.Contains(keys, k) {
+			t.Errorf("expected %v to be in %v", k, keys)
+		}
+	}
+
+	for _, v := range valueSlice {
+		if !mapContainsValue(m, v) {
+			t.Errorf("expected %v to be in %v", v, m)
+		}
+	}
+}
+
+func mapContainsValue(m map[string]string, value string) bool {
+	for _, v := range m {
+		if v == value {
+			return true
+		}
+	}
+	return false
+}

+ 9 - 0
core/pkg/util/stringutil/stringutil.go

@@ -103,6 +103,15 @@ func RandSeq(n int) string {
 	return string(b)
 }
 
+// RandSeq generates a pseudo-random alphabetic string of the given length
+func RandSeqWith(r *rand.Rand, n int) string {
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = alpha[r.Intn(len(alpha))] // #nosec No need for a cryptographic strength random here
+	}
+	return string(b)
+}
+
 // FormatBytes takes a number of bytes and formats it as a string
 func FormatBytes(numBytes int64) string {
 	if numBytes > TiB {

+ 64 - 53
core/pkg/util/stringutil/stringutil_test.go

@@ -1,16 +1,37 @@
-package stringutil
+package stringutil_test
 
 import (
 	"fmt"
 	"math/rand"
-	"runtime"
-	"runtime/debug"
+	"strings"
 	"sync"
 	"testing"
+
+	"github.com/opencost/opencost/core/pkg/util/stringutil"
 )
 
 var oldBank sync.Map
 
+type bankTest struct {
+	Bank     func(string) string
+	BankFunc func(string, func() string) string
+	Clear    func()
+}
+
+var (
+	legacyTest = bankTest{
+		Bank:     BankLegacy,
+		BankFunc: func(s string, f func() string) string { return s },
+		Clear:    ClearBankLegacy,
+	}
+
+	standardBankTest = bankTest{
+		Bank:     stringutil.Bank,
+		BankFunc: stringutil.BankFunc,
+		Clear:    stringutil.ClearBank,
+	}
+)
+
 // This is the old implementation of the string bank to use for comparison benchmarks
 func BankLegacy(s string) string {
 	ss, _ := oldBank.LoadOrStore(s, s)
@@ -27,118 +48,108 @@ func copyString(s string) string {
 
 func generateBenchData(totalStrings, totalUnique int) []string {
 	randStrings := make([]string, 0, totalStrings)
+	r := rand.New(rand.NewSource(27644437))
 
 	// create totalUnique unique strings
-	for i := 0; i < totalUnique; i++ {
-		randStrings = append(randStrings, fmt.Sprintf("%s/%s/%s", RandSeq(10), RandSeq(10), RandSeq(10)))
+	for range totalUnique {
+		randStrings = append(
+			randStrings,
+			fmt.Sprintf("%s/%s/%s", stringutil.RandSeqWith(r, 10), stringutil.RandSeqWith(r, 10), stringutil.RandSeqWith(r, 10)),
+		)
 	}
 
 	// set the seed such that the resulting "remainder" strings are deterministic for each bench
-	rand.Seed(1523942)
+	r = rand.New(rand.NewSource(1523942))
 
 	// append a random selection from 0-totalUnique to the list.
-	for i := 0; i < totalStrings-totalUnique; i++ {
-		randStrings = append(randStrings, copyString(randStrings[rand.Intn(totalUnique)]))
+	for range totalStrings - totalUnique {
+		randStrings = append(randStrings, strings.Clone(randStrings[r.Intn(totalUnique)]))
 	}
 
 	// shuffle the list of strings
-	rand.Shuffle(totalStrings, func(i, j int) { randStrings[i], randStrings[j] = randStrings[j], randStrings[i] })
+	r.Shuffle(totalStrings, func(i, j int) { randStrings[i], randStrings[j] = randStrings[j], randStrings[i] })
 
 	return randStrings
 }
 
-func benchmarkLegacyStringBank(b *testing.B, totalStrings, totalUnique int) {
+func benchmarkStringBank(b *testing.B, bt bankTest, totalStrings, totalUnique int, useBankFunc bool) {
 	b.StopTimer()
 	randStrings := generateBenchData(totalStrings, totalUnique)
 
-	for i := 0; i < b.N; i++ {
-		b.StartTimer()
-		for b := 0; b < totalStrings; b++ {
-			BankLegacy(randStrings[b])
-		}
-		b.StopTimer()
-		ClearBankLegacy()
-		runtime.GC()
-		debug.FreeOSMemory()
-	}
-}
-
-func benchmarkStringBank(b *testing.B, totalStrings, totalUnique int, useBankFunc bool) {
-	b.StopTimer()
-	randStrings := generateBenchData(totalStrings, totalUnique)
-
-	for i := 0; i < b.N; i++ {
-		b.StartTimer()
-		for b := 0; b < totalStrings; b++ {
-			if useBankFunc {
-				BankFunc(randStrings[b], func() string { return randStrings[b] })
-			} else {
-				Bank(randStrings[b])
+	b.Run(b.Name(), func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			b.StartTimer()
+			for bb := 0; bb < totalStrings; bb++ {
+				if useBankFunc {
+					bt.BankFunc(randStrings[bb], func() string { return randStrings[bb] })
+				} else {
+					bt.Bank(randStrings[bb])
+				}
 			}
+			b.StopTimer()
+			bt.Clear()
+			//runtime.GC()
+			//debug.FreeOSMemory()
 		}
-		b.StopTimer()
-		ClearBank()
-		runtime.GC()
-		debug.FreeOSMemory()
-	}
+	})
 }
 
 func BenchmarkLegacyStringBank90PercentDuplicate(b *testing.B) {
-	benchmarkLegacyStringBank(b, 1_000_000, 100_000)
+	benchmarkStringBank(b, legacyTest, 1_000_000, 100_000, false)
 }
 
 func BenchmarkLegacyStringBank75PercentDuplicate(b *testing.B) {
-	benchmarkLegacyStringBank(b, 1_000_000, 250_000)
+	benchmarkStringBank(b, legacyTest, 1_000_000, 250_000, false)
 }
 
 func BenchmarkLegacyStringBank50PercentDuplicate(b *testing.B) {
-	benchmarkLegacyStringBank(b, 1_000_000, 100_000)
+	benchmarkStringBank(b, legacyTest, 1_000_000, 100_000, false)
 }
 
 func BenchmarkLegacyStringBank25PercentDuplicate(b *testing.B) {
-	benchmarkLegacyStringBank(b, 1_000_000, 750_000)
+	benchmarkStringBank(b, legacyTest, 1_000_000, 750_000, false)
 }
 
 func BenchmarkLegacyStringBankNoDuplicate(b *testing.B) {
-	benchmarkLegacyStringBank(b, 1_000_000, 1_000_000)
+	benchmarkStringBank(b, legacyTest, 1_000_000, 1_000_000, false)
 }
 
 func BenchmarkStringBank90PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 100_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 100_000, false)
 }
 
 func BenchmarkStringBank75PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 250_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 250_000, false)
 }
 
 func BenchmarkStringBank50PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 100_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 100_000, false)
 }
 
 func BenchmarkStringBank25PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 750_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 750_000, false)
 }
 
 func BenchmarkStringBankNoDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 1_000_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 1_000_000, false)
 }
 
 func BenchmarkStringBankFunc90PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 100_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 100_000, true)
 }
 
 func BenchmarkStringBankFunc75PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 250_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 250_000, true)
 }
 
 func BenchmarkStringBankFunc50PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 100_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 100_000, true)
 }
 
 func BenchmarkStringBankFunc25PercentDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 750_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 750_000, true)
 }
 
 func BenchmarkStringBankFuncNoDuplicate(b *testing.B) {
-	benchmarkStringBank(b, 1_000_000, 1_000_000, false)
+	benchmarkStringBank(b, standardBankTest, 1_000_000, 1_000_000, true)
 }

+ 9 - 1
core/pkg/util/timeutil/timeutil.go

@@ -120,11 +120,19 @@ func ParseUTCOffset(offsetStr string) (time.Duration, error) {
 // FormatStoreResolution provides a clean notation for ETL store resolutions.
 // e.g. daily => 1d; hourly => 1h
 func FormatStoreResolution(dur time.Duration) string {
+	if dur >= (7 * 24 * time.Hour) {
+		return fmt.Sprintf("%dw", int(dur.Hours()/(24.0*7.0)))
+	}
 	if dur >= 24*time.Hour {
 		return fmt.Sprintf("%dd", int(dur.Hours()/24.0))
-	} else if dur >= time.Hour {
+	}
+	if dur >= time.Hour {
 		return fmt.Sprintf("%dh", int(dur.Hours()))
 	}
+	if dur >= 10*time.Minute {
+		return fmt.Sprintf("%dm", int(dur.Minutes()))
+	}
+
 	return fmt.Sprint(dur)
 }
 

+ 17 - 2
core/pkg/util/worker/worker.go

@@ -2,11 +2,13 @@ package worker
 
 import (
 	"fmt"
+	"iter"
 	"runtime"
 	"sync"
 	"sync/atomic"
 
 	"github.com/opencost/opencost/core/pkg/collections"
+	"github.com/opencost/opencost/core/pkg/util/sliceutil"
 )
 
 // Runner is a function type that takes a single input and returns nothing.
@@ -317,6 +319,12 @@ func ConcurrentCollect[T any, U any](workerFunc Worker[T, *U], inputs []T) []*U
 // ConcurrentCollectWith runs a pool of workers of the specified size which concurrently call the provided worker
 // func on each input to get a result slice of non-nil outputs. Size inputs < 1 will automatically be set to 1.
 func ConcurrentCollectWith[T any, U any](size int, workerFunc Worker[T, *U], inputs []T) []*U {
+	return ConcurrentIterCollect(size, workerFunc, sliceutil.AsSeq(inputs))
+}
+
+// ConcurrentIterCollect runs a pool of workers of the specified size which concurrently call the provided worker
+// func on each input to get a result slice of non-nil outputs. Size inputs < 1 will automatically be set to 1.
+func ConcurrentIterCollect[T any, U any](size int, workerFunc Worker[T, *U], inputs iter.Seq[T]) []*U {
 	if size < 1 {
 		size = 1
 	}
@@ -325,7 +333,7 @@ func ConcurrentCollectWith[T any, U any](size int, workerFunc Worker[T, *U], inp
 	defer workerPool.Shutdown()
 
 	workGroup := NewCollectionGroup(workerPool)
-	for _, input := range inputs {
+	for input := range inputs {
 		workGroup.Push(input)
 	}
 
@@ -342,6 +350,12 @@ func ConcurrentRun[T any](runner Runner[T], inputs []T) {
 // ConcurrentRunWith runs a pool of runners of the specified size which concurrently call the provided runner
 // func on each input. Size inputs < 1 will automatically be set to 1.
 func ConcurrentRunWith[T any](size int, runner Runner[T], inputs []T) {
+	ConcurrentIterRunWith(size, runner, sliceutil.AsSeq(inputs))
+}
+
+// ConcurrentIterRunWith runs a pool of runners of the specified size which concurrently call the provided runner
+// func on each input. Size inputs < 1 will automatically be set to 1.
+func ConcurrentIterRunWith[T any](size int, runner Runner[T], inputs iter.Seq[T]) {
 	if size < 1 {
 		size = 1
 	}
@@ -350,9 +364,10 @@ func ConcurrentRunWith[T any](size int, runner Runner[T], inputs []T) {
 		runner(input)
 		return
 	})
+	defer workerPool.Shutdown()
 
 	workGroup := NewNoResultGroup(workerPool)
-	for _, input := range inputs {
+	for input := range inputs {
 		workGroup.Push(input)
 	}
 

二進制
docs/image-1.png


+ 42 - 0
docs/modular-opencost.md

@@ -0,0 +1,42 @@
+# Modular OpenCost 
+
+This document proposes major architectural restructuring of the OpenCost project to include a more modular approach to collecting and emitting metrics, associating cost data, and providing a more extensible platform for development.
+
+
+## Purpose 
+
+OpenCost has concrete dependencies on Prometheus and PromQL for metric collection and querying. In addition to Prometheus, OpenCost also has dependencies on specific metric emitters, recording rules, metric relabeling,  the Kubernetes API, and provider specific cost data sources. These dependencies are tightly coupled in the core implementation with a bootstrapping process and HTTP router that are not easily extensible. This hinders the ability to extend or modify existing functionality, makes the code less approachable to contributors, makes optimization efforts difficult, and riddles the codebase with nuance and complexity. 
+
+While solving all of these problems at once is not feasible, this proposal aims to layout the basic design and structure of a `DataSource` contract as a replacement for Prometheus, such that contributors can use to capture the goals and direction of the project more easily. 
+
+## Components 
+
+There are three driving data components of the OpenCost project: 
+* Metric Collection and Queries: Prometheus is used to scrape specific emitters and expose a set of metrics that OpenCost requires to calculate cost. 
+* Cloud Provider: More simply, a Provider is an abstraction that provides specific cost data for a given resource. This data is used to calculate the cost of a given resource. 
+* Kubernetes API: The Kubernetes API is used as the glue between the raw metric data queried from Prometheus and the cost data gathered from the Provider. 
+
+The interactions between these components are what drive the cost calculation process. However, the current implementation of OpenCost does not provide a clear separation of concerns between these components. Nor does it allow for substitution of these components without significant refactoring.
+
+![](image-1.png)
+
+**This image represents the current architecture of OpenCost. The Prometheus data source is tightly coupled with the model generation and metric emission process. This makes it difficult to substitute Prometheus with other metric sources, including a low retention metric source to allow for better unit testing.**
+
+### Prometheus Data Source 
+
+In the above diagram, the separation of concerns doesn't seem as impactful as previous descriptions. However, there are base contracts in place for both the Provider and Kubernetes APIs. However, the Prometheus implementation reaches all of the components from metric emission, through model generation, and finally exposed via the HTTP router _directly_. Not only does this require that OpenCost be run with a Prometheus instance, but it also requires that the Prometheus instance be configured in a specific way. 
+
+More specifically, Prometheus is tightly coupled with model generation and metric emission. This restricts the ability to interface with other metric sources and hinders the expansion of new data models. 
+
+The proposed `DataSource` contract would provide a more abstract interface for metric collection and querying. This would allow for the substitution of Prometheus with other metric sources, such as InfluxDB, or even an in-memory, low retention metric source for smaller footprint and/or testing. 
+
+This also allows OpenCost to maintain the current data model and metric emission process, while allowing for the expansion of new data models and metric emitters. 
+
+
+## Structure
+
+Abstracting Prometheus provides a separation between core OpenCost functionality and specific implementation collecting the data required for cost calculation. It also clears up much of the nuance and complexity surrounding the Prometheus implementation that would otherwise prevent modifications to the core functionality. 
+
+Structurally, each data source could be maintained within it's own go module inside of the opencost git repository. Those leveraging the OpenCost project as a dependency could more specifically select the modules required rather than inherit the entire kitchen sink. 
+
+

+ 80 - 75
go.mod

@@ -3,6 +3,7 @@ module github.com/opencost/opencost
 replace (
 	github.com/golang/lint => golang.org/x/lint v0.0.0-20180702182130-06c8688daad7
 	github.com/opencost/opencost/core => ./core
+	github.com/opencost/opencost/modules/prometheus-source => ./modules/prometheus-source
 )
 
 require (
@@ -10,64 +11,71 @@ require (
 	cloud.google.com/go/compute/metadata v0.6.0
 	cloud.google.com/go/storage v1.42.0
 	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
-	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
-	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
 	github.com/Azure/go-autorest/autorest v0.11.28
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
 	github.com/aliyun/alibaba-cloud-sdk-go v1.62.3
 	github.com/aws/aws-sdk-go v1.50.8
-	github.com/aws/aws-sdk-go-v2 v1.25.1
-	github.com/aws/aws-sdk-go-v2/config v1.27.3
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.3
+	github.com/aws/aws-sdk-go-v2 v1.36.3
+	github.com/aws/aws-sdk-go-v2/config v1.29.10
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.63
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5
 	github.com/aws/aws-sdk-go-v2/service/athena v1.40.0
 	github.com/aws/aws-sdk-go-v2/service/ec2 v1.149.0
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0
-	github.com/aws/aws-sdk-go-v2/service/sts v1.28.0
-	github.com/aws/smithy-go v1.20.1
-	github.com/davecgh/go-spew v1.1.1
-	github.com/getsentry/sentry-go v0.25.0
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17
+	github.com/aws/smithy-go v1.22.2
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 	github.com/google/martian v2.1.0+incompatible
 	github.com/google/uuid v1.6.0
 	github.com/hashicorp/go-hclog v1.6.2
 	github.com/hashicorp/go-plugin v1.6.0
 	github.com/jszwec/csvutil v1.2.1
 	github.com/julienschmidt/httprouter v1.3.0
-	github.com/kubecost/events v0.0.6
-	github.com/lib/pq v1.2.0
+	github.com/kubecost/events v0.0.8
 	github.com/microcosm-cc/bluemonday v1.0.23
-	github.com/minio/minio-go/v7 v7.0.72
-	github.com/opencost/opencost/core v0.0.0-00010101000000-000000000000
+	github.com/opencost/opencost/core v0.0.0-20241211165149-ee44b80e2fd0
+	github.com/opencost/opencost/modules/prometheus-source v0.0.0-00010101000000-000000000000
 	github.com/patrickmn/go-cache v2.1.0+incompatible
-	github.com/pkg/errors v0.9.1
-	github.com/prometheus/client_golang v1.17.0
-	github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16
-	github.com/prometheus/common v0.45.0
+	github.com/prometheus/client_golang v1.22.0
+	github.com/prometheus/client_model v0.6.1
 	github.com/rs/cors v1.8.2
 	github.com/rs/zerolog v1.26.1
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9
-	github.com/spf13/cobra v1.2.1
+	github.com/spf13/cobra v1.8.1
 	github.com/spf13/viper v1.8.1
-	github.com/stretchr/testify v1.9.0
-	go.etcd.io/bbolt v1.3.5
-	go.opentelemetry.io/otel v1.24.0
+	github.com/stretchr/testify v1.10.0
+	go.opentelemetry.io/otel v1.33.0
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d
-	golang.org/x/oauth2 v0.21.0
-	golang.org/x/sync v0.10.0
-	golang.org/x/text v0.21.0
+	golang.org/x/oauth2 v0.27.0
+	golang.org/x/sync v0.12.0
+	golang.org/x/text v0.23.0
 	google.golang.org/api v0.183.0
-	google.golang.org/protobuf v1.34.1
-	gopkg.in/yaml.v2 v2.4.0
-	k8s.io/api v0.30.2
-	k8s.io/apimachinery v0.30.2
-	k8s.io/client-go v0.30.2
-	sigs.k8s.io/yaml v1.3.0
+	google.golang.org/protobuf v1.36.5
+	k8s.io/api v0.33.1
+	k8s.io/apimachinery v0.33.1
+	k8s.io/client-go v0.33.1
 )
 
 require (
+	github.com/Masterminds/semver/v3 v3.3.1 // indirect
+	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
 	github.com/gofrs/flock v0.8.1 // indirect
+	github.com/minio/crc64nvme v1.0.1 // indirect
+	github.com/minio/minio-go/v7 v7.0.88 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
 	github.com/sony/gobreaker v0.5.0 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	k8s.io/kubelet v0.33.1 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
 )
 
 require (
@@ -75,7 +83,7 @@ require (
 	cloud.google.com/go/auth v0.5.1 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
 	cloud.google.com/go/iam v1.1.8 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
@@ -84,35 +92,35 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
-	github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
 	github.com/apache/arrow/go/v15 v15.0.2 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 	github.com/fatih/color v1.16.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
-	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
-	github.com/go-openapi/jsonpointer v0.19.6 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
-	github.com/go-openapi/swag v0.22.3 // indirect
-	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gofrs/uuid v4.2.0+incompatible // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
@@ -120,9 +128,8 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/flatbuffers v23.5.26+incompatible // indirect
-	github.com/google/gnostic-models v0.6.8 // indirect
-	github.com/google/go-cmp v0.6.0 // indirect
-	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/gnostic-models v0.6.9 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.4 // indirect
@@ -131,19 +138,17 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
-	github.com/imdario/mergo v0.3.12 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/klauspost/compress v1.17.6 // indirect
-	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+	github.com/klauspost/compress v1.18.0 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
@@ -157,9 +162,9 @@ require (
 	github.com/pelletier/go-toml v1.9.3 // indirect
 	github.com/pierrec/lz4/v4 v4.1.18 // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/procfs v0.11.1 // indirect
-	github.com/rs/xid v1.5.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/rs/xid v1.6.0 // indirect
 	github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
 	github.com/spf13/afero v1.6.0 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
@@ -170,29 +175,29 @@ require (
 	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
-	go.opentelemetry.io/otel/metric v1.24.0 // indirect
-	go.opentelemetry.io/otel/trace v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.33.0 // indirect
+	go.opentelemetry.io/otel/trace v1.33.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
-	golang.org/x/crypto v0.31.0 // indirect
-	golang.org/x/mod v0.17.0 // indirect
-	golang.org/x/net v0.33.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
-	golang.org/x/time v0.5.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/mod v0.21.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/term v0.30.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
+	golang.org/x/tools v0.26.0 // indirect
 	golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
 	google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
-	google.golang.org/grpc v1.64.1 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+	google.golang.org/grpc v1.68.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/klog/v2 v2.120.1 // indirect
-	k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
-	k8s.io/utils v0.0.0-20230726121419-3b25d923346b
-	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
+	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
 )
 
-go 1.23.8
+go 1.24.2

+ 175 - 159
go.sum

@@ -57,16 +57,18 @@ cloud.google.com/go/storage v1.42.0/go.mod h1:HjMXRFq65pGKFn6hxj6x3HCyR41uSB72Z0
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
-github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
-github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0 h1:IfFdxTUDiV58iZqPKgyWiz4X4fCxZeQ1pTQPImLYXpY=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0/go.mod h1:SUZc9YRRHfx2+FAQKNDGrssXehqLpxmwRv2mC/5ntj4=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
 github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
 github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
@@ -92,10 +94,14 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
+github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.3 h1:kWY5c/9JOhSYBogi3mtNG7G9TxXS0CddtQ6RKOI3mvY=
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.3/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -106,48 +112,48 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4=
 github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
-github.com/aws/aws-sdk-go-v2 v1.25.1 h1:P7hU6A5qEdmajGwvae/zDkOq+ULLC9tQBTwqqiwFGpI=
-github.com/aws/aws-sdk-go-v2 v1.25.1/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo=
-github.com/aws/aws-sdk-go-v2/config v1.27.3 h1:0PRdb/q5a77HVYj+2rvPiCObfMfl/pWhwa5cs3cnl3c=
-github.com/aws/aws-sdk-go-v2/config v1.27.3/go.mod h1:WeRAr9ENap9NAegbfNsLqGQd8ERz5ypdIUx4j0/ZgKI=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.3 h1:dDM5wrgwOL5gTZ0Gv/bvewPldjBcJywoaO5ClERrOGE=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.3/go.mod h1:G96Nuaw9qJS+s3OnK8RW8VEKEOjXi8H5Jk4lC/ZyZbw=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 h1:lk1ZZFbdb24qpOwVC1AwYNrswUjAxeyey6kFBVANudQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1/go.mod h1:/xJ6x1NehNGCX4tvGzzj2bq5TBOT/Yxq+qbL9Jpx2Vk=
+github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc=
+github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5 h1:IEv6homMJMnedG/2VWfNuV34ouXUmK8E7y4rAl59Fhs=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5/go.mod h1:a+wq9mSuG13iSkVMR1O8VApmAISm1ca+E2RQpcB3flw=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 h1:evvi7FbTAoFxdP/mixmP7LIYzQWAmzBcwNB/es9XPNc=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1/go.mod h1:rH61DT6FDdikhPghymripNUCsf+uVF4Cnk4c4DBKH64=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 h1:RAnaIrbxPtlXNVI/OIlh1sidTQ3e1qM6LRjs7N0bE0I=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1/go.mod h1:nbgAGkH5lk0RZRMh6A4K/oG6Xj11eC/1CyDow+DUAFI=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 h1:rtYJd3w6IWCTVS8vmMaiXjW198noh2PBm5CiXyJea9o=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1/go.mod h1:zvXu+CTlib30LUy4LTNFc6HTZ/K6zCae5YIHTdX9wIo=
 github.com/aws/aws-sdk-go-v2/service/athena v1.40.0 h1:7XANtaAHYX8uD3ZqDcrHFYiwGOz21qTg8U1jhk9aO/A=
 github.com/aws/aws-sdk-go-v2/service/athena v1.40.0/go.mod h1:6uStyL/E8L2h4wrSXZzFf/8lmrmRRFmbJemH59UX0RM=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.149.0 h1:uMw4dz7s741WxewdyxOV7n8Rgajf6Azy+tx0VoJRm6k=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.149.0/go.mod h1:7MUTgVVnC1GAxx4SNQqzQalrm1n4v1HYa/R/LEB3CKo=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 h1:5Wxh862HkXL9CbQ83BIkWKLIgQapGeuh5zG2G9OZtQk=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1/go.mod h1:V7GLA01pNUxMCYSQsibdVrqUrNIYIT/9lCOyR8ExNvQ=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 h1:cVP8mng1RjDyI3JN/AXFCn5FHNlsBaBH0/MBtG1bg0o=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1/go.mod h1:C8sQjoyAsdfjC7hpy4+S6B92hnFzx0d0UAyHicaOTIE=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 h1:OYmmIcyw19f7x0qLBLQ3XsrCZSSyLhxd9GXng5evsN4=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1/go.mod h1:s5rqdn74Vdg10k61Pwf4ZHEApOSD6CKRe6qpeHDq32I=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0 h1:rNVsCe3bqTAhG+qjnHJKgYKdHEsqqo/GMK3gEYY8W6g=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 h1:6YL8G91QZ52KlPrLkEgEez5kejIVwChVCgND3qgY5j0=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.0/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 h1:+DqIa5Ll7W311QLUvGFDdVit9uC4G0VioDdw08cXcow=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 h1:F7tQr61zYnTaeY50Rn4jwfVQbtcqJuBRwN/nGGNwzb0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.0/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4=
-github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
-github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -157,8 +163,8 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
 github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
 github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -168,11 +174,14 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
 github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
@@ -197,29 +206,31 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
-github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
-github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
 github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
 github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
 github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
-github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -271,8 +282,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
 github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
-github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
-github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
+github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -285,11 +296,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -307,8 +316,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
 github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@@ -357,10 +366,8 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
 github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
 github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -383,13 +390,15 @@ github.com/jszwec/csvutil v1.2.1/go.mod h1:8YHz6C3KVdIeCxLMvwbbIVDCTA/Wi2df93AZl
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
-github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -399,12 +408,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/kubecost/events v0.0.6 h1:ql1ZUnLfheD2hHm/otsHZ8BOYt87rY5e9sPFHges4ec=
-github.com/kubecost/events v0.0.6/go.mod h1:i3DyCVatehxq6tAbvBrARuafjkX2DECPk9OWxiaRIhY=
+github.com/kubecost/events v0.0.8 h1:FEglMSOGkjiSZT2FnSYM99s2M4DMiBOgHVheM7Vnurs=
+github.com/kubecost/events v0.0.8/go.mod h1:PXnE7CSZs3OulOLcB8baQENploBp4NM7ERZVBCqNi4A=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -420,15 +427,15 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
 github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
 github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
+github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.72 h1:ZSbxs2BfJensLyHdVOgHv+pfmvxYraaUy07ER04dWnA=
-github.com/minio/minio-go/v7 v7.0.72/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
+github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs=
+github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -456,10 +463,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
 github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
-github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
-github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
-github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE=
-github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 github.com/oracle/oci-go-sdk/v65 v65.71.0 h1:eEnFD/CzcoqdAA0xu+EmK32kJL3jfV0oLYNWVzoKNyo=
@@ -471,45 +478,45 @@ github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5d
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
 github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
-github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
-github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
-github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
-github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
-github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
-github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
-github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
+github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
 github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
-github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
 github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
@@ -518,8 +525,8 @@ github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
-github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -542,14 +549,16 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
 github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
 github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
 github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -560,8 +569,6 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
 github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
 github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
 github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
-go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
-go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -574,22 +581,26 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
-go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
-go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
-go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
-go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
+go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
+go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
+go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
 go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
 go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
-go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
-go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
+go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
 go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -603,8 +614,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -642,8 +653,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -682,8 +693,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -696,8 +707,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
+golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -709,8 +720,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -762,14 +773,13 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -778,13 +788,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -837,8 +847,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -921,10 +931,10 @@ google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaE
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE=
 google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
-google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -945,8 +955,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
-google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
+google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -959,13 +969,15 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
-google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -975,7 +987,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -989,24 +1000,29 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI=
-k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI=
-k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg=
-k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
-k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50=
-k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs=
-k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
-k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
-k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
-k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
+k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
+k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
+k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
+k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
+k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
+k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
+k8s.io/kubelet v0.33.1 h1:x4LCw1/iZVWOKA4RoITnuB8gMHnw31HPB3S0EF0EexE=
+k8s.io/kubelet v0.33.1/go.mod h1:8WpdC9M95VmsqIdGSQrajXooTfT5otEj8pGWOm+KKfQ=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
-sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

+ 18 - 2
justfile

@@ -11,11 +11,27 @@ test-core:
     {{commonenv}} cd ./core && go test ./... -coverprofile=coverage.out
     {{commonenv}} cd ./core && go vet ./...
 
-# Run unit tests
-test: test-core
+# run prometheus-source unit tests 
+test-prometheus-source:
+    {{commonenv}} cd ./modules/prometheus-source && go test ./... -coverprofile=coverage.out
+    {{commonenv}} cd ./modules/prometheus-source && go vet ./...
+
+# run collector-source unit tests
+test-collector-source:
+    {{commonenv}} cd ./modules/collector-source && go test ./... -coverprofile=coverage.out
+    {{commonenv}} cd ./modules/collector-source && go vet ./...
+
+# run the opencost unit tests 
+test-opencost: 
     {{commonenv}} go test ./... -coverprofile=coverage.out
     {{commonenv}} go vet ./...
 
+# Run unit tests, merge coverage reports, remove old reports 
+test: test-core test-prometheus-source test-collector-source test-opencost
+    find . -name "coverage.out" -print0 | xargs -0 cat > coverage.new
+    find . -name "coverage.out" -delete
+    mv coverage.new coverage.out
+
 # Run unit tests and integration tests
 test-integration:
     {{commonenv}} INTEGRATION=true go test ./... -coverprofile=coverage.out

+ 3 - 0
modules/collector-source/README.md

@@ -0,0 +1,3 @@
+# OpenCost Data Sources - Collector
+
+The OpenCost Collector is a data source implementation which provides OpenCost with the metrics and metadata required to calculate cost allocation. The collector is responsible for gathering data from various sources, such as Kubernetes, cloud providers, and other external systems, and transforming it into a format that can be consumed by the OpenCost API.

+ 114 - 0
modules/collector-source/go.mod

@@ -0,0 +1,114 @@
+module github.com/opencost/opencost/modules/collector-source
+
+replace github.com/opencost/opencost/core => ./../../core
+
+go 1.24.2
+
+require (
+	github.com/julienschmidt/httprouter v1.3.0
+	github.com/opencost/opencost/core v0.0.0-20250521155634-81d2b597d1bc
+	golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
+	k8s.io/api v0.33.1
+	k8s.io/apimachinery v0.33.1
+	k8s.io/kubelet v0.33.1
+)
+
+require (
+	cloud.google.com/go v0.112.0 // indirect
+	cloud.google.com/go/compute/metadata v0.5.0 // indirect
+	cloud.google.com/go/iam v1.1.6 // indirect
+	cloud.google.com/go/storage v1.36.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.29.10 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.63 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
+	github.com/aws/smithy-go v1.22.2 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/s2a-go v0.1.7 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
+	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/minio/crc64nvme v1.0.1 // indirect
+	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/minio-go/v7 v7.0.88 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
+	github.com/pelletier/go-toml v1.9.3 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
+	github.com/rs/xid v1.6.0 // indirect
+	github.com/rs/zerolog v1.26.1 // indirect
+	github.com/spf13/afero v1.6.0 // indirect
+	github.com/spf13/cast v1.3.1 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/viper v1.8.1 // indirect
+	github.com/subosito/gotenv v1.2.0 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
+	go.opentelemetry.io/otel v1.33.0 // indirect
+	go.opentelemetry.io/otel/metric v1.33.0 // indirect
+	go.opentelemetry.io/otel/trace v1.33.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
+	golang.org/x/oauth2 v0.27.0 // indirect
+	golang.org/x/sync v0.13.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/text v0.23.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
+	google.golang.org/api v0.162.0 // indirect
+	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+	google.golang.org/grpc v1.68.1 // indirect
+	google.golang.org/protobuf v1.36.5 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
+)

+ 809 - 0
modules/collector-source/go.sum

@@ -0,0 +1,809 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
+cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
+cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
+cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc=
+github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
+github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
+github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs=
+github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+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=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
+github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
+github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
+github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
+github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
+go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
+go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
+go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
+go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
+go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
+golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
+golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps=
+google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
+google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
+google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
+k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
+k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
+k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kubelet v0.33.1 h1:x4LCw1/iZVWOKA4RoITnuB8gMHnw31HPB3S0EF0EexE=
+k8s.io/kubelet v0.33.1/go.mod h1:8WpdC9M95VmsqIdGSQrajXooTfT5otEj8pGWOm+KKfQ=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

+ 86 - 0
modules/collector-source/pkg/collector/clustermap.go

@@ -0,0 +1,86 @@
+package collector
+
+import (
+	"fmt"
+
+	"github.com/opencost/opencost/core/pkg/clusters"
+	"github.com/opencost/opencost/core/pkg/log"
+)
+
+type collectorClusterMap struct {
+	clusterInfo clusters.ClusterInfoProvider
+}
+
+func newCollectorClusterMap(clusterInfo clusters.ClusterInfoProvider) *collectorClusterMap {
+	return &collectorClusterMap{
+		clusterInfo: clusterInfo,
+	}
+}
+
+// getLocalClusterInfo returns the local cluster info in the event there does not exist a metric available.
+func (c *collectorClusterMap) getLocalClusterInfo() (*clusters.ClusterInfo, error) {
+	info := c.clusterInfo.GetClusterInfo()
+	clusterInfo, err := clusters.MapToClusterInfo(info)
+	if err != nil {
+		return nil, fmt.Errorf("parsing local cluster info failed: %w", err)
+	}
+
+	return clusterInfo, nil
+}
+
+func (c *collectorClusterMap) GetClusterIDs() []string {
+	info, err := c.getLocalClusterInfo()
+	if err != nil {
+		log.Errorf("%s", err.Error())
+		return nil
+	}
+	return []string{info.ID}
+}
+
+func (c *collectorClusterMap) AsMap() map[string]*clusters.ClusterInfo {
+	info, err := c.getLocalClusterInfo()
+	if err != nil {
+		log.Errorf("%s", err.Error())
+		return nil
+	}
+	return map[string]*clusters.ClusterInfo{
+		info.ID: info,
+	}
+}
+
+func (c *collectorClusterMap) InfoFor(clusterID string) *clusters.ClusterInfo {
+	info, err := c.getLocalClusterInfo()
+	if err != nil {
+		log.Errorf("%s", err.Error())
+		return nil
+	}
+
+	if info.ID == clusterID {
+		return info
+	}
+	return nil
+}
+
+func (c *collectorClusterMap) NameFor(clusterID string) string {
+	info, err := c.getLocalClusterInfo()
+	if err != nil {
+		log.Errorf("%s", err.Error())
+		return ""
+	}
+	if info.ID == clusterID {
+		return info.Name
+	}
+	return ""
+}
+
+func (c *collectorClusterMap) NameIDFor(clusterID string) string {
+	info, err := c.getLocalClusterInfo()
+	if err != nil {
+		log.Errorf("%s", err.Error())
+		return clusterID
+	}
+	if info.ID == clusterID {
+		return fmt.Sprintf("%s/%s", info.Name, clusterID)
+	}
+	return clusterID
+}

Some files were not shown because too many files changed in this diff