Przeglądaj źródła

Kubernetes cache optimization (#2736)

* delete cluster importer/exporter

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced pod and namespace structs

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced node struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced service struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced daemonset struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced deployment struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced statefulset struct

Signed-off-by: r2k1 <yokree@gmail.com>

* delete unused ReplicaSets ReplicationControllers watches

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced PersistentVolume struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced PersistentVolumeClaim struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced StorageClass struct

Signed-off-by: r2k1 <yokree@gmail.com>

* use reduced Job struct

Signed-off-by: r2k1 <yokree@gmail.com>

* delete unused PodDisruptionBudget

Signed-off-by: r2k1 <yokree@gmail.com>

* create reflector-based version of cluster cache

Signed-off-by: r2k1 <yokree@gmail.com>

* update uid getter

Signed-off-by: r2k1 <yokree@gmail.com>

* upgrade kube api version

Signed-off-by: r2k1 <yokree@gmail.com>

* rename NewKubernetesClusterCache2 => NewKubernetesClusterCacheV2

Signed-off-by: r2k1 <yokree@gmail.com>

* improve type safety

Signed-off-by: r2k1 <yokree@gmail.com>

* add replicationControllerStore, replicaSetStore, pdbStore

Signed-off-by: r2k1 <yokree@gmail.com>

* return cluster importer/exporter

Signed-off-by: r2k1 <yokree@gmail.com>

* group statements

Signed-off-by: r2k1 <yokree@gmail.com>

* fix pdb store reference

Signed-off-by: r2k1 <yokree@gmail.com>

* fix post-merge conflicts

Signed-off-by: r2k1 <yokree@gmail.com>

* Cluster Cache Optimization Effort - Additional Fields (#3008)

* merge develop

Signed-off-by: Matt Bolt <mbolt35@gmail.com>

* First iteration on adding necessary fields to flyweights.

Signed-off-by: Matt Bolt <mbolt35@gmail.com>

---------

Signed-off-by: Matt Bolt <mbolt35@gmail.com>
Signed-off-by: r2k1 <yokree@gmail.com>
Co-authored-by: r2k1 <yokree@gmail.com>

* make cache selection configurable

Signed-off-by: r2k1 <yokree@gmail.com>

* fix compilation error

Signed-off-by: r2k1 <yokree@gmail.com>

* Cluster Cache Optimization Part 2 - Config Map Separation (#3011)

* [Cache Optimization] Remove the k8s resource endpoints (#3016)

* Remove the k8s resource endpoints

Signed-off-by: Matt Bolt <mbolt35@gmail.com>

* drop pod logs from opencost

Signed-off-by: Matt Bolt <mbolt35@gmail.com>

---------

Signed-off-by: Matt Bolt <mbolt35@gmail.com>

---------

Signed-off-by: r2k1 <yokree@gmail.com>
Signed-off-by: Matt Bolt <mbolt35@gmail.com>
Co-authored-by: Matt Bolt <mbolt35@gmail.com>
r2k1 1 rok temu
rodzic
commit
ee44b80e2f
41 zmienionych plików z 1083 dodań i 935 usunięć
  1. 0 74
      core/pkg/util/watcher/configwatchers.go
  2. 6 7
      pkg/cloud/alibaba/provider.go
  3. 10 9
      pkg/cloud/alibaba/provider_test.go
  4. 7 9
      pkg/cloud/aws/provider.go
  5. 5 3
      pkg/cloud/aws/provider_test.go
  6. 3 5
      pkg/cloud/azure/provider.go
  7. 7 8
      pkg/cloud/gcp/provider.go
  8. 3 4
      pkg/cloud/models/models.go
  9. 5 6
      pkg/cloud/oracle/provider.go
  10. 5 6
      pkg/cloud/oracle/provider_test.go
  11. 4 5
      pkg/cloud/otc/provider.go
  12. 10 11
      pkg/cloud/provider/csvprovider.go
  13. 2 4
      pkg/cloud/provider/customprovider.go
  14. 3 5
      pkg/cloud/provider/provider.go
  15. 2 4
      pkg/cloud/scaleway/provider.go
  16. 410 72
      pkg/clustercache/clustercache.go
  17. 111 0
      pkg/clustercache/clustercache2.go
  18. 14 20
      pkg/clustercache/clusterexporter.go
  19. 29 138
      pkg/clustercache/clusterimporter.go
  20. 111 0
      pkg/clustercache/store.go
  21. 4 21
      pkg/cmd/agent/agent.go
  22. 4 4
      pkg/costmodel/containerkeys.go
  23. 53 53
      pkg/costmodel/costmodel.go
  24. 12 16
      pkg/costmodel/costmodel_test.go
  25. 2 2
      pkg/costmodel/metrics.go
  26. 7 335
      pkg/costmodel/router.go
  27. 8 0
      pkg/env/costmodelenv.go
  28. 8 8
      pkg/metrics/deploymentmetrics.go
  29. 2 2
      pkg/metrics/jobmetrics.go
  30. 1 1
      pkg/metrics/kubemetrics.go
  31. 1 1
      pkg/metrics/metricsconfig.go
  32. 2 2
      pkg/metrics/namespacemetrics.go
  33. 2 2
      pkg/metrics/nodemetrics.go
  34. 10 10
      pkg/metrics/podlabelmetrics.go
  35. 12 18
      pkg/metrics/podlabelmetrics_test.go
  36. 6 6
      pkg/metrics/podmetrics.go
  37. 3 3
      pkg/metrics/servicemetrics.go
  38. 3 3
      pkg/metrics/statefulsetmetrics.go
  39. 10 6
      pkg/util/watcher/configwatcher_test.go
  40. 135 0
      pkg/util/watcher/configwatchers.go
  41. 51 52
      test/cloud_test.go

+ 0 - 74
core/pkg/util/watcher/configwatchers.go

@@ -1,74 +0,0 @@
-package watcher
-
-import (
-	"github.com/opencost/opencost/core/pkg/log"
-	v1 "k8s.io/api/core/v1"
-)
-
-// ConfigMapWatcher represents a single configmap watcher
-type ConfigMapWatcher struct {
-	ConfigMapName string
-	WatchFunc     func(string, map[string]string) error
-}
-
-type ConfigMapWatchers struct {
-	watchers map[string][]*ConfigMapWatcher
-}
-
-func NewConfigMapWatchers(watchers ...*ConfigMapWatcher) *ConfigMapWatchers {
-	cmw := &ConfigMapWatchers{
-		watchers: make(map[string][]*ConfigMapWatcher),
-	}
-
-	for _, w := range watchers {
-		cmw.AddWatcher(w)
-	}
-
-	return cmw
-}
-
-func (cmw *ConfigMapWatchers) AddWatcher(watcher *ConfigMapWatcher) {
-	if watcher == nil {
-		return
-	}
-
-	name := watcher.ConfigMapName
-	cmw.watchers[name] = append(cmw.watchers[name], watcher)
-}
-
-func (cmw *ConfigMapWatchers) Add(configMapName string, watchFunc func(string, map[string]string) error) {
-	cmw.AddWatcher(&ConfigMapWatcher{
-		ConfigMapName: configMapName,
-		WatchFunc:     watchFunc,
-	})
-}
-
-func (cmw *ConfigMapWatchers) GetWatchedConfigs() []string {
-	configNames := []string{}
-
-	for k := range cmw.watchers {
-		configNames = append(configNames, k)
-	}
-
-	return configNames
-}
-
-func (cmw *ConfigMapWatchers) ToWatchFunc() func(interface{}) {
-	return func(c interface{}) {
-		conf, ok := c.(*v1.ConfigMap)
-		if !ok {
-			return
-		}
-
-		name := conf.GetName()
-		data := conf.Data
-		if watchers, ok := cmw.watchers[name]; ok {
-			for _, cw := range watchers {
-				err := cw.WatchFunc(name, data)
-				if err != nil {
-					log.Infof("ERROR UPDATING %s CONFIG: %s", name, err.Error())
-				}
-			}
-		}
-	}
-}

+ 6 - 7
pkg/cloud/alibaba/provider.go

@@ -27,7 +27,6 @@ import (
 
 	ocenv "github.com/opencost/opencost/pkg/env"
 	"golang.org/x/exp/slices"
-	v1 "k8s.io/api/core/v1"
 )
 
 const (
@@ -847,7 +846,7 @@ func (alibabaNodeKey *AlibabaNodeKey) GPUCount() int {
 }
 
 // Get's the key for the k8s node input
-func (alibaba *Alibaba) GetKey(mapValue map[string]string, node *v1.Node) models.Key {
+func (alibaba *Alibaba) GetKey(mapValue map[string]string, node *clustercache.Node) models.Key {
 	slimK8sNode := generateSlimK8sNodeFromV1Node(node)
 
 	var aak *credentials.AccessKeyCredential
@@ -913,7 +912,7 @@ type AlibabaPVKey struct {
 	SizeInGiB         string
 }
 
-func (alibaba *Alibaba) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (alibaba *Alibaba) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	regionID := defaultRegion
 	// If default Region is not passed default it to cluster region ID.
 	if defaultRegion == "" {
@@ -1253,7 +1252,7 @@ func getSystemDiskInfoOfANode(instanceID, regionID string, client *sdk.Client, s
 }
 
 // generateSlimK8sNodeFromV1Node generates SlimK8sNode struct from v1.Node to fetch pricing information and call alibaba API.
-func generateSlimK8sNodeFromV1Node(node *v1.Node) *SlimK8sNode {
+func generateSlimK8sNodeFromV1Node(node *clustercache.Node) *SlimK8sNode {
 	var regionID, osType, instanceType, providerID, priceUnit, instanceFamily string
 	var memorySizeInKiB string // TO-DO: try to convert it into float
 	var ok, IsIoOptimized bool
@@ -1272,7 +1271,7 @@ func generateSlimK8sNodeFromV1Node(node *v1.Node) *SlimK8sNode {
 
 	instanceFamily = getInstanceFamilyFromType(instanceType)
 	memorySizeInKiB = fmt.Sprintf("%s", node.Status.Capacity.Memory())
-	providerID = node.Spec.ProviderID // Alibaba Cloud provider doesnt follow convention of prefix with cloud provider name
+	providerID = node.SpecProviderID // Alibaba Cloud provider doesnt follow convention of prefix with cloud provider name
 
 	// Looking at current Instance offering , all of the Instances seem to be I/O optimized - https://www.alibabacloud.com/help/en/elastic-compute-service/latest/instance-family
 	// Basic price Json has it as part of the key so defaulting to true.
@@ -1303,7 +1302,7 @@ func getNumericalValueFromResourceQuantity(quantity string) (value string) {
 
 // generateSlimK8sDiskFromV1PV function generates SlimK8sDisk from v1.PersistentVolume
 // to generate slim disk type that can be used to fetch pricing information for Data disk type.
-func generateSlimK8sDiskFromV1PV(pv *v1.PersistentVolume, regionID string) *SlimK8sDisk {
+func generateSlimK8sDiskFromV1PV(pv *clustercache.PersistentVolume, regionID string) *SlimK8sDisk {
 
 	// All PVs are data disks while local disk are categorized as system disk
 	diskType := ALIBABA_DATA_DISK_CATEGORY
@@ -1349,7 +1348,7 @@ func generateSlimK8sDiskFromV1PV(pv *v1.PersistentVolume, regionID string) *Slim
 // if topology.diskplugin.csi.alibabacloud.com/zone label/annotation is passed during PV creation determine the region based on this pv label.
 // if neither of the above label/annotation is present check node affinity for the zone affinity and determine the region based on this zone.
 // if nether of the above yields a region , return empty string to default it to cluster region.
-func determinePVRegion(pv *v1.PersistentVolume) string {
+func determinePVRegion(pv *clustercache.PersistentVolume) string {
 	// if "topology.diskplugin.csi.alibabacloud.com/region" is present as a label or annotation return that as the PV region
 	if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
 		log.Debugf("determinePVRegion returned a region value of: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)

+ 10 - 9
pkg/cloud/alibaba/provider_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
 	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/signers"
 	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/clustercache"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 )
@@ -637,7 +638,7 @@ func TestDetermineKeyForPricing(t *testing.T) {
 }
 
 func TestGenerateSlimK8sNodeFromV1Node(t *testing.T) {
-	testv1Node := &v1.Node{}
+	testv1Node := &clustercache.Node{}
 	testv1Node.Labels = make(map[string]string)
 	testv1Node.Labels["topology.kubernetes.io/region"] = "us-east-1"
 	testv1Node.Labels["beta.kubernetes.io/os"] = "linux"
@@ -647,7 +648,7 @@ func TestGenerateSlimK8sNodeFromV1Node(t *testing.T) {
 	}
 	cases := []struct {
 		name             string
-		testNode         *v1.Node
+		testNode         *clustercache.Node
 		expectedSlimNode *SlimK8sNode
 	}{
 		{
@@ -690,7 +691,7 @@ func TestGenerateSlimK8sNodeFromV1Node(t *testing.T) {
 }
 
 func TestGenerateSlimK8sDiskFromV1PV(t *testing.T) {
-	testv1PV := &v1.PersistentVolume{}
+	testv1PV := &clustercache.PersistentVolume{}
 	testv1PV.Spec.Capacity = v1.ResourceList{
 		v1.ResourceStorage: *resource.NewQuantity(16*1024*1024*1024, resource.BinarySI),
 	}
@@ -704,7 +705,7 @@ func TestGenerateSlimK8sDiskFromV1PV(t *testing.T) {
 	testv1PV.Spec.StorageClassName = "testStorageClass"
 	cases := []struct {
 		name             string
-		testPV           *v1.PersistentVolume
+		testPV           *clustercache.PersistentVolume
 		expectedSlimDisk *SlimK8sDisk
 		inpRegionID      string
 	}{
@@ -805,7 +806,7 @@ func TestDeterminePVRegion(t *testing.T) {
 	}
 
 	// testPV1 contains the Label with region information as well as node affinity in spec
-	testPV1 := &v1.PersistentVolume{}
+	testPV1 := &clustercache.PersistentVolume{}
 	testPV1.Name = "testPV1"
 	testPV1.Labels = make(map[string]string)
 	testPV1.Labels[ALIBABA_DISK_TOPOLOGY_REGION_LABEL] = "us-east-1"
@@ -816,13 +817,13 @@ func TestDeterminePVRegion(t *testing.T) {
 	}
 
 	// testPV2 contains the only zone label
-	testPV2 := &v1.PersistentVolume{}
+	testPV2 := &clustercache.PersistentVolume{}
 	testPV2.Name = "testPV2"
 	testPV2.Labels = make(map[string]string)
 	testPV2.Labels[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL] = "us-east-1a"
 
 	// testPV3 contains only node affinity in spec
-	testPV3 := &v1.PersistentVolume{}
+	testPV3 := &clustercache.PersistentVolume{}
 	testPV3.Name = "testPV3"
 	testPV3.Spec.NodeAffinity = &v1.VolumeNodeAffinity{
 		Required: &v1.NodeSelector{
@@ -831,12 +832,12 @@ func TestDeterminePVRegion(t *testing.T) {
 	}
 
 	// testPV4 contains no label/annotation or any node affinity
-	testPV4 := &v1.PersistentVolume{}
+	testPV4 := &clustercache.PersistentVolume{}
 	testPV4.Name = "testPV4"
 
 	cases := []struct {
 		name           string
-		inputPV        *v1.PersistentVolume
+		inputPV        *clustercache.PersistentVolume
 		expectedRegion string
 	}{
 		{

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

@@ -43,8 +43,6 @@ import (
 	"github.com/aws/aws-sdk-go-v2/service/sts"
 
 	"github.com/jszwec/csvutil"
-
-	v1 "k8s.io/api/core/v1"
 )
 
 const (
@@ -679,7 +677,7 @@ type awsPVKey struct {
 	ProviderID             string
 }
 
-func (aws *AWS) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (aws *AWS) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	providerID := ""
 	if pv.Spec.AWSElasticBlockStore != nil {
 		providerID = pv.Spec.AWSElasticBlockStore.VolumeID
@@ -745,7 +743,7 @@ func getStorageClassTypeFrom(provisioner string) string {
 }
 
 // GetKey maps node labels to information needed to retrieve pricing data
-func (aws *AWS) GetKey(labels map[string]string, n *v1.Node) models.Key {
+func (aws *AWS) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
 	return &awsKey{
 		SpotLabelName:  aws.SpotLabelName,
 		SpotLabelValue: aws.SpotLabelValue,
@@ -767,13 +765,13 @@ func (aws *AWS) ClusterManagementPricing() (string, float64, error) {
 }
 
 // Use the pricing data from the current region. Fall back to using all region data if needed.
-func (aws *AWS) getRegionPricing(nodeList []*v1.Node) (*http.Response, string, error) {
+func (aws *AWS) getRegionPricing(nodeList []*clustercache.Node) (*http.Response, string, error) {
 
 	pricingURL := "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/"
 	region := ""
 	multiregion := false
 	for _, n := range nodeList {
-		labels := n.GetLabels()
+		labels := n.Labels
 		currentNodeRegion := ""
 		if r, ok := util.GetRegion(labels); ok {
 			currentNodeRegion = r
@@ -857,7 +855,7 @@ func (aws *AWS) DownloadPricingData() error {
 			aws.clusterProvisioner = "KOPS"
 		}
 
-		labels := n.GetObjectMeta().GetLabels()
+		labels := n.Labels
 		key := aws.GetKey(labels, n)
 		inputkeys[key.Features()] = true
 	}
@@ -871,8 +869,8 @@ func (aws *AWS) DownloadPricingData() error {
 		if params != nil {
 			params["provisioner"] = storageClass.Provisioner
 		}
-		storageClassMap[storageClass.ObjectMeta.Name] = params
-		if storageClass.GetAnnotations()["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.GetAnnotations()["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
+		storageClassMap[storageClass.Name] = params
+		if storageClass.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.Annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
 			storageClassMap["default"] = params
 			storageClassMap[""] = params
 		}

+ 5 - 3
pkg/cloud/aws/provider_test.go

@@ -10,6 +10,7 @@ import (
 	"testing"
 
 	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/clustercache"
 	v1 "k8s.io/api/core/v1"
 )
 
@@ -119,11 +120,12 @@ func Test_PricingData_Regression(t *testing.T) {
 
 	// Check pricing data produced for each region
 	for _, region := range awsRegions {
-		node := v1.Node{}
-		node.SetLabels(map[string]string{"topology.kubernetes.io/region": region})
 
 		awsTest := AWS{}
-		res, _, err := awsTest.getRegionPricing([]*v1.Node{&node})
+		res, _, err := awsTest.getRegionPricing([]*clustercache.Node{
+			{
+				Labels: map[string]string{"topology.kubernetes.io/region": region},
+			}})
 		if err != nil {
 			t.Errorf("Failed to download pricing data for region %s: %v", region, err)
 		}

+ 3 - 5
pkg/cloud/azure/provider.go

@@ -31,8 +31,6 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/utils"
 	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
-
-	v1 "k8s.io/api/core/v1"
 )
 
 const (
@@ -681,7 +679,7 @@ func (az *Azure) loadAzureStorageConfig(force bool) (*AzureStorageConfig, error)
 	return &asc, nil
 }
 
-func (az *Azure) GetKey(labels map[string]string, n *v1.Node) models.Key {
+func (az *Azure) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
 	cfg, err := az.GetConfig()
 	if err != nil {
 		log.Infof("Error loading azure custom pricing information")
@@ -778,7 +776,7 @@ func (az *Azure) GetManagementPlatform() (string, error) {
 
 	if len(nodes) > 0 {
 		n := nodes[0]
-		providerID := n.Spec.ProviderID
+		providerID := n.SpecProviderID
 		if strings.Contains(providerID, "aks") {
 			return "aks", nil
 		}
@@ -1255,7 +1253,7 @@ type azurePvKey struct {
 	ProviderId             string
 }
 
-func (az *Azure) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (az *Azure) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	providerID := ""
 	if pv.Spec.AzureDisk != nil {
 		providerID = pv.Spec.AzureDisk.DiskName

+ 7 - 8
pkg/cloud/gcp/provider.go

@@ -32,7 +32,6 @@ import (
 	"cloud.google.com/go/compute/metadata"
 	"golang.org/x/oauth2/google"
 	"google.golang.org/api/compute/v1"
-	v1 "k8s.io/api/core/v1"
 )
 
 const GKE_GPU_TAG = "cloud.google.com/gke-accelerator"
@@ -1099,7 +1098,7 @@ func (gcp *GCP) DownloadPricingData() error {
 
 	defaultRegion := "" // Sometimes, PVs may be missing the region label. In that case assume that they are in the same region as the nodes
 	for _, n := range nodeList {
-		labels := n.GetObjectMeta().GetLabels()
+		labels := n.Labels
 		if _, ok := labels["cloud.google.com/gke-nodepool"]; ok { // The node is part of a GKE nodepool, so you're paying a cluster management cost
 			gcp.clusterManagementPrice = 0.10
 			gcp.clusterProvisioner = "GKE"
@@ -1117,8 +1116,8 @@ func (gcp *GCP) DownloadPricingData() error {
 	storageClassMap := make(map[string]map[string]string)
 	for _, storageClass := range storageClasses {
 		params := storageClass.Parameters
-		storageClassMap[storageClass.ObjectMeta.Name] = params
-		if storageClass.GetAnnotations()["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.GetAnnotations()["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
+		storageClassMap[storageClass.Name] = params
+		if storageClass.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.Annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
 			storageClassMap["default"] = params
 			storageClassMap[""] = params
 		}
@@ -1296,12 +1295,12 @@ func (gcp *GCP) ApplyReservedInstancePricing(nodes map[string]*models.Node) {
 		}
 	}
 
-	gcpNodes := make(map[string]*v1.Node)
+	gcpNodes := make(map[string]*clustercache.Node)
 	currentNodes := gcp.Clientset.GetAllNodes()
 
 	// Create a node name -> node map
 	for _, gcpNode := range currentNodes {
-		gcpNodes[gcpNode.GetName()] = gcpNode
+		gcpNodes[gcpNode.Name] = gcpNode
 	}
 
 	// go through all provider nodes using k8s nodes for region
@@ -1453,7 +1452,7 @@ func (key *pvKey) GetStorageClass() string {
 	return key.StorageClass
 }
 
-func (gcp *GCP) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (gcp *GCP) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	providerID := ""
 	if pv.Spec.GCEPersistentDisk != nil {
 		providerID = pv.Spec.GCEPersistentDisk.PDName
@@ -1492,7 +1491,7 @@ type gcpKey struct {
 	Labels map[string]string
 }
 
-func (gcp *GCP) GetKey(labels map[string]string, n *v1.Node) models.Key {
+func (gcp *GCP) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
 	return &gcpKey{
 		Labels: labels,
 	}

+ 3 - 4
pkg/cloud/models/models.go

@@ -10,9 +10,8 @@ import (
 	"time"
 
 	"github.com/microcosm-cc/bluemonday"
-	v1 "k8s.io/api/core/v1"
-
 	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/config"
 )
 
@@ -315,8 +314,8 @@ type Provider interface {
 	LoadBalancerPricing() (*LoadBalancer, error) // TODO: add key interface arg for dynamic price fetching
 	AllNodePricing() (interface{}, error)
 	DownloadPricingData() error
-	GetKey(map[string]string, *v1.Node) Key
-	GetPVKey(*v1.PersistentVolume, map[string]string, string) PVKey
+	GetKey(map[string]string, *clustercache.Node) Key
+	GetPVKey(*clustercache.PersistentVolume, map[string]string, string) PVKey
 	UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error)
 	UpdateConfigFromConfigMap(map[string]string) (*CustomPricing, error)
 	GetConfig() (*CustomPricing, error)

+ 5 - 6
pkg/cloud/oracle/provider.go

@@ -15,7 +15,6 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/utils"
 	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
-	v1 "k8s.io/api/core/v1"
 )
 
 const nodePoolIdAnnotation = "oci.oraclecloud.com/node-pool-id"
@@ -125,7 +124,7 @@ func (o *Oracle) DownloadPricingData() error {
 	return nil
 }
 
-func (o *Oracle) GetKey(labels map[string]string, n *v1.Node) models.Key {
+func (o *Oracle) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
 	var gpuCount int
 	var gpuType string
 	if gpuc, ok := n.Status.Capacity["nvidia.com/gpu"]; ok {
@@ -134,7 +133,7 @@ func (o *Oracle) GetKey(labels map[string]string, n *v1.Node) models.Key {
 	}
 	instanceType, _ := util.GetInstanceType(labels)
 	return &oracleKey{
-		providerID:   n.Spec.ProviderID,
+		providerID:   n.SpecProviderID,
 		instanceType: instanceType,
 		labels:       labels,
 		gpuCount:     gpuCount,
@@ -142,7 +141,7 @@ func (o *Oracle) GetKey(labels map[string]string, n *v1.Node) models.Key {
 	}
 }
 
-func (o *Oracle) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, _ string) models.PVKey {
+func (o *Oracle) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, _ string) models.PVKey {
 	var providerID string
 	var driver string
 	if pv.Spec.CSI != nil {
@@ -211,10 +210,10 @@ func (o *Oracle) GetConfig() (*models.CustomPricing, error) {
 func (o *Oracle) GetManagementPlatform() (string, error) {
 	nodes := o.Clientset.GetAllNodes()
 	for _, node := range nodes {
-		if _, ok := node.GetObjectMeta().GetAnnotations()[nodePoolIdAnnotation]; ok {
+		if _, ok := node.Annotations[nodePoolIdAnnotation]; ok {
 			return managementPlatformOKE, nil
 		}
-		if _, ok := node.GetObjectMeta().GetAnnotations()[virtualPoolIdAnnotation]; ok {
+		if _, ok := node.Annotations[virtualPoolIdAnnotation]; ok {
 			return managementPlatformOKE, nil
 		}
 	}

+ 5 - 6
pkg/cloud/oracle/provider_test.go

@@ -6,6 +6,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -56,7 +57,7 @@ func TestGetKey(t *testing.T) {
 func TestGetPVKey(t *testing.T) {
 	storageClass := "xyz"
 	providerID := "ocid.abc"
-	pv := &v1.PersistentVolume{
+	pv := &clustercache.PersistentVolume{
 		Spec: v1.PersistentVolumeSpec{
 			StorageClassName: storageClass,
 			PersistentVolumeSource: v1.PersistentVolumeSource{
@@ -78,15 +79,13 @@ func TestRegions(t *testing.T) {
 	assert.Len(t, regions, 39)
 }
 
-func testNode(gpus int) *v1.Node {
+func testNode(gpus int) *clustercache.Node {
 	capacity := map[v1.ResourceName]resource.Quantity{}
 	if gpus > 0 {
 		capacity["nvidia.com/gpu"] = resource.MustParse(fmt.Sprintf("%d", gpus))
 	}
-	return &v1.Node{
-		Spec: v1.NodeSpec{
-			ProviderID: "ocid.abc",
-		},
+	return &clustercache.Node{
+		SpecProviderID: "ocid.abc",
 		Status: v1.NodeStatus{
 			Capacity: capacity,
 		},

+ 4 - 5
pkg/cloud/otc/provider.go

@@ -16,7 +16,6 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/models"
 	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
-	v1 "k8s.io/api/core/v1"
 )
 
 // OTC node pricing attributes
@@ -116,7 +115,7 @@ func (k *otcKey) Features() string {
 
 // Extract/generate a key that holds the data required to calculate
 // the cost of the given node (like s2.large.4).
-func (otc *OTC) GetKey(labels map[string]string, n *v1.Node) models.Key {
+func (otc *OTC) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
 	return &otcKey{
 		Labels:     labels,
 		ProviderID: labels["providerID"],
@@ -133,7 +132,7 @@ func (k *otcPVKey) ID() string {
 	return k.ProviderId
 }
 
-func (otc *OTC) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (otc *OTC) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	providerID := ""
 	return &otcPVKey{
 		Labels:                 pv.Labels,
@@ -229,7 +228,7 @@ func (otc *OTC) DownloadPricingData() error {
 		fmt.Println(tmp.Parameters)
 		fmt.Println(tmp.Labels)
 		fmt.Println(tmp.TypeMeta)
-		fmt.Println(tmp.Size())
+		fmt.Println(tmp.Size)
 	}
 
 	// Slice with all persistent volumes present in the cluster
@@ -240,7 +239,7 @@ func (otc *OTC) DownloadPricingData() error {
 	inputkeys := make(map[string]bool)
 	tmp := []string{}
 	for _, node := range nodeList {
-		labels := node.GetObjectMeta().GetLabels()
+		labels := node.Labels
 		key := otc.GetKey(labels, node)
 		inputkeys[key.Features()] = true
 		tmp = append(tmp, key.Features())

+ 10 - 11
pkg/cloud/provider/csvprovider.go

@@ -13,15 +13,14 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/util"
 	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/s3"
-	"github.com/opencost/opencost/core/pkg/log"
-	v1 "k8s.io/api/core/v1"
-
 	"github.com/jszwec/csvutil"
+	"github.com/opencost/opencost/core/pkg/log"
 )
 
 const refreshMinutes = 60
@@ -289,7 +288,7 @@ func (c *CSVProvider) NodePricing(key models.Key) (*models.Node, models.PricingM
 	return node, models.PricingMetadata{}, nil
 }
 
-func NodeValueFromMapField(m string, n *v1.Node, useRegion bool) string {
+func NodeValueFromMapField(m string, n *clustercache.Node, useRegion bool) string {
 	mf := strings.Split(m, ".")
 	toReturn := ""
 	if useRegion {
@@ -300,16 +299,16 @@ func NodeValueFromMapField(m string, n *v1.Node, useRegion bool) string {
 		}
 	}
 	if len(mf) == 2 && mf[0] == "spec" && mf[1] == "providerID" {
-		for matchNum, group := range provIdRx.FindStringSubmatch(n.Spec.ProviderID) {
+		for matchNum, group := range provIdRx.FindStringSubmatch(n.SpecProviderID) {
 			if matchNum == 2 {
 				return toReturn + group
 			}
 		}
-		if strings.HasPrefix(n.Spec.ProviderID, "azure://") {
-			vmOrScaleSet := strings.ToLower(strings.TrimPrefix(n.Spec.ProviderID, "azure://"))
+		if strings.HasPrefix(n.SpecProviderID, "azure://") {
+			vmOrScaleSet := strings.ToLower(strings.TrimPrefix(n.SpecProviderID, "azure://"))
 			return toReturn + vmOrScaleSet
 		}
-		return toReturn + n.Spec.ProviderID
+		return toReturn + n.SpecProviderID
 	} else if len(mf) > 1 && mf[0] == "metadata" {
 		if mf[1] == "name" {
 			return toReturn + n.Name
@@ -329,7 +328,7 @@ func NodeValueFromMapField(m string, n *v1.Node, useRegion bool) string {
 	}
 }
 
-func PVValueFromMapField(m string, n *v1.PersistentVolume) string {
+func PVValueFromMapField(m string, n *clustercache.PersistentVolume) string {
 	mf := strings.Split(m, ".")
 	if len(mf) > 1 && mf[0] == "metadata" {
 		if mf[1] == "name" {
@@ -365,7 +364,7 @@ func PVValueFromMapField(m string, n *v1.PersistentVolume) string {
 	}
 }
 
-func (c *CSVProvider) GetKey(l map[string]string, n *v1.Node) models.Key {
+func (c *CSVProvider) GetKey(l map[string]string, n *clustercache.Node) models.Key {
 	id := NodeValueFromMapField(c.NodeMapField, n, c.UsesRegion)
 	var gpuCount int64
 	gpuCount = 0
@@ -401,7 +400,7 @@ func (key *csvPVKey) Features() string {
 	return key.ProviderID
 }
 
-func (c *CSVProvider) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (c *CSVProvider) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	id := PVValueFromMapField(c.PVMapField, pv)
 	return &csvPVKey{
 		Labels:                 pv.Labels,

+ 2 - 4
pkg/cloud/provider/customprovider.go

@@ -16,8 +16,6 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/utils"
 	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
-
-	v1 "k8s.io/api/core/v1"
 )
 
 type NodePrice struct {
@@ -242,7 +240,7 @@ func (cp *CustomProvider) DownloadPricingData() error {
 	return nil
 }
 
-func (cp *CustomProvider) GetKey(labels map[string]string, n *v1.Node) models.Key {
+func (cp *CustomProvider) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
 	return &customProviderKey{
 		SpotLabel:      cp.SpotLabel,
 		SpotLabelValue: cp.SpotLabelValue,
@@ -329,7 +327,7 @@ func (cp *CustomProvider) LoadBalancerPricing() (*models.LoadBalancer, error) {
 	}, nil
 }
 
-func (*CustomProvider) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (*CustomProvider) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	return &customPVKey{
 		Labels:                 pv.Labels,
 		StorageClassName:       pv.Spec.StorageClassName,

+ 3 - 5
pkg/cloud/provider/provider.go

@@ -26,12 +26,10 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/util/httputil"
-	"github.com/opencost/opencost/core/pkg/util/watcher"
 	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/config"
 	"github.com/opencost/opencost/pkg/env"
-
-	v1 "k8s.io/api/core/v1"
+	"github.com/opencost/opencost/pkg/util/watcher"
 )
 
 // ClusterName returns the name defined in cluster info, defaulting to the
@@ -278,8 +276,8 @@ type clusterProperties struct {
 	projectID      string
 }
 
-func getClusterProperties(node *v1.Node) clusterProperties {
-	providerID := strings.ToLower(node.Spec.ProviderID)
+func getClusterProperties(node *clustercache.Node) clusterProperties {
+	providerID := strings.ToLower(node.SpecProviderID)
 	region, _ := util.GetRegion(node.Labels)
 	cp := clusterProperties{
 		provider:       "DEFAULT",

+ 2 - 4
pkg/cloud/scaleway/provider.go

@@ -19,8 +19,6 @@ import (
 	"github.com/opencost/opencost/pkg/env"
 
 	"github.com/opencost/opencost/core/pkg/log"
-	v1 "k8s.io/api/core/v1"
-
 	"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
 	"github.com/scaleway/scaleway-sdk-go/scw"
 )
@@ -181,7 +179,7 @@ func (c *Scaleway) NetworkPricing() (*models.Network, error) {
 	}, nil
 }
 
-func (c *Scaleway) GetKey(l map[string]string, n *v1.Node) models.Key {
+func (c *Scaleway) GetKey(l map[string]string, n *clustercache.Node) models.Key {
 	return &scalewayKey{
 		Labels: l,
 	}
@@ -208,7 +206,7 @@ func (key *scalewayPVKey) Features() string {
 	return key.Zone
 }
 
-func (c *Scaleway) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+func (c *Scaleway) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
 	// the csi volume handle is the form <az>/<volume-id>
 	zone := ""
 	if pv.Spec.CSI != nil {

+ 410 - 72
pkg/clustercache/clustercache.go

@@ -2,9 +2,13 @@ package clustercache
 
 import (
 	"sync"
+	"time"
 
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/pkg/env"
+	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"
@@ -15,6 +19,347 @@ import (
 	"k8s.io/client-go/kubernetes"
 )
 
+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 {
+	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
+}
+
+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
+	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{
+		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,
+	}
+}
+
+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,
+		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 {
@@ -25,49 +370,46 @@ type ClusterCache interface {
 	Stop()
 
 	// GetAllNamespaces returns all the cached namespaces
-	GetAllNamespaces() []*v1.Namespace
+	GetAllNamespaces() []*Namespace
 
 	// GetAllNodes returns all the cached nodes
-	GetAllNodes() []*v1.Node
+	GetAllNodes() []*Node
 
 	// GetAllPods returns all the cached pods
-	GetAllPods() []*v1.Pod
+	GetAllPods() []*Pod
 
 	// GetAllServices returns all the cached services
-	GetAllServices() []*v1.Service
+	GetAllServices() []*Service
 
 	// GetAllDaemonSets returns all the cached DaemonSets
-	GetAllDaemonSets() []*appsv1.DaemonSet
+	GetAllDaemonSets() []*DaemonSet
 
 	// GetAllDeployments returns all the cached deployments
-	GetAllDeployments() []*appsv1.Deployment
+	GetAllDeployments() []*Deployment
 
 	// GetAllStatfulSets returns all the cached StatefulSets
-	GetAllStatefulSets() []*appsv1.StatefulSet
+	GetAllStatefulSets() []*StatefulSet
 
 	// GetAllReplicaSets returns all the cached ReplicaSets
-	GetAllReplicaSets() []*appsv1.ReplicaSet
+	GetAllReplicaSets() []*ReplicaSet
 
 	// GetAllPersistentVolumes returns all the cached persistent volumes
-	GetAllPersistentVolumes() []*v1.PersistentVolume
+	GetAllPersistentVolumes() []*PersistentVolume
 
 	// GetAllPersistentVolumeClaims returns all the cached persistent volume claims
-	GetAllPersistentVolumeClaims() []*v1.PersistentVolumeClaim
+	GetAllPersistentVolumeClaims() []*PersistentVolumeClaim
 
 	// GetAllStorageClasses returns all the cached storage classes
-	GetAllStorageClasses() []*stv1.StorageClass
+	GetAllStorageClasses() []*StorageClass
 
 	// GetAllJobs returns all the cached jobs
-	GetAllJobs() []*batchv1.Job
+	GetAllJobs() []*Job
 
 	// GetAllPodDisruptionBudgets returns all cached pod disruption budgets
-	GetAllPodDisruptionBudgets() []*policyv1.PodDisruptionBudget
+	GetAllPodDisruptionBudgets() []*PodDisruptionBudget
 
 	// GetAllReplicationControllers returns all cached replication controllers
-	GetAllReplicationControllers() []*v1.ReplicationController
-
-	// SetConfigMapUpdateFunc sets the configmap update function
-	SetConfigMapUpdateFunc(func(interface{}))
+	GetAllReplicationControllers() []*ReplicationController
 }
 
 // KubernetesClusterCache is the implementation of ClusterCache
@@ -77,7 +419,6 @@ type KubernetesClusterCache struct {
 	namespaceWatch             WatchController
 	nodeWatch                  WatchController
 	podWatch                   WatchController
-	kubecostConfigMapWatch     WatchController
 	serviceWatch               WatchController
 	daemonsetsWatch            WatchController
 	deploymentsWatch           WatchController
@@ -98,6 +439,13 @@ func initializeCache(wc WatchController, wg *sync.WaitGroup, cancel chan struct{
 }
 
 func NewKubernetesClusterCache(client kubernetes.Interface) ClusterCache {
+	if env.GetUseCacheV1() {
+		return NewKubernetesClusterCacheV1(client)
+	}
+	return NewKubernetesClusterCacheV2(client)
+}
+
+func NewKubernetesClusterCacheV1(client kubernetes.Interface) ClusterCache {
 	coreRestClient := client.CoreV1().RESTClient()
 	appsRestClient := client.AppsV1().RESTClient()
 	storageRestClient := client.StorageV1().RESTClient()
@@ -112,7 +460,6 @@ func NewKubernetesClusterCache(client kubernetes.Interface) ClusterCache {
 		namespaceWatch:             NewCachingWatcher(coreRestClient, "namespaces", &v1.Namespace{}, "", fields.Everything()),
 		nodeWatch:                  NewCachingWatcher(coreRestClient, "nodes", &v1.Node{}, "", fields.Everything()),
 		podWatch:                   NewCachingWatcher(coreRestClient, "pods", &v1.Pod{}, "", fields.Everything()),
-		kubecostConfigMapWatch:     NewCachingWatcher(coreRestClient, "configmaps", &v1.ConfigMap{}, kubecostNamespace, fields.Everything()),
 		serviceWatch:               NewCachingWatcher(coreRestClient, "services", &v1.Service{}, "", fields.Everything()),
 		daemonsetsWatch:            NewCachingWatcher(appsRestClient, "daemonsets", &appsv1.DaemonSet{}, "", fields.Everything()),
 		deploymentsWatch:           NewCachingWatcher(appsRestClient, "deployments", &appsv1.Deployment{}, "", fields.Everything()),
@@ -129,12 +476,8 @@ func NewKubernetesClusterCache(client kubernetes.Interface) ClusterCache {
 	// Wait for each caching watcher to initialize
 	cancel := make(chan struct{})
 	var wg sync.WaitGroup
-	if env.IsETLReadOnlyMode() {
-		wg.Add(1)
-		go initializeCache(kcc.kubecostConfigMapWatch, &wg, cancel)
-	} else {
-		wg.Add(15)
-		go initializeCache(kcc.kubecostConfigMapWatch, &wg, cancel)
+	if !env.IsETLReadOnlyMode() {
+		wg.Add(14)
 		go initializeCache(kcc.namespaceWatch, &wg, cancel)
 		go initializeCache(kcc.nodeWatch, &wg, cancel)
 		go initializeCache(kcc.podWatch, &wg, cancel)
@@ -168,7 +511,6 @@ func (kcc *KubernetesClusterCache) Run() {
 	go kcc.nodeWatch.Run(1, stopCh)
 	go kcc.podWatch.Run(1, stopCh)
 	go kcc.serviceWatch.Run(1, stopCh)
-	go kcc.kubecostConfigMapWatch.Run(1, stopCh)
 	go kcc.daemonsetsWatch.Run(1, stopCh)
 	go kcc.deploymentsWatch.Run(1, stopCh)
 	go kcc.statefulsetWatch.Run(1, stopCh)
@@ -192,132 +534,128 @@ func (kcc *KubernetesClusterCache) Stop() {
 	kcc.stop = nil
 }
 
-func (kcc *KubernetesClusterCache) GetAllNamespaces() []*v1.Namespace {
-	var namespaces []*v1.Namespace
+func (kcc *KubernetesClusterCache) GetAllNamespaces() []*Namespace {
+	var namespaces []*Namespace
 	items := kcc.namespaceWatch.GetAll()
 	for _, ns := range items {
-		namespaces = append(namespaces, ns.(*v1.Namespace))
+		namespaces = append(namespaces, transformNamespace(ns.(*v1.Namespace)))
 	}
 	return namespaces
 }
 
-func (kcc *KubernetesClusterCache) GetAllNodes() []*v1.Node {
-	var nodes []*v1.Node
+func (kcc *KubernetesClusterCache) GetAllNodes() []*Node {
+	var nodes []*Node
 	items := kcc.nodeWatch.GetAll()
 	for _, node := range items {
-		nodes = append(nodes, node.(*v1.Node))
+		nodes = append(nodes, transformNode(node.(*v1.Node)))
 	}
 	return nodes
 }
 
-func (kcc *KubernetesClusterCache) GetAllPods() []*v1.Pod {
-	var pods []*v1.Pod
+func (kcc *KubernetesClusterCache) GetAllPods() []*Pod {
+	var pods []*Pod
 	items := kcc.podWatch.GetAll()
 	for _, pod := range items {
-		pods = append(pods, pod.(*v1.Pod))
+		pods = append(pods, transformPod(pod.(*v1.Pod)))
 	}
 	return pods
 }
 
-func (kcc *KubernetesClusterCache) GetAllServices() []*v1.Service {
-	var services []*v1.Service
+func (kcc *KubernetesClusterCache) GetAllServices() []*Service {
+	var services []*Service
 	items := kcc.serviceWatch.GetAll()
 	for _, service := range items {
-		services = append(services, service.(*v1.Service))
+		services = append(services, transformService(service.(*v1.Service)))
 	}
 	return services
 }
 
-func (kcc *KubernetesClusterCache) GetAllDaemonSets() []*appsv1.DaemonSet {
-	var daemonsets []*appsv1.DaemonSet
+func (kcc *KubernetesClusterCache) GetAllDaemonSets() []*DaemonSet {
+	var daemonsets []*DaemonSet
 	items := kcc.daemonsetsWatch.GetAll()
 	for _, daemonset := range items {
-		daemonsets = append(daemonsets, daemonset.(*appsv1.DaemonSet))
+		daemonsets = append(daemonsets, transformDaemonSet(daemonset.(*appsv1.DaemonSet)))
 	}
 	return daemonsets
 }
 
-func (kcc *KubernetesClusterCache) GetAllDeployments() []*appsv1.Deployment {
-	var deployments []*appsv1.Deployment
+func (kcc *KubernetesClusterCache) GetAllDeployments() []*Deployment {
+	var deployments []*Deployment
 	items := kcc.deploymentsWatch.GetAll()
 	for _, deployment := range items {
-		deployments = append(deployments, deployment.(*appsv1.Deployment))
+		deployments = append(deployments, transformDeployment(deployment.(*appsv1.Deployment)))
 	}
 	return deployments
 }
 
-func (kcc *KubernetesClusterCache) GetAllStatefulSets() []*appsv1.StatefulSet {
-	var statefulsets []*appsv1.StatefulSet
+func (kcc *KubernetesClusterCache) GetAllStatefulSets() []*StatefulSet {
+	var statefulsets []*StatefulSet
 	items := kcc.statefulsetWatch.GetAll()
 	for _, statefulset := range items {
-		statefulsets = append(statefulsets, statefulset.(*appsv1.StatefulSet))
+		statefulsets = append(statefulsets, transformStatefulSet(statefulset.(*appsv1.StatefulSet)))
 	}
 	return statefulsets
 }
 
-func (kcc *KubernetesClusterCache) GetAllReplicaSets() []*appsv1.ReplicaSet {
-	var replicasets []*appsv1.ReplicaSet
+func (kcc *KubernetesClusterCache) GetAllReplicaSets() []*ReplicaSet {
+	var replicasets []*ReplicaSet
 	items := kcc.replicasetWatch.GetAll()
 	for _, replicaset := range items {
-		replicasets = append(replicasets, replicaset.(*appsv1.ReplicaSet))
+		replicasets = append(replicasets, transformReplicaSet(replicaset.(*appsv1.ReplicaSet)))
 	}
 	return replicasets
 }
 
-func (kcc *KubernetesClusterCache) GetAllPersistentVolumes() []*v1.PersistentVolume {
-	var pvs []*v1.PersistentVolume
+func (kcc *KubernetesClusterCache) GetAllPersistentVolumes() []*PersistentVolume {
+	var pvs []*PersistentVolume
 	items := kcc.pvWatch.GetAll()
 	for _, pv := range items {
-		pvs = append(pvs, pv.(*v1.PersistentVolume))
+		pvs = append(pvs, transformPersistentVolume(pv.(*v1.PersistentVolume)))
 	}
 	return pvs
 }
 
-func (kcc *KubernetesClusterCache) GetAllPersistentVolumeClaims() []*v1.PersistentVolumeClaim {
-	var pvcs []*v1.PersistentVolumeClaim
+func (kcc *KubernetesClusterCache) GetAllPersistentVolumeClaims() []*PersistentVolumeClaim {
+	var pvcs []*PersistentVolumeClaim
 	items := kcc.pvcWatch.GetAll()
 	for _, pvc := range items {
-		pvcs = append(pvcs, pvc.(*v1.PersistentVolumeClaim))
+		pvcs = append(pvcs, transformPersistentVolumeClaim(pvc.(*v1.PersistentVolumeClaim)))
 	}
 	return pvcs
 }
 
-func (kcc *KubernetesClusterCache) GetAllStorageClasses() []*stv1.StorageClass {
-	var storageClasses []*stv1.StorageClass
+func (kcc *KubernetesClusterCache) GetAllStorageClasses() []*StorageClass {
+	var storageClasses []*StorageClass
 	items := kcc.storageClassWatch.GetAll()
 	for _, stc := range items {
-		storageClasses = append(storageClasses, stc.(*stv1.StorageClass))
+		storageClasses = append(storageClasses, transformStorageClass(stc.(*stv1.StorageClass)))
 	}
 	return storageClasses
 }
 
-func (kcc *KubernetesClusterCache) GetAllJobs() []*batchv1.Job {
-	var jobs []*batchv1.Job
+func (kcc *KubernetesClusterCache) GetAllJobs() []*Job {
+	var jobs []*Job
 	items := kcc.jobsWatch.GetAll()
 	for _, job := range items {
-		jobs = append(jobs, job.(*batchv1.Job))
+		jobs = append(jobs, transformJob(job.(*batchv1.Job)))
 	}
 	return jobs
 }
 
-func (kcc *KubernetesClusterCache) GetAllPodDisruptionBudgets() []*policyv1.PodDisruptionBudget {
-	var pdbs []*policyv1.PodDisruptionBudget
+func (kcc *KubernetesClusterCache) GetAllPodDisruptionBudgets() []*PodDisruptionBudget {
+	var pdbs []*PodDisruptionBudget
 	items := kcc.pdbWatch.GetAll()
 	for _, pdb := range items {
-		pdbs = append(pdbs, pdb.(*policyv1.PodDisruptionBudget))
+		pdbs = append(pdbs, transformPodDisruptionBudget(pdb.(*policyv1.PodDisruptionBudget)))
 	}
 	return pdbs
 }
 
-func (kcc *KubernetesClusterCache) GetAllReplicationControllers() []*v1.ReplicationController {
-	var rcs []*v1.ReplicationController
+func (kcc *KubernetesClusterCache) GetAllReplicationControllers() []*ReplicationController {
+	var rcs []*ReplicationController
 	items := kcc.replicationControllerWatch.GetAll()
 	for _, rc := range items {
-		rcs = append(rcs, rc.(*v1.ReplicationController))
+		rcs = append(rcs, transformReplicationController(rc.(*v1.ReplicationController)))
 	}
 	return rcs
 }
-
-func (kcc *KubernetesClusterCache) SetConfigMapUpdateFunc(f func(interface{})) {
-	kcc.kubecostConfigMapWatch.SetUpdateHandler(f)
-}

+ 111 - 0
pkg/clustercache/clustercache2.go

@@ -0,0 +1,111 @@
+package clustercache
+
+import (
+	"context"
+
+	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"
+	"k8s.io/client-go/kubernetes"
+)
+
+type KubernetesClusterCacheV2 struct {
+	namespaceStore             *GenericStore[*v1.Namespace, *Namespace]
+	nodeStore                  *GenericStore[*v1.Node, *Node]
+	podStore                   *GenericStore[*v1.Pod, *Pod]
+	serviceStore               *GenericStore[*v1.Service, *Service]
+	daemonSetStore             *GenericStore[*appsv1.DaemonSet, *DaemonSet]
+	deploymentStore            *GenericStore[*appsv1.Deployment, *Deployment]
+	statefulSetStore           *GenericStore[*appsv1.StatefulSet, *StatefulSet]
+	persistentVolumeStore      *GenericStore[*v1.PersistentVolume, *PersistentVolume]
+	persistentVolumeClaimStore *GenericStore[*v1.PersistentVolumeClaim, *PersistentVolumeClaim]
+	storageClassStore          *GenericStore[*stv1.StorageClass, *StorageClass]
+	jobStore                   *GenericStore[*batchv1.Job, *Job]
+	replicationControllerStore *GenericStore[*v1.ReplicationController, *ReplicationController]
+	replicaSetStore            *GenericStore[*appsv1.ReplicaSet, *ReplicaSet]
+	pdbStore                   *GenericStore[*policyv1.PodDisruptionBudget, *PodDisruptionBudget]
+}
+
+func NewKubernetesClusterCacheV2(clientset kubernetes.Interface) *KubernetesClusterCacheV2 {
+	ctx := context.TODO()
+	return &KubernetesClusterCacheV2{
+		namespaceStore:             CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "namespaces", transformNamespace),
+		nodeStore:                  CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "nodes", transformNode),
+		persistentVolumeClaimStore: CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "persistentvolumeclaims", transformPersistentVolumeClaim),
+		persistentVolumeStore:      CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "persistentvolumes", transformPersistentVolume),
+		podStore:                   CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "pods", transformPod),
+		replicationControllerStore: CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "replicationcontrollers", transformReplicationController),
+		serviceStore:               CreateStoreAndWatch(ctx, clientset.CoreV1().RESTClient(), "services", transformService),
+		daemonSetStore:             CreateStoreAndWatch(ctx, clientset.AppsV1().RESTClient(), "daemonsets", transformDaemonSet),
+		deploymentStore:            CreateStoreAndWatch(ctx, clientset.AppsV1().RESTClient(), "deployments", transformDeployment),
+		replicaSetStore:            CreateStoreAndWatch(ctx, clientset.AppsV1().RESTClient(), "replicasets", transformReplicaSet),
+		statefulSetStore:           CreateStoreAndWatch(ctx, clientset.AppsV1().RESTClient(), "statefulsets", transformStatefulSet),
+		storageClassStore:          CreateStoreAndWatch(ctx, clientset.StorageV1().RESTClient(), "storageclasses", transformStorageClass),
+		jobStore:                   CreateStoreAndWatch(ctx, clientset.BatchV1().RESTClient(), "jobs", transformJob),
+		pdbStore:                   CreateStoreAndWatch(ctx, clientset.PolicyV1().RESTClient(), "poddisruptionbudgets", transformPodDisruptionBudget),
+	}
+}
+
+func (kcc *KubernetesClusterCacheV2) Run() {
+}
+
+func (kcc *KubernetesClusterCacheV2) Stop() {
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllNamespaces() []*Namespace {
+	return kcc.namespaceStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllNodes() []*Node {
+	return kcc.nodeStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllPods() []*Pod {
+	return kcc.podStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllServices() []*Service {
+	return kcc.serviceStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllDaemonSets() []*DaemonSet {
+	return kcc.daemonSetStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllDeployments() []*Deployment {
+	return kcc.deploymentStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllStatefulSets() []*StatefulSet {
+	return kcc.statefulSetStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllPersistentVolumes() []*PersistentVolume {
+	return kcc.persistentVolumeStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllPersistentVolumeClaims() []*PersistentVolumeClaim {
+	return kcc.persistentVolumeClaimStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllStorageClasses() []*StorageClass {
+	return kcc.storageClassStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllJobs() []*Job {
+	return kcc.jobStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllReplicationControllers() []*ReplicationController {
+	return kcc.replicationControllerStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllReplicaSets() []*ReplicaSet {
+	return kcc.replicaSetStore.GetAll()
+}
+
+func (kcc *KubernetesClusterCacheV2) GetAllPodDisruptionBudgets() []*PodDisruptionBudget {
+	return kcc.pdbStore.GetAll()
+}

+ 14 - 20
pkg/clustercache/clusterexporter.go

@@ -7,30 +7,24 @@ import (
 	"github.com/opencost/opencost/core/pkg/util/atomic"
 	"github.com/opencost/opencost/core/pkg/util/json"
 	"github.com/opencost/opencost/pkg/config"
-
-	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"
 )
 
 // clusterEncoding is used to represent the cluster objects in the encoded states.
 type clusterEncoding struct {
-	Namespaces             []*v1.Namespace                 `json:"namespaces,omitempty"`
-	Nodes                  []*v1.Node                      `json:"nodes,omitempty"`
-	Pods                   []*v1.Pod                       `json:"pods,omitempty"`
-	Services               []*v1.Service                   `json:"services,omitempty"`
-	DaemonSets             []*appsv1.DaemonSet             `json:"daemonSets,omitempty"`
-	Deployments            []*appsv1.Deployment            `json:"deployments,omitempty"`
-	StatefulSets           []*appsv1.StatefulSet           `json:"statefulSets,omitempty"`
-	ReplicaSets            []*appsv1.ReplicaSet            `json:"replicaSets,omitempty"`
-	PersistentVolumes      []*v1.PersistentVolume          `json:"persistentVolumes,omitempty"`
-	PersistentVolumeClaims []*v1.PersistentVolumeClaim     `json:"persistentVolumeClaims,omitempty"`
-	StorageClasses         []*stv1.StorageClass            `json:"storageClasses,omitempty"`
-	Jobs                   []*batchv1.Job                  `json:"jobs,omitempty"`
-	PodDisruptionBudgets   []*policyv1.PodDisruptionBudget `json:"podDisruptionBudgets,omitempty"`
-	ReplicationControllers []*v1.ReplicationController     `json:"replicationController,omitempty"`
+	Namespaces             []*Namespace             `json:"namespaces,omitempty"`
+	Nodes                  []*Node                  `json:"nodes,omitempty"`
+	Pods                   []*Pod                   `json:"pods,omitempty"`
+	Services               []*Service               `json:"services,omitempty"`
+	DaemonSets             []*DaemonSet             `json:"daemonSets,omitempty"`
+	Deployments            []*Deployment            `json:"deployments,omitempty"`
+	StatefulSets           []*StatefulSet           `json:"statefulSets,omitempty"`
+	ReplicaSets            []*ReplicaSet            `json:"replicaSets,omitempty"`
+	PersistentVolumes      []*PersistentVolume      `json:"persistentVolumes,omitempty"`
+	PersistentVolumeClaims []*PersistentVolumeClaim `json:"persistentVolumeClaims,omitempty"`
+	StorageClasses         []*StorageClass          `json:"storageClasses,omitempty"`
+	Jobs                   []*Job                   `json:"jobs,omitempty"`
+	PodDisruptionBudgets   []*PodDisruptionBudget   `json:"podDisruptionBudgets,omitempty"`
+	ReplicationControllers []*ReplicationController `json:"replicationController,omitempty"`
 }
 
 // ClusterExporter manages and runs an file export process which dumps the local kubernetes cluster to a target location.

+ 29 - 138
pkg/clustercache/clusterimporter.go

@@ -6,11 +6,7 @@ import (
 	"github.com/opencost/opencost/core/pkg/log"
 	"github.com/opencost/opencost/core/pkg/util/json"
 	"github.com/opencost/opencost/pkg/config"
-	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"
+	"golang.org/x/exp/slices"
 )
 
 // ClusterImporter is an implementation of ClusterCache which leverages a backing configuration file
@@ -91,217 +87,112 @@ func (ci *ClusterImporter) Stop() {
 }
 
 // GetAllNamespaces returns all the cached namespaces
-func (ci *ClusterImporter) GetAllNamespaces() []*v1.Namespace {
+func (ci *ClusterImporter) GetAllNamespaces() []*Namespace {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	namespaces := ci.data.Namespaces
-	cloneList := make([]*v1.Namespace, 0, len(namespaces))
-	for _, v := range namespaces {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.Namespaces)
 }
 
 // GetAllNodes returns all the cached nodes
-func (ci *ClusterImporter) GetAllNodes() []*v1.Node {
+func (ci *ClusterImporter) GetAllNodes() []*Node {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	nodes := ci.data.Nodes
-	cloneList := make([]*v1.Node, 0, len(nodes))
-	for _, v := range nodes {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.Nodes)
 }
 
 // GetAllPods returns all the cached pods
-func (ci *ClusterImporter) GetAllPods() []*v1.Pod {
+func (ci *ClusterImporter) GetAllPods() []*Pod {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	pods := ci.data.Pods
-	cloneList := make([]*v1.Pod, 0, len(pods))
-	for _, v := range pods {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.Pods)
 }
 
 // GetAllServices returns all the cached services
-func (ci *ClusterImporter) GetAllServices() []*v1.Service {
+func (ci *ClusterImporter) GetAllServices() []*Service {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	services := ci.data.Services
-	cloneList := make([]*v1.Service, 0, len(services))
-	for _, v := range services {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.Services)
 }
 
 // GetAllDaemonSets returns all the cached DaemonSets
-func (ci *ClusterImporter) GetAllDaemonSets() []*appsv1.DaemonSet {
+func (ci *ClusterImporter) GetAllDaemonSets() []*DaemonSet {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	daemonSets := ci.data.DaemonSets
-	cloneList := make([]*appsv1.DaemonSet, 0, len(daemonSets))
-	for _, v := range daemonSets {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.DaemonSets)
 }
 
 // GetAllDeployments returns all the cached deployments
-func (ci *ClusterImporter) GetAllDeployments() []*appsv1.Deployment {
+func (ci *ClusterImporter) GetAllDeployments() []*Deployment {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	deployments := ci.data.Deployments
-	cloneList := make([]*appsv1.Deployment, 0, len(deployments))
-	for _, v := range deployments {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.Deployments)
 }
 
 // GetAllStatfulSets returns all the cached StatefulSets
-func (ci *ClusterImporter) GetAllStatefulSets() []*appsv1.StatefulSet {
+func (ci *ClusterImporter) GetAllStatefulSets() []*StatefulSet {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	statefulSets := ci.data.StatefulSets
-	cloneList := make([]*appsv1.StatefulSet, 0, len(statefulSets))
-	for _, v := range statefulSets {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.StatefulSets)
 }
 
 // GetAllReplicaSets returns all the cached ReplicaSets
-func (ci *ClusterImporter) GetAllReplicaSets() []*appsv1.ReplicaSet {
+func (ci *ClusterImporter) GetAllReplicaSets() []*ReplicaSet {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	replicaSets := ci.data.ReplicaSets
-	cloneList := make([]*appsv1.ReplicaSet, 0, len(replicaSets))
-	for _, v := range replicaSets {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.ReplicaSets)
 }
 
 // GetAllPersistentVolumes returns all the cached persistent volumes
-func (ci *ClusterImporter) GetAllPersistentVolumes() []*v1.PersistentVolume {
+func (ci *ClusterImporter) GetAllPersistentVolumes() []*PersistentVolume {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	pvs := ci.data.PersistentVolumes
-	cloneList := make([]*v1.PersistentVolume, 0, len(pvs))
-	for _, v := range pvs {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.PersistentVolumes)
 }
 
 // GetAllPersistentVolumeClaims returns all the cached persistent volume claims
-func (ci *ClusterImporter) GetAllPersistentVolumeClaims() []*v1.PersistentVolumeClaim {
+func (ci *ClusterImporter) GetAllPersistentVolumeClaims() []*PersistentVolumeClaim {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	pvcs := ci.data.PersistentVolumeClaims
-	cloneList := make([]*v1.PersistentVolumeClaim, 0, len(pvcs))
-	for _, v := range pvcs {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.PersistentVolumeClaims)
 }
 
 // GetAllStorageClasses returns all the cached storage classes
-func (ci *ClusterImporter) GetAllStorageClasses() []*stv1.StorageClass {
+func (ci *ClusterImporter) GetAllStorageClasses() []*StorageClass {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	storageClasses := ci.data.StorageClasses
-	cloneList := make([]*stv1.StorageClass, 0, len(storageClasses))
-	for _, v := range storageClasses {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.StorageClasses)
 }
 
 // GetAllJobs returns all the cached jobs
-func (ci *ClusterImporter) GetAllJobs() []*batchv1.Job {
+func (ci *ClusterImporter) GetAllJobs() []*Job {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	jobs := ci.data.Jobs
-	cloneList := make([]*batchv1.Job, 0, len(jobs))
-	for _, v := range jobs {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.Jobs)
 }
 
 // GetAllPodDisruptionBudgets returns all cached pod disruption budgets
-func (ci *ClusterImporter) GetAllPodDisruptionBudgets() []*policyv1.PodDisruptionBudget {
+func (ci *ClusterImporter) GetAllPodDisruptionBudgets() []*PodDisruptionBudget {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	pdbs := ci.data.PodDisruptionBudgets
-	cloneList := make([]*policyv1.PodDisruptionBudget, 0, len(pdbs))
-	for _, v := range pdbs {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
+	return slices.Clone(ci.data.PodDisruptionBudgets)
 }
 
-func (ci *ClusterImporter) GetAllReplicationControllers() []*v1.ReplicationController {
+func (ci *ClusterImporter) GetAllReplicationControllers() []*ReplicationController {
 	ci.dataLock.Lock()
 	defer ci.dataLock.Unlock()
 
-	// Deep copy here to avoid callers from corrupting the cache
-	// This also mimics the behavior of the default cluster cache impl.
-	rcs := ci.data.ReplicationControllers
-	cloneList := make([]*v1.ReplicationController, 0, len(rcs))
-	for _, v := range rcs {
-		cloneList = append(cloneList, v.DeepCopy())
-	}
-	return cloneList
-}
-
-// SetConfigMapUpdateFunc sets the configmap update function
-func (ci *ClusterImporter) SetConfigMapUpdateFunc(_ func(interface{})) {
-	// TODO: (bolt) This function is still a bit strange to me for the ClusterCache interface.
-	// TODO: (bolt) no-op for now.
-	log.Warnf("SetConfigMapUpdateFunc is disabled for imported cluster data.")
+	return slices.Clone(ci.data.ReplicationControllers)
 }

+ 111 - 0
pkg/clustercache/store.go

@@ -0,0 +1,111 @@
+package clustercache
+
+import (
+	"context"
+	"sync"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/cache"
+)
+
+// GenericStore is a generic store implementation. It converts objects to a different type using a transform function.
+// The main purpose is to reduce a memory footprint by storing only the necessary data.
+type GenericStore[Input UIDGetter, Output any] struct {
+	mutex         sync.RWMutex
+	items         map[types.UID]Output
+	transformFunc func(input Input) Output
+}
+
+// NewGenericStore creates a new instance of GenericStore.
+func NewGenericStore[Input UIDGetter, Output any](transformFunc func(input Input) Output) *GenericStore[Input, Output] {
+	return &GenericStore[Input, Output]{
+		items:         make(map[types.UID]Output),
+		transformFunc: transformFunc,
+	}
+}
+
+type UIDGetter interface {
+	GetUID() types.UID
+}
+
+func CreateStoreAndWatch[Input UIDGetter, Output any](
+	ctx context.Context,
+	restClient rest.Interface,
+	resource string,
+	transformFunc func(input Input) Output,
+) *GenericStore[Input, Output] {
+	lw := cache.NewListWatchFromClient(restClient, resource, v1.NamespaceAll, fields.Everything())
+	store := NewGenericStore(transformFunc)
+	var zeroValue Input
+	reflector := cache.NewReflector(lw, zeroValue, store, 0)
+	go reflector.Run(ctx.Done())
+	return store
+}
+
+// Add inserts an object into the store.
+func (s *GenericStore[Input, Output]) Add(obj any) error {
+	return s.Update(obj)
+}
+
+// Update updates the existing entry in the store.
+func (s *GenericStore[Input, Output]) Update(obj any) error {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	item := obj.(Input)
+	s.items[item.GetUID()] = s.transformFunc(item)
+
+	return nil
+}
+
+// Delete removes an object from the store.
+func (s *GenericStore[Input, Output]) Delete(obj any) error {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	item := obj.(Input)
+	delete(s.items, item.GetUID())
+
+	return nil
+}
+
+// GetAll returns all stored objects.
+func (s *GenericStore[Input, Output]) GetAll() []Output {
+	s.mutex.RLock()
+	defer s.mutex.RUnlock()
+	allItems := make([]Output, 0, len(s.items))
+	for _, item := range s.items {
+		allItems = append(allItems, item)
+	}
+	return allItems
+}
+
+// Replace replaces the current list of items in the store.
+func (s *GenericStore[Input, Output]) Replace(list []any, _ string) error {
+	s.mutex.Lock()
+	s.items = make(map[types.UID]Output, len(list))
+	s.mutex.Unlock()
+
+	for _, o := range list {
+		err := s.Add(o)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// Stubs to satisfy the cache.Store interface
+func (s *GenericStore[Input, Output]) List() []interface{} { return nil }
+func (s *GenericStore[Input, Output]) ListKeys() []string  { return nil }
+func (s *GenericStore[Input, Output]) Get(_ interface{}) (item interface{}, exists bool, err error) {
+	return nil, false, nil
+}
+func (s *GenericStore[Input, Output]) GetByKey(_ string) (item interface{}, exists bool, err error) {
+	return nil, false, nil
+}
+func (s *GenericStore[Input, Output]) Resync() error { return nil }

+ 4 - 21
pkg/cmd/agent/agent.go

@@ -9,7 +9,7 @@ import (
 
 	"github.com/opencost/opencost/core/pkg/clusters"
 	"github.com/opencost/opencost/core/pkg/log"
-	"github.com/opencost/opencost/core/pkg/util/watcher"
+	"github.com/opencost/opencost/pkg/util/watcher"
 
 	"github.com/opencost/opencost/core/pkg/version"
 	"github.com/opencost/opencost/pkg/cloud/provider"
@@ -25,7 +25,6 @@ import (
 	prometheus "github.com/prometheus/client_golang/api"
 	prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/rs/cors"
 	"k8s.io/client-go/kubernetes"
@@ -129,9 +128,6 @@ func newPrometheusClient() (prometheus.Client, error) {
 
 func Execute(opts *AgentOpts) error {
 	log.Infof("Starting Kubecost Agent version %s", version.FriendlyVersion())
-
-	configWatchers := watcher.NewConfigMapWatchers()
-
 	scrapeInterval := env.GetKubecostScrapeInterval()
 	promCli, err := newPrometheusClient()
 	if err != nil {
@@ -168,23 +164,10 @@ func Execute(opts *AgentOpts) error {
 	}
 
 	// Append the pricing config watcher
-	configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
-	watchConfigFunc := configWatchers.ToWatchFunc()
-	watchedConfigs := configWatchers.GetWatchedConfigs()
-
 	kubecostNamespace := env.GetKubecostNamespace()
-
-	// We need an initial invocation because the init of the cache has happened before we had access to the provider.
-	for _, cw := range watchedConfigs {
-		configs, err := k8sClient.CoreV1().ConfigMaps(kubecostNamespace).Get(context.Background(), cw, metav1.GetOptions{})
-		if err != nil {
-			log.Infof("No %s configmap found at install time, using existing configs: %s", cw, err.Error())
-		} else {
-			watchConfigFunc(configs)
-		}
-	}
-
-	clusterCache.SetConfigMapUpdateFunc(watchConfigFunc)
+	configWatchers := watcher.NewConfigMapWatchers(k8sClient, kubecostNamespace)
+	configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
+	configWatchers.Watch()
 
 	configPrefix := env.GetConfigPathWithDefault(env.DefaultConfigMountPath)
 

+ 4 - 4
pkg/costmodel/containerkeys.go

@@ -5,8 +5,8 @@ import (
 	"strings"
 
 	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
-	v1 "k8s.io/api/core/v1"
 )
 
 var (
@@ -135,9 +135,9 @@ func NewContainerMetricFromValues(ns, podName, containerName, nodeName, clusterI
 
 // NewContainerMetricsFromPod creates a slice of ContainerMetric instances for each container in the
 // provided Pod.
-func NewContainerMetricsFromPod(pod *v1.Pod, clusterID string) ([]*ContainerMetric, error) {
-	podName := pod.GetObjectMeta().GetName()
-	ns := pod.GetObjectMeta().GetNamespace()
+func NewContainerMetricsFromPod(pod *clustercache.Pod, clusterID string) ([]*ContainerMetric, error) {
+	podName := pod.Name
+	ns := pod.Namespace
 	node := pod.Spec.NodeName
 
 	var cs []*ContainerMetric

+ 53 - 53
pkg/costmodel/costmodel.go

@@ -358,7 +358,7 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, cp costAnalyze
 	for key := range CPUUsedMap {
 		containers[key] = true
 	}
-	currentContainers := make(map[string]v1.Pod)
+	currentContainers := make(map[string]clustercache.Pod)
 	for _, pod := range podlist {
 		if pod.Status.Phase != v1.PodRunning {
 			continue
@@ -382,11 +382,11 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, cp costAnalyze
 		// deleted so we have usage information but not request information. In that case,
 		// we return partial data for CPU and RAM: only usage and not requests.
 		if pod, ok := currentContainers[key]; ok {
-			podName := pod.GetObjectMeta().GetName()
-			ns := pod.GetObjectMeta().GetNamespace()
+			podName := pod.Name
+			ns := pod.Namespace
 
 			nsLabels := namespaceLabelsMapping[ns+","+clusterID]
-			podLabels := pod.GetObjectMeta().GetLabels()
+			podLabels := pod.Labels
 			if podLabels == nil {
 				podLabels = make(map[string]string)
 			}
@@ -398,7 +398,7 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, cp costAnalyze
 			}
 
 			nsAnnotations := namespaceAnnotationsMapping[ns+","+clusterID]
-			podAnnotations := pod.GetObjectMeta().GetAnnotations()
+			podAnnotations := pod.Annotations
 			if podAnnotations == nil {
 				podAnnotations = make(map[string]string)
 			}
@@ -419,7 +419,7 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, cp costAnalyze
 
 			var podDeployments []string
 			if _, ok := podDeploymentsMapping[nsKey]; ok {
-				if ds, ok := podDeploymentsMapping[nsKey][pod.GetObjectMeta().GetName()]; ok {
+				if ds, ok := podDeploymentsMapping[nsKey][pod.Name]; ok {
 					podDeployments = ds
 				} else {
 					podDeployments = []string{}
@@ -454,7 +454,7 @@ func (cm *CostModel) ComputeCostData(cli prometheusClient.Client, cp costAnalyze
 
 			var podServices []string
 			if _, ok := podServicesMapping[nsKey]; ok {
-				if svcs, ok := podServicesMapping[nsKey][pod.GetObjectMeta().GetName()]; ok {
+				if svcs, ok := podServicesMapping[nsKey][pod.Name]; ok {
 					podServices = svcs
 				} else {
 					podServices = []string{}
@@ -904,8 +904,8 @@ func addPVData(cache clustercache.ClusterCache, pvClaimMapping map[string]*Persi
 	storageClassMap := make(map[string]map[string]string)
 	for _, storageClass := range storageClasses {
 		params := storageClass.Parameters
-		storageClassMap[storageClass.ObjectMeta.Name] = params
-		if storageClass.GetAnnotations()["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.GetAnnotations()["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
+		storageClassMap[storageClass.Name] = params
+		if storageClass.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.Annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
 			storageClassMap["default"] = params
 			storageClassMap[""] = params
 		}
@@ -951,7 +951,7 @@ func addPVData(cache clustercache.ClusterCache, pvClaimMapping map[string]*Persi
 	return nil
 }
 
-func GetPVCost(pv *costAnalyzerCloud.PV, kpv *v1.PersistentVolume, cp costAnalyzerCloud.Provider, defaultRegion string) error {
+func GetPVCost(pv *costAnalyzerCloud.PV, kpv *clustercache.PersistentVolume, cp costAnalyzerCloud.Provider, defaultRegion string) error {
 	cfg, err := cp.GetConfig()
 	if err != nil {
 		return err
@@ -993,9 +993,9 @@ func (cm *CostModel) GetNodeCost(cp costAnalyzerCloud.Provider) (map[string]*cos
 		PricingTypeCounts: make(map[costAnalyzerCloud.PricingType]int),
 	}
 	for _, n := range nodeList {
-		name := n.GetObjectMeta().GetName()
-		nodeLabels := n.GetObjectMeta().GetLabels()
-		nodeLabels["providerID"] = n.Spec.ProviderID
+		name := n.Name
+		nodeLabels := n.Labels
+		nodeLabels["providerID"] = n.SpecProviderID
 
 		pmd.TotalNodes++
 
@@ -1034,7 +1034,7 @@ func (cm *CostModel) GetNodeCost(cp costAnalyzerCloud.Provider) (map[string]*cos
 			arch, _ := util.GetArchType(n.Labels)
 			newCnode.ArchType = arch
 		}
-		newCnode.ProviderID = n.Spec.ProviderID
+		newCnode.ProviderID = n.SpecProviderID
 
 		var cpu float64
 		if newCnode.VCPU == "" {
@@ -1349,15 +1349,15 @@ func (cm *CostModel) GetLBCost(cp costAnalyzerCloud.Provider) (map[serviceKey]*c
 	loadBalancerMap := make(map[serviceKey]*costAnalyzerCloud.LoadBalancer)
 
 	for _, service := range servicesList {
-		namespace := service.GetObjectMeta().GetNamespace()
-		name := service.GetObjectMeta().GetName()
+		namespace := service.Namespace
+		name := service.Name
 		key := serviceKey{
 			Cluster:   env.GetClusterID(),
 			Namespace: namespace,
 			Service:   name,
 		}
 
-		if service.Spec.Type == "LoadBalancer" {
+		if service.Type == "LoadBalancer" {
 			loadBalancer, err := cp.LoadBalancerPricing()
 			if err != nil {
 				return nil, err
@@ -1378,28 +1378,28 @@ func (cm *CostModel) GetLBCost(cp costAnalyzerCloud.Provider) (map[serviceKey]*c
 	return loadBalancerMap, nil
 }
 
-func getPodServices(cache clustercache.ClusterCache, podList []*v1.Pod, clusterID string) (map[string]map[string][]string, error) {
+func getPodServices(cache clustercache.ClusterCache, podList []*clustercache.Pod, clusterID string) (map[string]map[string][]string, error) {
 	servicesList := cache.GetAllServices()
 	podServicesMapping := make(map[string]map[string][]string)
 	for _, service := range servicesList {
-		namespace := service.GetObjectMeta().GetNamespace()
-		name := service.GetObjectMeta().GetName()
+		namespace := service.Namespace
+		name := service.Name
 		key := namespace + "," + clusterID
 		if _, ok := podServicesMapping[key]; !ok {
 			podServicesMapping[key] = make(map[string][]string)
 		}
 		s := labels.Nothing()
-		if service.Spec.Selector != nil && len(service.Spec.Selector) > 0 {
-			s = labels.Set(service.Spec.Selector).AsSelectorPreValidated()
+		if service.SpecSelector != nil && len(service.SpecSelector) > 0 {
+			s = labels.Set(service.SpecSelector).AsSelectorPreValidated()
 		}
 		for _, pod := range podList {
-			labelSet := labels.Set(pod.GetObjectMeta().GetLabels())
-			if s.Matches(labelSet) && pod.GetObjectMeta().GetNamespace() == namespace {
-				services, ok := podServicesMapping[key][pod.GetObjectMeta().GetName()]
+			labelSet := labels.Set(pod.Labels)
+			if s.Matches(labelSet) && pod.Namespace == namespace {
+				services, ok := podServicesMapping[key][pod.Name]
 				if ok {
-					podServicesMapping[key][pod.GetObjectMeta().GetName()] = append(services, name)
+					podServicesMapping[key][pod.Name] = append(services, name)
 				} else {
-					podServicesMapping[key][pod.GetObjectMeta().GetName()] = []string{name}
+					podServicesMapping[key][pod.Name] = []string{name}
 				}
 			}
 		}
@@ -1407,29 +1407,29 @@ func getPodServices(cache clustercache.ClusterCache, podList []*v1.Pod, clusterI
 	return podServicesMapping, nil
 }
 
-func getPodStatefulsets(cache clustercache.ClusterCache, podList []*v1.Pod, clusterID string) (map[string]map[string][]string, error) {
+func getPodStatefulsets(cache clustercache.ClusterCache, podList []*clustercache.Pod, clusterID string) (map[string]map[string][]string, error) {
 	ssList := cache.GetAllStatefulSets()
 	podSSMapping := make(map[string]map[string][]string) // namespace: podName: [deploymentNames]
 	for _, ss := range ssList {
-		namespace := ss.GetObjectMeta().GetNamespace()
-		name := ss.GetObjectMeta().GetName()
+		namespace := ss.Namespace
+		name := ss.Name
 
 		key := namespace + "," + clusterID
 		if _, ok := podSSMapping[key]; !ok {
 			podSSMapping[key] = make(map[string][]string)
 		}
-		s, err := metav1.LabelSelectorAsSelector(ss.Spec.Selector)
+		s, err := metav1.LabelSelectorAsSelector(ss.SpecSelector)
 		if err != nil {
 			log.Errorf("Error doing deployment label conversion: " + err.Error())
 		}
 		for _, pod := range podList {
-			labelSet := labels.Set(pod.GetObjectMeta().GetLabels())
-			if s.Matches(labelSet) && pod.GetObjectMeta().GetNamespace() == namespace {
-				sss, ok := podSSMapping[key][pod.GetObjectMeta().GetName()]
+			labelSet := labels.Set(pod.Labels)
+			if s.Matches(labelSet) && pod.Namespace == namespace {
+				sss, ok := podSSMapping[key][pod.Name]
 				if ok {
-					podSSMapping[key][pod.GetObjectMeta().GetName()] = append(sss, name)
+					podSSMapping[key][pod.Name] = append(sss, name)
 				} else {
-					podSSMapping[key][pod.GetObjectMeta().GetName()] = []string{name}
+					podSSMapping[key][pod.Name] = []string{name}
 				}
 			}
 		}
@@ -1438,29 +1438,29 @@ func getPodStatefulsets(cache clustercache.ClusterCache, podList []*v1.Pod, clus
 
 }
 
-func getPodDeployments(cache clustercache.ClusterCache, podList []*v1.Pod, clusterID string) (map[string]map[string][]string, error) {
+func getPodDeployments(cache clustercache.ClusterCache, podList []*clustercache.Pod, clusterID string) (map[string]map[string][]string, error) {
 	deploymentsList := cache.GetAllDeployments()
 	podDeploymentsMapping := make(map[string]map[string][]string) // namespace: podName: [deploymentNames]
 	for _, deployment := range deploymentsList {
-		namespace := deployment.GetObjectMeta().GetNamespace()
-		name := deployment.GetObjectMeta().GetName()
+		namespace := deployment.Namespace
+		name := deployment.Name
 
 		key := namespace + "," + clusterID
 		if _, ok := podDeploymentsMapping[key]; !ok {
 			podDeploymentsMapping[key] = make(map[string][]string)
 		}
-		s, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
+		s, err := metav1.LabelSelectorAsSelector(deployment.SpecSelector)
 		if err != nil {
 			log.Errorf("Error doing deployment label conversion: " + err.Error())
 		}
 		for _, pod := range podList {
-			labelSet := labels.Set(pod.GetObjectMeta().GetLabels())
-			if s.Matches(labelSet) && pod.GetObjectMeta().GetNamespace() == namespace {
-				deployments, ok := podDeploymentsMapping[key][pod.GetObjectMeta().GetName()]
+			labelSet := labels.Set(pod.Labels)
+			if s.Matches(labelSet) && pod.Namespace == namespace {
+				deployments, ok := podDeploymentsMapping[key][pod.Name]
 				if ok {
-					podDeploymentsMapping[key][pod.GetObjectMeta().GetName()] = append(deployments, name)
+					podDeploymentsMapping[key][pod.Name] = append(deployments, name)
 				} else {
-					podDeploymentsMapping[key][pod.GetObjectMeta().GetName()] = []string{name}
+					podDeploymentsMapping[key][pod.Name] = []string{name}
 				}
 			}
 		}
@@ -2300,8 +2300,8 @@ func getNamespaceAnnotations(cache clustercache.ClusterCache, clusterID string)
 	return nsToAnnotations, nil
 }
 
-func getDaemonsetsOfPod(pod v1.Pod) []string {
-	for _, ownerReference := range pod.ObjectMeta.OwnerReferences {
+func getDaemonsetsOfPod(pod clustercache.Pod) []string {
+	for _, ownerReference := range pod.OwnerReferences {
 		if ownerReference.Kind == "DaemonSet" {
 			return []string{ownerReference.Name}
 		}
@@ -2309,8 +2309,8 @@ func getDaemonsetsOfPod(pod v1.Pod) []string {
 	return []string{}
 }
 
-func getJobsOfPod(pod v1.Pod) []string {
-	for _, ownerReference := range pod.ObjectMeta.OwnerReferences {
+func getJobsOfPod(pod clustercache.Pod) []string {
+	for _, ownerReference := range pod.OwnerReferences {
 		if ownerReference.Kind == "Job" {
 			return []string{ownerReference.Name}
 		}
@@ -2318,8 +2318,8 @@ func getJobsOfPod(pod v1.Pod) []string {
 	return []string{}
 }
 
-func getStatefulSetsOfPod(pod v1.Pod) []string {
-	for _, ownerReference := range pod.ObjectMeta.OwnerReferences {
+func getStatefulSetsOfPod(pod clustercache.Pod) []string {
+	for _, ownerReference := range pod.OwnerReferences {
 		if ownerReference.Kind == "StatefulSet" {
 			return []string{ownerReference.Name}
 		}
@@ -2330,7 +2330,7 @@ func getStatefulSetsOfPod(pod v1.Pod) []string {
 // getGPUCount reads the node's Status and Labels (via the k8s API) to identify
 // the number of GPUs and vGPUs are equipped on the node. If unable to identify
 // a GPU count, it will return -1.
-func getGPUCount(cache clustercache.ClusterCache, n *v1.Node) (float64, float64, error) {
+func getGPUCount(cache clustercache.ClusterCache, n *clustercache.Node) (float64, float64, error) {
 	g, hasGpu := n.Status.Capacity["nvidia.com/gpu"]
 	_, hasReplicas := n.Labels["nvidia.com/gpu.replicas"]
 
@@ -2393,7 +2393,7 @@ func getAllocatableVGPUs(cache clustercache.ClusterCache) (float64, error) {
 	daemonsets := cache.GetAllDaemonSets()
 	vgpuCount := 0.0
 	for _, ds := range daemonsets {
-		dsContainerList := &ds.Spec.Template.Spec.Containers
+		dsContainerList := &ds.SpecContainers
 		for _, ctnr := range *dsContainerList {
 			if ctnr.Args != nil {
 				for _, arg := range ctnr.Args {

+ 12 - 16
pkg/costmodel/costmodel_test.go

@@ -4,23 +4,23 @@ import (
 	"testing"
 
 	"github.com/opencost/opencost/core/pkg/util"
+	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestGetGPUCount(t *testing.T) {
 	tests := []struct {
 		name          string
-		node          *v1.Node
+		node          *clustercache.Node
 		expectedGPU   float64
 		expectedVGPU  float64
 		expectedError bool
 	}{
 		{
 			name: "Standard NVIDIA GPU",
-			node: &v1.Node{
+			node: &clustercache.Node{
 				Status: v1.NodeStatus{
 					Capacity: v1.ResourceList{
 						"nvidia.com/gpu": resource.MustParse("2"),
@@ -32,12 +32,10 @@ func TestGetGPUCount(t *testing.T) {
 		},
 		{
 			name: "NVIDIA GPU with GFD - renameByDefault=true",
-			node: &v1.Node{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						"nvidia.com/gpu.replicas": "4",
-						"nvidia.com/gpu.count":    "1",
-					},
+			node: &clustercache.Node{
+				Labels: map[string]string{
+					"nvidia.com/gpu.replicas": "4",
+					"nvidia.com/gpu.count":    "1",
 				},
 				Status: v1.NodeStatus{
 					Capacity: v1.ResourceList{
@@ -50,12 +48,10 @@ func TestGetGPUCount(t *testing.T) {
 		},
 		{
 			name: "NVIDIA GPU with GFD - renameByDefault=false",
-			node: &v1.Node{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						"nvidia.com/gpu.replicas": "4",
-						"nvidia.com/gpu.count":    "1",
-					},
+			node: &clustercache.Node{
+				Labels: map[string]string{
+					"nvidia.com/gpu.replicas": "4",
+					"nvidia.com/gpu.count":    "1",
 				},
 				Status: v1.NodeStatus{
 					Capacity: v1.ResourceList{
@@ -68,7 +64,7 @@ func TestGetGPUCount(t *testing.T) {
 		},
 		{
 			name: "No GPU",
-			node: &v1.Node{
+			node: &clustercache.Node{
 				Status: v1.NodeStatus{
 					Capacity: v1.ResourceList{},
 				},

+ 2 - 2
pkg/costmodel/metrics.go

@@ -650,8 +650,8 @@ func (cmme *CostModelMetricsEmitter) Start() bool {
 			storageClassMap := make(map[string]map[string]string)
 			for _, storageClass := range storageClasses {
 				params := storageClass.Parameters
-				storageClassMap[storageClass.ObjectMeta.Name] = params
-				if storageClass.GetAnnotations()["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.GetAnnotations()["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
+				storageClassMap[storageClass.Name] = params
+				if storageClass.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" || storageClass.Annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true" {
 					storageClassMap["default"] = params
 					storageClassMap[""] = params
 				}

+ 7 - 335
pkg/costmodel/router.go

@@ -4,12 +4,10 @@ import (
 	"context"
 	"encoding/base64"
 	"fmt"
-	"io"
 	"net/http"
 	"os"
 	"path"
 	"reflect"
-	"regexp"
 	"strconv"
 	"strings"
 	"sync"
@@ -19,7 +17,6 @@ import (
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/util/httputil"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
-	"github.com/opencost/opencost/core/pkg/util/watcher"
 	"github.com/opencost/opencost/core/pkg/version"
 	"github.com/opencost/opencost/pkg/cloud/aws"
 	cloudconfig "github.com/opencost/opencost/pkg/cloud/config"
@@ -32,8 +29,7 @@ import (
 	"github.com/opencost/opencost/pkg/kubeconfig"
 	"github.com/opencost/opencost/pkg/metrics"
 	"github.com/opencost/opencost/pkg/services"
-	"github.com/spf13/viper"
-	v1 "k8s.io/api/core/v1"
+	"github.com/opencost/opencost/pkg/util/watcher"
 
 	"github.com/julienschmidt/httprouter"
 
@@ -53,7 +49,6 @@ import (
 	"github.com/opencost/opencost/pkg/thanos"
 	prometheus "github.com/prometheus/client_golang/api"
 	prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
-	appsv1 "k8s.io/api/apps/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/patrickmn/go-cache"
@@ -72,15 +67,11 @@ const (
 	CustomPricingSetting = "CustomPricing"
 	DiscountSetting      = "Discount"
 	epRules              = apiPrefix + "/rules"
-	LogSeparator         = "+-------------------------------------------------------------------------------------"
 )
 
 var (
 	// gitCommit is set by the build system
 	gitCommit string
-
-	// ANSIRegex matches ANSI escape and colors https://en.wikipedia.org/wiki/ANSI_escape_code
-	ANSIRegex = regexp.MustCompile("\x1b\\[[0-9;]*m")
 )
 
 // Accesses defines a singleton application instance, providing access to
@@ -930,182 +921,6 @@ func (a *Accesses) GetPrometheusMetrics(w http.ResponseWriter, _ *http.Request,
 	w.Write(WrapData(result, nil))
 }
 
-func (a *Accesses) GetAllPersistentVolumes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	pvList := a.ClusterCache.GetAllPersistentVolumes()
-
-	body, err := json.Marshal(wrapAsObjectItems(pvList))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding persistent volumes: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-
-}
-
-func (a *Accesses) GetAllDeployments(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	qp := httputil.NewQueryParams(r.URL.Query())
-
-	namespace := qp.Get("namespace", "")
-
-	deploymentsList := a.ClusterCache.GetAllDeployments()
-
-	// filter for provided namespace
-	var deployments []*appsv1.Deployment
-	if namespace == "" {
-		deployments = deploymentsList
-	} else {
-		deployments = []*appsv1.Deployment{}
-
-		for _, d := range deploymentsList {
-			if d.Namespace == namespace {
-				deployments = append(deployments, d)
-			}
-		}
-	}
-
-	body, err := json.Marshal(wrapAsObjectItems(deployments))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetAllStorageClasses(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	scList := a.ClusterCache.GetAllStorageClasses()
-
-	body, err := json.Marshal(wrapAsObjectItems(scList))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding storageclasses: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetAllStatefulSets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	qp := httputil.NewQueryParams(r.URL.Query())
-
-	namespace := qp.Get("namespace", "")
-
-	statefulSetsList := a.ClusterCache.GetAllStatefulSets()
-
-	// filter for provided namespace
-	var statefulSets []*appsv1.StatefulSet
-	if namespace == "" {
-		statefulSets = statefulSetsList
-	} else {
-		statefulSets = []*appsv1.StatefulSet{}
-
-		for _, ss := range statefulSetsList {
-			if ss.Namespace == namespace {
-				statefulSets = append(statefulSets, ss)
-			}
-		}
-	}
-
-	body, err := json.Marshal(wrapAsObjectItems(statefulSets))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetAllNodes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	nodeList := a.ClusterCache.GetAllNodes()
-
-	body, err := json.Marshal(wrapAsObjectItems(nodeList))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding nodes: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetAllPods(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	podlist := a.ClusterCache.GetAllPods()
-
-	body, err := json.Marshal(wrapAsObjectItems(podlist))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding pods: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetAllNamespaces(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	namespaces := a.ClusterCache.GetAllNamespaces()
-
-	body, err := json.Marshal(wrapAsObjectItems(namespaces))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetAllDaemonSets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	daemonSets := a.ClusterCache.GetAllDaemonSets()
-
-	body, err := json.Marshal(wrapAsObjectItems(daemonSets))
-	if err != nil {
-		fmt.Fprintf(w, "Error decoding daemon set: "+err.Error())
-	} else {
-		w.Write(body)
-	}
-}
-
-func (a *Accesses) GetPod(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	podName := ps.ByName("name")
-	podNamespace := ps.ByName("namespace")
-
-	// TODO: ClusterCache API could probably afford to have some better filtering
-	allPods := a.ClusterCache.GetAllPods()
-	for _, pod := range allPods {
-		for _, container := range pod.Spec.Containers {
-			container.Env = make([]v1.EnvVar, 0)
-		}
-		if pod.Namespace == podNamespace && pod.Name == podName {
-			body, err := json.Marshal(pod)
-			if err != nil {
-				fmt.Fprintf(w, "Error decoding pod: "+err.Error())
-			} else {
-				w.Write(body)
-			}
-			return
-		}
-	}
-
-	fmt.Fprintf(w, "Pod not found\n")
-}
-
 func (a *Accesses) PrometheusRecordingRules(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -1166,7 +981,7 @@ func (a *Accesses) GetOrphanedPods(w http.ResponseWriter, r *http.Request, ps ht
 
 	podlist := a.ClusterCache.GetAllPods()
 
-	var lonePods []*v1.Pod
+	var lonePods []*clustercache.Pod
 	for _, pod := range podlist {
 		if len(pod.OwnerReferences) == 0 {
 			lonePods = append(lonePods, pod)
@@ -1261,118 +1076,6 @@ func GetKubecostContainers(kubeClientSet kubernetes.Interface) ([]ContainerInfo,
 	return containers, nil
 }
 
-// logsFor pulls the logs for a specific pod, namespace, and container
-func logsFor(c kubernetes.Interface, namespace string, pod string, container string, dur time.Duration, ctx context.Context) (string, error) {
-	since := time.Now().UTC().Add(-dur)
-
-	logOpts := v1.PodLogOptions{
-		SinceTime: &metav1.Time{Time: since},
-	}
-	if container != "" {
-		logOpts.Container = container
-	}
-
-	req := c.CoreV1().Pods(namespace).GetLogs(pod, &logOpts)
-	reader, err := req.Stream(ctx)
-	if err != nil {
-		return "", err
-	}
-
-	podLogs, err := io.ReadAll(reader)
-	if err != nil {
-		return "", err
-	}
-
-	// If color is already disabled then we don't need to process the logs
-	// to drop ANSI colors
-	if !viper.GetBool("disable-log-color") {
-		podLogs = ANSIRegex.ReplaceAll(podLogs, []byte{})
-	}
-
-	return string(podLogs), nil
-}
-
-func (a *Accesses) GetPodLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-
-	qp := httputil.NewQueryParams(r.URL.Query())
-
-	ns := qp.Get("namespace", env.GetKubecostNamespace())
-	pod := qp.Get("pod", "")
-	selector := qp.Get("selector", "")
-	container := qp.Get("container", "")
-	since := qp.Get("since", "24h")
-
-	sinceDuration, err := time.ParseDuration(since)
-	if err != nil {
-		fmt.Fprintf(w, "Invalid Duration String: "+err.Error())
-		return
-	}
-
-	var logResult string
-	appendLog := func(ns string, pod string, container string, l string) {
-		if l == "" {
-			return
-		}
-
-		logResult += fmt.Sprintf("%s\n| %s:%s:%s\n%s\n%s\n\n", LogSeparator, ns, pod, container, LogSeparator, l)
-	}
-
-	if pod != "" {
-		pd, err := a.KubeClientSet.CoreV1().Pods(ns).Get(r.Context(), pod, metav1.GetOptions{})
-		if err != nil {
-			fmt.Fprintf(w, "Error Finding Pod: "+err.Error())
-			return
-		}
-
-		if container != "" {
-			var foundContainer bool
-			for _, cont := range pd.Spec.Containers {
-				if strings.EqualFold(cont.Name, container) {
-					foundContainer = true
-					break
-				}
-			}
-			if !foundContainer {
-				fmt.Fprintf(w, "Could not find container: "+container)
-				return
-			}
-		}
-
-		logs, err := logsFor(a.KubeClientSet, ns, pod, container, sinceDuration, r.Context())
-		if err != nil {
-			fmt.Fprintf(w, "Error Getting Logs: "+err.Error())
-			return
-		}
-
-		appendLog(ns, pod, container, logs)
-
-		w.Write([]byte(logResult))
-		return
-	}
-
-	if selector != "" {
-		pods, err := a.KubeClientSet.CoreV1().Pods(ns).List(r.Context(), metav1.ListOptions{LabelSelector: selector})
-		if err != nil {
-			fmt.Fprintf(w, "Error Finding Pod: "+err.Error())
-			return
-		}
-
-		for _, pd := range pods.Items {
-			for _, cont := range pd.Spec.Containers {
-				logs, err := logsFor(a.KubeClientSet, ns, pd.Name, cont.Name, sinceDuration, r.Context())
-				if err != nil {
-					continue
-				}
-				appendLog(ns, pd.Name, cont.Name, logs)
-			}
-		}
-	}
-
-	w.Write([]byte(logResult))
-}
-
 func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -1455,8 +1158,6 @@ func handlePanic(p errors.Panic) bool {
 }
 
 func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
-	configWatchers := watcher.NewConfigMapWatchers(additionalConfigWatchers...)
-
 	var err error
 	if errorReportingEnabled {
 		err = sentry.Init(sentry.ClientOptions{Release: version.FriendlyVersion()})
@@ -1555,13 +1256,7 @@ func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.
 	configPrefix := env.GetConfigPathWithDefault("/var/configs/")
 
 	// Create Kubernetes Cluster Cache + Watchers
-	var k8sCache clustercache.ClusterCache
-	if env.IsClusterCacheFileEnabled() {
-		importLocation := confManager.ConfigFileAt(path.Join(configPrefix, "cluster-cache.json"))
-		k8sCache = clustercache.NewClusterImporter(importLocation)
-	} else {
-		k8sCache = clustercache.NewKubernetesClusterCache(kubeClientset)
-	}
+	k8sCache := clustercache.NewKubernetesClusterCache(kubeClientset)
 	k8sCache.Run()
 
 	cloudProviderKey := env.GetCloudProviderAPIKey()
@@ -1571,25 +1266,12 @@ func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.
 	}
 
 	// Append the pricing config watcher
-	configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
-	configWatchers.AddWatcher(metrics.GetMetricsConfigWatcher())
-
-	watchConfigFunc := configWatchers.ToWatchFunc()
-	watchedConfigs := configWatchers.GetWatchedConfigs()
-
 	kubecostNamespace := env.GetKubecostNamespace()
-	// We need an initial invocation because the init of the cache has happened before we had access to the provider.
-	for _, cw := range watchedConfigs {
-		configs, err := kubeClientset.CoreV1().ConfigMaps(kubecostNamespace).Get(context.Background(), cw, metav1.GetOptions{})
-		if err != nil {
-			log.Infof("No %s configmap found at install time, using existing configs: %s", cw, err.Error())
-		} else {
-			log.Infof("Found configmap %s, watching...", configs.Name)
-			watchConfigFunc(configs)
-		}
-	}
 
-	k8sCache.SetConfigMapUpdateFunc(watchConfigFunc)
+	configWatchers := watcher.NewConfigMapWatchers(kubeClientset, kubecostNamespace, additionalConfigWatchers...)
+	configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
+	configWatchers.AddWatcher(metrics.GetMetricsConfigWatcher())
+	configWatchers.Watch()
 
 	remoteEnabled := env.IsRemoteEnabled()
 	if remoteEnabled {
@@ -1752,22 +1434,12 @@ func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.
 	router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
 
 	// endpoints migrated from server
-	router.GET("/allPersistentVolumes", a.GetAllPersistentVolumes)
-	router.GET("/allDeployments", a.GetAllDeployments)
-	router.GET("/allStorageClasses", a.GetAllStorageClasses)
-	router.GET("/allStatefulSets", a.GetAllStatefulSets)
-	router.GET("/allNodes", a.GetAllNodes)
-	router.GET("/allPods", a.GetAllPods)
-	router.GET("/allNamespaces", a.GetAllNamespaces)
-	router.GET("/allDaemonSets", a.GetAllDaemonSets)
-	router.GET("/pod/:namespace/:name", a.GetPod)
 	router.GET("/prometheusRecordingRules", a.PrometheusRecordingRules)
 	router.GET("/prometheusConfig", a.PrometheusConfig)
 	router.GET("/prometheusTargets", a.PrometheusTargets)
 	router.GET("/orphanedPods", a.GetOrphanedPods)
 	router.GET("/installNamespace", a.GetInstallNamespace)
 	router.GET("/installInfo", a.GetInstallInfo)
-	router.GET("/podLogs", a.GetPodLogs)
 	router.POST("/serviceKey", a.AddServiceKey)
 	router.GET("/helmValues", a.GetHelmValues)
 	router.GET("/status", a.Status)

+ 8 - 0
pkg/env/costmodelenv.go

@@ -140,6 +140,8 @@ const (
 	OCIPricingURL = "OCI_PRICING_URL"
 
 	CarbonEstimatesEnabledEnvVar = "CARBON_ESTIMATES_ENABLED"
+
+	UseCacheV1 = "USE_CACHE_V1"
 )
 
 const DefaultConfigMountPath = "/var/configs"
@@ -728,3 +730,9 @@ func GetCustomCostRefreshRateHours() string {
 func IsCarbonEstimatesEnabled() bool {
 	return env.GetBool(CarbonEstimatesEnabledEnvVar, false)
 }
+
+// GetUseCacheV1 is a temporary flag to allow users to opt-in to using the old cache
+// Mainly for comparison purposes
+func GetUseCacheV1() bool {
+	return env.GetBool(UseCacheV1, false)
+}

+ 8 - 8
pkg/metrics/deploymentmetrics.go

@@ -39,10 +39,10 @@ func (kdc KubecostDeploymentCollector) Collect(ch chan<- prometheus.Metric) {
 
 	ds := kdc.KubeClusterCache.GetAllDeployments()
 	for _, deployment := range ds {
-		deploymentName := deployment.GetName()
-		deploymentNS := deployment.GetNamespace()
+		deploymentName := deployment.Name
+		deploymentNS := deployment.Namespace
 
-		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(deployment.Spec.Selector.MatchLabels))
+		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(deployment.MatchLabels))
 		if len(labels) > 0 {
 			m := newDeploymentMatchLabelsMetric(deploymentName, deploymentNS, "deployment_match_labels", labels, values)
 			ch <- m
@@ -143,15 +143,15 @@ func (kdc KubeDeploymentCollector) Collect(ch chan<- prometheus.Metric) {
 	disabledMetrics := kdc.metricsConfig.GetDisabledMetricsMap()
 
 	for _, deployment := range deployments {
-		deploymentName := deployment.GetName()
-		deploymentNS := deployment.GetNamespace()
+		deploymentName := deployment.Name
+		deploymentNS := deployment.Namespace
 
 		// Replicas Defined
 		var replicas int32
-		if deployment.Spec.Replicas == nil {
+		if deployment.SpecReplicas == nil {
 			replicas = 1 // defaults to 1, documented on the 'Replicas' field
 		} else {
-			replicas = *deployment.Spec.Replicas
+			replicas = *deployment.SpecReplicas
 		}
 
 		if _, disabled := disabledMetrics["kube_deployment_spec_replicas"]; !disabled {
@@ -163,7 +163,7 @@ func (kdc KubeDeploymentCollector) Collect(ch chan<- prometheus.Metric) {
 				"kube_deployment_status_replicas_available",
 				deploymentName,
 				deploymentNS,
-				deployment.Status.AvailableReplicas)
+				deployment.StatusAvailableReplicas)
 		}
 	}
 }

+ 2 - 2
pkg/metrics/jobmetrics.go

@@ -41,8 +41,8 @@ func (kjc KubeJobCollector) Collect(ch chan<- prometheus.Metric) {
 
 	jobs := kjc.KubeClusterCache.GetAllJobs()
 	for _, job := range jobs {
-		jobName := job.GetName()
-		jobNS := job.GetNamespace()
+		jobName := job.Name
+		jobNS := job.Namespace
 
 		if job.Status.Failed == 0 {
 			ch <- newKubeJobStatusFailedMetric(jobName, jobNS, "kube_job_status_failed", "", 0)

+ 1 - 1
pkg/metrics/kubemetrics.go

@@ -161,7 +161,7 @@ func InitKubeMetrics(clusterCache clustercache.ClusterCache, metricsConfig *Metr
 
 // getPersistentVolumeClaimClass returns StorageClassName. If no storage class was
 // requested, it returns "".
-func getPersistentVolumeClaimClass(claim *v1.PersistentVolumeClaim) string {
+func getPersistentVolumeClaimClass(claim *clustercache.PersistentVolumeClaim) string {
 	// Use beta annotation first
 	if class, found := claim.Annotations[v1.BetaStorageClassAnnotation]; found {
 		return class

+ 1 - 1
pkg/metrics/metricsconfig.go

@@ -7,8 +7,8 @@ import (
 	"path"
 	"sync"
 
-	"github.com/opencost/opencost/core/pkg/util/watcher"
 	"github.com/opencost/opencost/pkg/env"
+	"github.com/opencost/opencost/pkg/util/watcher"
 )
 
 var (

+ 2 - 2
pkg/metrics/namespacemetrics.go

@@ -39,7 +39,7 @@ func (nsac KubecostNamespaceCollector) Collect(ch chan<- prometheus.Metric) {
 
 	namespaces := nsac.KubeClusterCache.GetAllNamespaces()
 	for _, namespace := range namespaces {
-		nsName := namespace.GetName()
+		nsName := namespace.Name
 
 		labels, values := promutil.KubeAnnotationsToLabels(namespace.Annotations)
 		if len(labels) > 0 {
@@ -137,7 +137,7 @@ func (nsac KubeNamespaceCollector) Collect(ch chan<- prometheus.Metric) {
 
 	namespaces := nsac.KubeClusterCache.GetAllNamespaces()
 	for _, namespace := range namespaces {
-		nsName := namespace.GetName()
+		nsName := namespace.Name
 
 		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(namespace.Labels))
 		if len(labels) > 0 {

+ 2 - 2
pkg/metrics/nodemetrics.go

@@ -62,7 +62,7 @@ func (nsac KubeNodeCollector) Collect(ch chan<- prometheus.Metric) {
 	disabledMetrics := nsac.metricsConfig.GetDisabledMetricsMap()
 
 	for _, node := range nodes {
-		nodeName := node.GetName()
+		nodeName := node.Name
 
 		// Node Capacity
 		for resourceName, quantity := range node.Status.Capacity {
@@ -120,7 +120,7 @@ func (nsac KubeNodeCollector) Collect(ch chan<- prometheus.Metric) {
 
 		// node labels
 		if _, disabled := disabledMetrics["kube_node_labels"]; !disabled {
-			labelNames, labelValues := promutil.KubePrependQualifierToLabels(promutil.SanitizeLabels(node.GetLabels()), "label_")
+			labelNames, labelValues := promutil.KubePrependQualifierToLabels(promutil.SanitizeLabels(node.Labels), "label_")
 			ch <- newKubeNodeLabelsMetric(nodeName, "kube_node_labels", labelNames, labelValues)
 		}
 

+ 10 - 10
pkg/metrics/podlabelmetrics.go

@@ -39,18 +39,18 @@ func (kpmc KubePodLabelsCollector) Describe(ch chan<- *prometheus.Desc) {
 
 func (kpmc *KubePodLabelsCollector) UpdateControllerSelectorsCache() {
 	for _, r := range kpmc.KubeClusterCache.GetAllReplicaSets() {
-		for k := range r.Spec.Selector.MatchLabels {
+		for k := range r.SpecSelector.MatchLabels {
 			kpmc.labelsWhitelist[k] = true
 		}
-		for _, v := range r.Spec.Selector.MatchExpressions {
+		for _, v := range r.SpecSelector.MatchExpressions {
 			kpmc.labelsWhitelist[v.Key] = true
 		}
 	}
 	for _, ss := range kpmc.KubeClusterCache.GetAllStatefulSets() {
-		for k := range ss.Spec.Selector.MatchLabels {
+		for k := range ss.SpecSelector.MatchLabels {
 			kpmc.labelsWhitelist[k] = true
 		}
-		for _, v := range ss.Spec.Selector.MatchExpressions {
+		for _, v := range ss.SpecSelector.MatchExpressions {
 			kpmc.labelsWhitelist[v.Key] = true
 		}
 	}
@@ -59,7 +59,7 @@ func (kpmc *KubePodLabelsCollector) UpdateControllerSelectorsCache() {
 func (kpmc *KubePodLabelsCollector) UpdateServiceLabels() {
 	for _, service := range kpmc.KubeClusterCache.GetAllServices() {
 		// Just unroll the selector and keep all labels whose keys could match a service selector
-		for k := range service.Spec.Selector {
+		for k := range service.SpecSelector {
 			kpmc.labelsWhitelist[k] = true
 		}
 	}
@@ -77,16 +77,16 @@ func (kpmc KubePodLabelsCollector) Collect(ch chan<- prometheus.Metric) {
 	disabledMetrics := kpmc.metricsConfig.GetDisabledMetricsMap()
 
 	for _, pod := range pods {
-		podName := pod.GetName()
-		podNS := pod.GetNamespace()
-		podUID := string(pod.GetUID())
+		podName := pod.Name
+		podNS := pod.Namespace
+		podUID := string(pod.UID)
 
 		// Pod Labels
 		if _, disabled := disabledMetrics["kube_pod_labels"]; !disabled {
-			podLabels := pod.GetLabels()
+			podLabels := pod.Labels
 			if kpmc.metricsConfig.UseLabelsWhitelist {
 				kpmc.UpdateWhitelist()
-				for lname := range podLabels {
+				for lname := range pod.Labels {
 					if _, ok := kpmc.labelsWhitelist[lname]; !ok {
 						delete(podLabels, lname)
 					}

+ 12 - 18
pkg/metrics/podlabelmetrics_test.go

@@ -4,27 +4,21 @@ import (
 	"testing"
 
 	"github.com/opencost/opencost/pkg/clustercache"
-	appsv1 "k8s.io/api/apps/v1"
-	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestWhitelist(t *testing.T) {
-	sampleServices := []*v1.Service{&v1.Service{
-		Spec: v1.ServiceSpec{
-			Selector: map[string]string{"servicewhitelistlabel": "foo"},
-		},
+	sampleServices := []*clustercache.Service{{
+		SpecSelector: map[string]string{"servicewhitelistlabel": "foo"},
 	}}
 	replicaSetLabelSelector := metav1.LabelSelector{
 		MatchLabels: map[string]string{"replicasetwhitelistlabel1": "bar"},
 	}
-	sampleReplicaSets := []*appsv1.ReplicaSet{{
-		Spec: appsv1.ReplicaSetSpec{
-			Selector: &replicaSetLabelSelector,
-		},
+	sampleReplicaSets := []*clustercache.ReplicaSet{{
+		SpecSelector: &replicaSetLabelSelector,
 	}}
 
-	sampleStatefulSets := []*appsv1.StatefulSet{}
+	sampleStatefulSets := []*clustercache.StatefulSet{}
 
 	kc := NewFakeCache(sampleReplicaSets, sampleStatefulSets, sampleServices)
 	wl := map[string]bool{
@@ -51,24 +45,24 @@ func TestWhitelist(t *testing.T) {
 
 type FakeCache struct {
 	clustercache.ClusterCache
-	replicasets  []*appsv1.ReplicaSet
-	statefulsets []*appsv1.StatefulSet
-	services     []*v1.Service
+	replicasets  []*clustercache.ReplicaSet
+	statefulsets []*clustercache.StatefulSet
+	services     []*clustercache.Service
 }
 
-func (f FakeCache) GetAllReplicaSets() []*appsv1.ReplicaSet {
+func (f FakeCache) GetAllReplicaSets() []*clustercache.ReplicaSet {
 	return f.replicasets
 }
 
-func (f FakeCache) GetAllStatefulSets() []*appsv1.StatefulSet {
+func (f FakeCache) GetAllStatefulSets() []*clustercache.StatefulSet {
 	return f.statefulsets
 }
 
-func (f FakeCache) GetAllServices() []*v1.Service {
+func (f FakeCache) GetAllServices() []*clustercache.Service {
 	return f.services
 }
 
-func NewFakeCache(replicasets []*appsv1.ReplicaSet, statefulsets []*appsv1.StatefulSet, services []*v1.Service) FakeCache {
+func NewFakeCache(replicasets []*clustercache.ReplicaSet, statefulsets []*clustercache.StatefulSet, services []*clustercache.Service) FakeCache {
 	return FakeCache{
 		replicasets:  replicasets,
 		statefulsets: statefulsets,

+ 6 - 6
pkg/metrics/podmetrics.go

@@ -42,8 +42,8 @@ func (kpmc KubecostPodCollector) Collect(ch chan<- prometheus.Metric) {
 
 	pods := kpmc.KubeClusterCache.GetAllPods()
 	for _, pod := range pods {
-		podName := pod.GetName()
-		podNS := pod.GetNamespace()
+		podName := pod.Name
+		podNS := pod.Namespace
 
 		// Pod Annotations
 		labels, values := promutil.KubeAnnotationsToLabels(pod.Annotations)
@@ -107,9 +107,9 @@ func (kpmc KubePodCollector) Collect(ch chan<- prometheus.Metric) {
 	disabledMetrics := kpmc.metricsConfig.GetDisabledMetricsMap()
 
 	for _, pod := range pods {
-		podName := pod.GetName()
-		podNS := pod.GetNamespace()
-		podUID := string(pod.GetUID())
+		podName := pod.Name
+		podNS := pod.Namespace
+		podUID := string(pod.UID)
 		node := pod.Spec.NodeName
 		phase := pod.Status.Phase
 
@@ -135,7 +135,7 @@ func (kpmc KubePodCollector) Collect(ch chan<- prometheus.Metric) {
 
 		// Pod Labels
 		if _, disabled := disabledMetrics["kube_pod_labels"]; !disabled {
-			labelNames, labelValues := promutil.KubePrependQualifierToLabels(promutil.SanitizeLabels(pod.GetLabels()), "label_")
+			labelNames, labelValues := promutil.KubePrependQualifierToLabels(promutil.SanitizeLabels(pod.Labels), "label_")
 			ch <- newKubePodLabelsMetric("kube_pod_labels", podNS, podName, podUID, labelNames, labelValues)
 		}
 

+ 3 - 3
pkg/metrics/servicemetrics.go

@@ -39,10 +39,10 @@ func (sc KubecostServiceCollector) Collect(ch chan<- prometheus.Metric) {
 
 	svcs := sc.KubeClusterCache.GetAllServices()
 	for _, svc := range svcs {
-		serviceName := svc.GetName()
-		serviceNS := svc.GetNamespace()
+		serviceName := svc.Name
+		serviceNS := svc.Namespace
 
-		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(svc.Spec.Selector))
+		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(svc.SpecSelector))
 		if len(labels) > 0 {
 			m := newServiceSelectorLabelsMetric(serviceName, serviceNS, "service_selector_labels", labels, values)
 			ch <- m

+ 3 - 3
pkg/metrics/statefulsetmetrics.go

@@ -38,10 +38,10 @@ func (sc KubecostStatefulsetCollector) Collect(ch chan<- prometheus.Metric) {
 
 	ds := sc.KubeClusterCache.GetAllStatefulSets()
 	for _, statefulset := range ds {
-		statefulsetName := statefulset.GetName()
-		statefulsetNS := statefulset.GetNamespace()
+		statefulsetName := statefulset.Name
+		statefulsetNS := statefulset.Namespace
 
-		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(statefulset.Spec.Selector.MatchLabels))
+		labels, values := promutil.KubeLabelsToLabels(promutil.SanitizeLabels(statefulset.SpecSelector.MatchLabels))
 		if len(labels) > 0 {
 			m := newStatefulsetMatchLabelsMetric(statefulsetName, statefulsetNS, "statefulSet_match_labels", labels, values)
 			ch <- m

+ 10 - 6
core/pkg/util/watcher/configwatcher_test.go → pkg/util/watcher/configwatcher_test.go

@@ -40,8 +40,8 @@ func TestConfigWatcherSingleHandler(t *testing.T) {
 	// triggered
 	var didRun bool = false
 
-	w := NewConfigMapWatchers(newTestWatcher(t, TestConfigMapName, "single", &didRun))
-	f := w.ToWatchFunc()
+	w := NewConfigMapWatchers(nil, "", newTestWatcher(t, TestConfigMapName, "single", &didRun))
+	f := w.toWatchFunc()
 
 	// Execute watch func with a 'test-config' configmap
 	f(newConfigMap(TestConfigMapName, "testing 1 2 3"))
@@ -57,10 +57,12 @@ func TestConfigWatcherMultipleHandlers(t *testing.T) {
 	var secondDidRun bool = false
 
 	w := NewConfigMapWatchers(
+		nil,
+		"",
 		newTestWatcher(t, TestConfigMapName, "single", &firstDidRun),
 		newTestWatcher(t, AlternateTestConfigMapName, "alternate", &secondDidRun))
 
-	f := w.ToWatchFunc()
+	f := w.toWatchFunc()
 
 	// Execute watch func with a 'alternate-test-config' configmap
 	f(newConfigMap(AlternateTestConfigMapName, "oof!"))
@@ -82,13 +84,15 @@ func TestConfigWatcherMultipleHandlersForSameConfig(t *testing.T) {
 	var thirdDidRun bool = false
 
 	w := NewConfigMapWatchers(
+		nil,
+		"",
 		newTestWatcher(t, TestConfigMapName, "first", &firstDidRun),
 		newTestWatcher(t, AlternateTestConfigMapName, "alternate", &secondDidRun),
 		// third watcher watches for the same configmap as "first"
 		newTestWatcher(t, TestConfigMapName, "third", &thirdDidRun),
 	)
 
-	f := w.ToWatchFunc()
+	f := w.toWatchFunc()
 
 	// Execute watch func with a 'test-config' configmap
 	f(newConfigMap(TestConfigMapName, "double trouble"))
@@ -118,12 +122,12 @@ func TestConfigMapWatcherWithAdd(t *testing.T) {
 		// third watcher watches for the same configmap as "first"
 		newTestWatcher(t, TestConfigMapName, "third", &thirdDidRun)
 
-	w := NewConfigMapWatchers()
+	w := NewConfigMapWatchers(nil, "")
 	w.AddWatcher(a)
 	w.AddWatcher(b)
 	w.Add(c.ConfigMapName, c.WatchFunc)
 
-	f := w.ToWatchFunc()
+	f := w.toWatchFunc()
 
 	// Execute watch func with a 'test-config' configmap
 	f(newConfigMap(TestConfigMapName, "double trouble"))

+ 135 - 0
pkg/util/watcher/configwatchers.go

@@ -0,0 +1,135 @@
+package watcher
+
+import (
+	"context"
+	"sync/atomic"
+
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/pkg/clustercache"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/fields"
+
+	"k8s.io/client-go/kubernetes"
+)
+
+// ConfigMapWatcher represents a single configmap watcher
+type ConfigMapWatcher struct {
+	ConfigMapName string
+	WatchFunc     func(string, map[string]string) error
+}
+
+type ConfigMapWatchers struct {
+	kubeClientset   kubernetes.Interface
+	namespace       string
+	watchers        map[string][]*ConfigMapWatcher
+	watchController clustercache.WatchController
+	started         atomic.Bool
+	stop            chan struct{}
+}
+
+func NewConfigMapWatchers(kubeClientset kubernetes.Interface, namespace string, watchers ...*ConfigMapWatcher) *ConfigMapWatchers {
+	var stopCh chan struct{}
+	var watchController clustercache.WatchController
+
+	if kubeClientset != nil {
+		coreRestClient := kubeClientset.CoreV1().RESTClient()
+		watchController = clustercache.NewCachingWatcher(coreRestClient, "configmaps", &v1.ConfigMap{}, namespace, fields.Everything())
+		stopCh = make(chan struct{})
+
+		// a bit awkward here, but since we'll mostly be deferring adding a watcher after initializing k8s,
+		// we'll warmup and start the actual watcher here
+		watchController.WarmUp(stopCh)
+		go watchController.Run(1, stopCh)
+	}
+
+	cmw := &ConfigMapWatchers{
+		kubeClientset:   kubeClientset,
+		namespace:       namespace,
+		watchController: watchController,
+		watchers:        make(map[string][]*ConfigMapWatcher),
+		stop:            stopCh,
+	}
+
+	for _, w := range watchers {
+		cmw.AddWatcher(w)
+	}
+
+	return cmw
+}
+
+func (cmw *ConfigMapWatchers) AddWatcher(watcher *ConfigMapWatcher) {
+	if cmw.started.Load() {
+		log.Warnf("Cannot add watcher %s after starting", watcher.ConfigMapName)
+		return
+	}
+
+	if watcher == nil {
+		return
+	}
+
+	name := watcher.ConfigMapName
+	cmw.watchers[name] = append(cmw.watchers[name], watcher)
+}
+
+func (cmw *ConfigMapWatchers) Add(configMapName string, watchFunc func(string, map[string]string) error) {
+	cmw.AddWatcher(&ConfigMapWatcher{
+		ConfigMapName: configMapName,
+		WatchFunc:     watchFunc,
+	})
+}
+
+func (cmw *ConfigMapWatchers) Watch() {
+	if cmw.kubeClientset == nil {
+		return
+	}
+
+	if !cmw.started.CompareAndSwap(false, true) {
+		log.Warnf("Already started")
+		return
+	}
+
+	watchConfigFunc := cmw.toWatchFunc()
+
+	// We need an initial invocation because the init of the cache has happened before we had access to the provider.
+	for cw := range cmw.watchers {
+		configs, err := cmw.kubeClientset.CoreV1().ConfigMaps(cmw.namespace).Get(context.Background(), cw, metav1.GetOptions{})
+		if err != nil {
+			log.Infof("No %s configmap found at install time, using existing configs: %s", cw, err.Error())
+		} else {
+			log.Infof("Found configmap %s, watching...", configs.Name)
+			watchConfigFunc(configs)
+		}
+	}
+
+	cmw.watchController.SetUpdateHandler(watchConfigFunc)
+}
+
+func (cmw *ConfigMapWatchers) Stop() {
+	if cmw.stop == nil {
+		return
+	}
+
+	close(cmw.stop)
+	cmw.stop = nil
+}
+
+func (cmw *ConfigMapWatchers) toWatchFunc() func(any) {
+	return func(c any) {
+		conf, ok := c.(*v1.ConfigMap)
+		if !ok {
+			return
+		}
+
+		name := conf.GetName()
+		data := conf.Data
+		if watchers, ok := cmw.watchers[name]; ok {
+			for _, cw := range watchers {
+				err := cw.WatchFunc(name, data)
+				if err != nil {
+					log.Infof("ERROR UPDATING %s CONFIG: %s", name, err.Error())
+				}
+			}
+		}
+	}
+}

+ 51 - 52
test/cloud_test.go

@@ -16,7 +16,6 @@ import (
 	"github.com/opencost/opencost/pkg/config"
 	"github.com/opencost/opencost/pkg/costmodel"
 
-	appsv1 "k8s.io/api/apps/v1"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 )
@@ -33,8 +32,8 @@ func TestRegionValueFromMapField(t *testing.T) {
 	wantpid := strings.ToLower("/subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0")
 	providerIDWant := wantRegion + "," + wantpid
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0"
+	n := &clustercache.Node{}
+	n.SpecProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0"
 	n.Labels = make(map[string]string)
 	n.Labels[v1.LabelTopologyRegion] = wantRegion
 	got := provider.NodeValueFromMapField(providerIDMap, n, true)
@@ -45,24 +44,24 @@ func TestRegionValueFromMapField(t *testing.T) {
 }
 func TestTransformedValueFromMapField(t *testing.T) {
 	providerIDWant := "i-05445591e0d182d42"
-	n := &v1.Node{}
-	n.Spec.ProviderID = "aws:///us-east-1a/i-05445591e0d182d42"
+	n := &clustercache.Node{}
+	n.SpecProviderID = "aws:///us-east-1a/i-05445591e0d182d42"
 	got := provider.NodeValueFromMapField(providerIDMap, n, false)
 	if got != providerIDWant {
 		t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant, got)
 	}
 
 	providerIDWant2 := strings.ToLower("/subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0")
-	n2 := &v1.Node{}
-	n2.Spec.ProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0"
+	n2 := &clustercache.Node{}
+	n2.SpecProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/MC_test_test_eastus/providers/Microsoft.Compute/virtualMachines/aks-agentpool-20139558-0"
 	got2 := provider.NodeValueFromMapField(providerIDMap, n2, false)
 	if got2 != providerIDWant2 {
 		t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant2, got2)
 	}
 
 	providerIDWant3 := strings.ToLower("/subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/mc_testspot_testspot_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-19213364-vmss/virtualMachines/0")
-	n3 := &v1.Node{}
-	n3.Spec.ProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/mc_testspot_testspot_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-19213364-vmss/virtualMachines/0"
+	n3 := &clustercache.Node{}
+	n3.SpecProviderID = "azure:///subscriptions/0bd50fdf-c923-4e1e-850c-196dd3dcc5d3/resourceGroups/mc_testspot_testspot_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-19213364-vmss/virtualMachines/0"
 	got3 := provider.NodeValueFromMapField(providerIDMap, n3, false)
 	if got3 != providerIDWant3 {
 		t.Errorf("Assert on '%s' want '%s' got '%s'", providerIDMap, providerIDWant3, got3)
@@ -74,8 +73,8 @@ func TestNodeValueFromMapField(t *testing.T) {
 	nameWant := "gke-standard-cluster-1-pool-1-91dc432d-cg69"
 	labelFooWant := "labelfoo"
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = providerIDWant
+	n := &clustercache.Node{}
+	n.SpecProviderID = providerIDWant
 	n.Name = nameWant
 	n.Labels = make(map[string]string)
 	n.Labels["foo"] = labelFooWant
@@ -99,7 +98,7 @@ func TestNodeValueFromMapField(t *testing.T) {
 
 func TestPVPriceFromCSV(t *testing.T) {
 	nameWant := "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d"
-	pv := &v1.PersistentVolume{}
+	pv := &clustercache.PersistentVolume{}
 	pv.Name = nameWant
 
 	confMan := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
@@ -132,7 +131,7 @@ func TestPVPriceFromCSV(t *testing.T) {
 func TestPVPriceFromCSVStorageClass(t *testing.T) {
 	nameWant := "pvc-08e1f205-d7a9-4430-90fc-7b3965a18c4d"
 	storageClassWant := "storageclass0"
-	pv := &v1.PersistentVolume{}
+	pv := &clustercache.PersistentVolume{}
 	pv.Name = nameWant
 	pv.Spec.StorageClassName = storageClassWant
 
@@ -173,8 +172,8 @@ func TestNodePriceFromCSVWithGPU(t *testing.T) {
 		LocalConfigPath: "./",
 	})
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = providerIDWant
+	n := &clustercache.Node{}
+	n.SpecProviderID = providerIDWant
 	n.Name = nameWant
 	n.Labels = make(map[string]string)
 	n.Labels["foo"] = labelFooWant
@@ -182,8 +181,8 @@ func TestNodePriceFromCSVWithGPU(t *testing.T) {
 	n.Status.Capacity = v1.ResourceList{"nvidia.com/gpu": *resource.NewScaledQuantity(2, 0)}
 	wantPrice := "1.633700"
 
-	n2 := &v1.Node{}
-	n2.Spec.ProviderID = providerIDWant
+	n2 := &clustercache.Node{}
+	n2.SpecProviderID = providerIDWant
 	n2.Name = nameWant
 	n2.Labels = make(map[string]string)
 	n2.Labels["foo"] = labelFooWant
@@ -244,7 +243,7 @@ func TestNodePriceFromCSVSpecialChar(t *testing.T) {
 		LocalConfigPath: "./",
 	})
 
-	n := &v1.Node{}
+	n := &clustercache.Node{}
 	n.Name = nameWant
 	n.Labels = make(map[string]string)
 	n.Labels["<http://metadata.label.servers.com/label|metadata.label.servers.com/label>"] = nameWant
@@ -281,8 +280,8 @@ func TestNodePriceFromCSV(t *testing.T) {
 		LocalConfigPath: "./",
 	})
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = providerIDWant
+	n := &clustercache.Node{}
+	n.SpecProviderID = providerIDWant
 	n.Name = nameWant
 	n.Labels = make(map[string]string)
 	n.Labels["foo"] = labelFooWant
@@ -309,8 +308,8 @@ func TestNodePriceFromCSV(t *testing.T) {
 		}
 	}
 
-	unknownN := &v1.Node{}
-	unknownN.Spec.ProviderID = providerIDWant
+	unknownN := &clustercache.Node{}
+	unknownN.SpecProviderID = providerIDWant
 	unknownN.Name = "unknownname"
 	unknownN.Labels = make(map[string]string)
 	unknownN.Labels["foo"] = labelFooWant
@@ -343,24 +342,24 @@ func TestNodePriceFromCSVWithRegion(t *testing.T) {
 		LocalConfigPath: "./",
 	})
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = providerIDWant
+	n := &clustercache.Node{}
+	n.SpecProviderID = providerIDWant
 	n.Name = nameWant
 	n.Labels = make(map[string]string)
 	n.Labels["foo"] = labelFooWant
 	n.Labels[v1.LabelTopologyRegion] = "regionone"
 	wantPrice := "0.133700"
 
-	n2 := &v1.Node{}
-	n2.Spec.ProviderID = providerIDWant
+	n2 := &clustercache.Node{}
+	n2.SpecProviderID = providerIDWant
 	n2.Name = nameWant
 	n2.Labels = make(map[string]string)
 	n2.Labels["foo"] = labelFooWant
 	n2.Labels[v1.LabelTopologyRegion] = "regiontwo"
 	wantPrice2 := "0.133800"
 
-	n3 := &v1.Node{}
-	n3.Spec.ProviderID = providerIDWant
+	n3 := &clustercache.Node{}
+	n3.SpecProviderID = providerIDWant
 	n3.Name = nameWant
 	n3.Labels = make(map[string]string)
 	n3.Labels["foo"] = labelFooWant
@@ -411,8 +410,8 @@ func TestNodePriceFromCSVWithRegion(t *testing.T) {
 		}
 	}
 
-	unknownN := &v1.Node{}
-	unknownN.Spec.ProviderID = "fake providerID"
+	unknownN := &clustercache.Node{}
+	unknownN.SpecProviderID = "fake providerID"
 	unknownN.Name = "unknownname"
 	unknownN.Labels = make(map[string]string)
 	unknownN.Labels[v1.LabelTopologyRegion] = "fakeregion"
@@ -437,19 +436,19 @@ func TestNodePriceFromCSVWithRegion(t *testing.T) {
 }
 
 type FakeCache struct {
-	nodes []*v1.Node
+	nodes []*clustercache.Node
 	clustercache.ClusterCache
 }
 
-func (f FakeCache) GetAllNodes() []*v1.Node {
+func (f FakeCache) GetAllNodes() []*clustercache.Node {
 	return f.nodes
 }
 
-func (f FakeCache) GetAllDaemonSets() []*appsv1.DaemonSet {
+func (f FakeCache) GetAllDaemonSets() []*clustercache.DaemonSet {
 	return nil
 }
 
-func NewFakeNodeCache(nodes []*v1.Node) FakeCache {
+func NewFakeNodeCache(nodes []*clustercache.Node) FakeCache {
 	return FakeCache{
 		nodes: nodes,
 	}
@@ -473,14 +472,14 @@ func TestNodePriceFromCSVWithBadConfig(t *testing.T) {
 	}
 	c.DownloadPricingData()
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = "fake"
+	n := &clustercache.Node{}
+	n.SpecProviderID = "fake"
 	n.Name = "nameWant"
 	n.Labels = make(map[string]string)
 	n.Labels["foo"] = "labelFooWant"
 	n.Labels[v1.LabelTopologyRegion] = "regionone"
 
-	fc := NewFakeNodeCache([]*v1.Node{n})
+	fc := NewFakeNodeCache([]*clustercache.Node{n})
 	fm := FakeClusterMap{}
 	d, _ := time.ParseDuration("1m")
 
@@ -507,15 +506,15 @@ func TestSourceMatchesFromCSV(t *testing.T) {
 	}
 	c.DownloadPricingData()
 
-	n := &v1.Node{}
-	n.Spec.ProviderID = "fake"
+	n := &clustercache.Node{}
+	n.SpecProviderID = "fake"
 	n.Name = "nameWant"
 	n.Labels = make(map[string]string)
 	n.Labels["foo"] = "labelFooWant"
 	n.Labels[v1.LabelTopologyRegion] = "regionone"
 
-	n2 := &v1.Node{}
-	n2.Spec.ProviderID = "azure:///subscriptions/123a7sd-asd-1234-578a9-123abcdef/resourceGroups/case_12_STaGe_TeSt7/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-agent-worker0-12stagetest7-ezggnore/virtualMachines/7"
+	n2 := &clustercache.Node{}
+	n2.SpecProviderID = "azure:///subscriptions/123a7sd-asd-1234-578a9-123abcdef/resourceGroups/case_12_STaGe_TeSt7/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-agent-worker0-12stagetest7-ezggnore/virtualMachines/7"
 	n2.Labels = make(map[string]string)
 	n2.Labels[v1.LabelTopologyRegion] = "eastus2"
 	n2.Labels["foo"] = "labelFooWant"
@@ -532,14 +531,14 @@ func TestSourceMatchesFromCSV(t *testing.T) {
 		}
 	}
 
-	n3 := &v1.Node{}
-	n3.Spec.ProviderID = "fake"
+	n3 := &clustercache.Node{}
+	n3.SpecProviderID = "fake"
 	n3.Name = "nameWant"
 	n3.Labels = make(map[string]string)
 	n3.Labels[v1.LabelTopologyRegion] = "eastus2"
 	n3.Labels[v1.LabelInstanceTypeStable] = "Standard_F32s_v2"
 
-	fc := NewFakeNodeCache([]*v1.Node{n, n2, n3})
+	fc := NewFakeNodeCache([]*clustercache.Node{n, n2, n3})
 	fm := FakeClusterMap{}
 	d, _ := time.ParseDuration("1m")
 
@@ -568,8 +567,8 @@ func TestSourceMatchesFromCSV(t *testing.T) {
 }
 
 func TestNodePriceFromCSVWithCase(t *testing.T) {
-	n := &v1.Node{}
-	n.Spec.ProviderID = "azure:///subscriptions/123a7sd-asd-1234-578a9-123abcdef/resourceGroups/case_12_STaGe_TeSt7/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-agent-worker0-12stagetest7-ezggnore/virtualMachines/7"
+	n := &clustercache.Node{}
+	n.SpecProviderID = "azure:///subscriptions/123a7sd-asd-1234-578a9-123abcdef/resourceGroups/case_12_STaGe_TeSt7/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-agent-worker0-12stagetest7-ezggnore/virtualMachines/7"
 	n.Labels = make(map[string]string)
 	n.Labels[v1.LabelTopologyRegion] = "eastus2"
 	wantPrice := "0.13370357"
@@ -608,7 +607,7 @@ func TestNodePriceFromCSVMixed(t *testing.T) {
 		LocalConfigPath: "./",
 	})
 
-	n := &v1.Node{}
+	n := &clustercache.Node{}
 	n.Labels = make(map[string]string)
 	n.Labels["TestClusterUsage"] = labelFooWant
 	n.Labels["nvidia.com/gpu_type"] = "a100-ondemand"
@@ -616,7 +615,7 @@ func TestNodePriceFromCSVMixed(t *testing.T) {
 	wantPrice := "1.904110"
 
 	labelFooWant2 := "Reserved"
-	n2 := &v1.Node{}
+	n2 := &clustercache.Node{}
 	n2.Labels = make(map[string]string)
 	n2.Labels["TestClusterUsage"] = labelFooWant2
 	n2.Labels["nvidia.com/gpu_type"] = "a100-reserved"
@@ -659,8 +658,8 @@ func TestNodePriceFromCSVMixed(t *testing.T) {
 }
 
 func TestNodePriceFromCSVByClass(t *testing.T) {
-	n := &v1.Node{}
-	n.Spec.ProviderID = "fakeproviderid"
+	n := &clustercache.Node{}
+	n.SpecProviderID = "fakeproviderid"
 	n.Labels = make(map[string]string)
 	n.Labels[v1.LabelTopologyRegion] = "eastus2"
 	n.Labels[v1.LabelInstanceTypeStable] = "Standard_F32s_v2"
@@ -691,8 +690,8 @@ func TestNodePriceFromCSVByClass(t *testing.T) {
 		}
 	}
 
-	n2 := &v1.Node{}
-	n2.Spec.ProviderID = "fakeproviderid"
+	n2 := &clustercache.Node{}
+	n2.SpecProviderID = "fakeproviderid"
 	n2.Labels = make(map[string]string)
 	n2.Labels[v1.LabelTopologyRegion] = "fakeregion"
 	n2.Labels[v1.LabelInstanceTypeStable] = "Standard_F32s_v2"