From 55373a76ec986ea788c76e493d6c64dc762f6eec Mon Sep 17 00:00:00 2001 From: Alan Rodrigues Date: Thu, 25 Jun 2026 14:23:14 -0700 Subject: [PATCH 1/4] test configmap reading role and binding from helm --- finops-agent-chart | 1 + pkg/core/opencost/opencost.go | 71 +++++++++++++++++++++++++++++++++++ pkg/env/unifiedagentenv.go | 21 +++++++++++ 3 files changed, 93 insertions(+) create mode 120000 finops-agent-chart diff --git a/finops-agent-chart b/finops-agent-chart new file mode 120000 index 00000000..acae6376 --- /dev/null +++ b/finops-agent-chart @@ -0,0 +1 @@ +/Users/alanrodrigues/devel/finops-agent-chart \ No newline at end of file diff --git a/pkg/core/opencost/opencost.go b/pkg/core/opencost/opencost.go index cd91abbd..52b9614c 100644 --- a/pkg/core/opencost/opencost.go +++ b/pkg/core/opencost/opencost.go @@ -2,13 +2,17 @@ package opencost import ( "context" + "fmt" "os" + "strings" "time" + agentenv "github.com/ibm/finops-agent/pkg/env" kcenv "github.com/ibm/finops-agent/kubecost/env" "github.com/opencost/opencost/core/pkg/kubeconfig" "github.com/opencost/opencost/core/pkg/storage" "github.com/opencost/opencost/pkg/util/watcher" + "sigs.k8s.io/yaml" "github.com/ibm/finops-agent/pkg/cluster" "github.com/ibm/finops-agent/pkg/nodes" @@ -59,6 +63,30 @@ func NewOpenCostDataSource( configWatchers := watcher.NewConfigMapWatchers(kubeClientset, kcenv.GetFinOpsAgentNamespace()) configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider)) + + // If an external labels ConfigMap is configured, watch it and log its labels on every change. + if cmName := agentenv.GetExternalLabelsConfigMapName(); cmName != "" { + cmNamespace := agentenv.GetExternalLabelsConfigMapNamespace() + cmPath := agentenv.GetExternalLabelsConfigMapPath() + + // The watcher monitors ConfigMaps in the agent namespace; for cross-namespace ConfigMaps + // we use a separate watcher scoped to the target namespace. + extWatchers := watcher.NewConfigMapWatchers(kubeClientset, cmNamespace) + extWatchers.Add(cmName, func(_ string, data map[string]string) error { + labels, err := extractLabelsFromConfigMap(data, cmPath) + if err != nil { + log.Warnf("ExternalLabels: failed to extract labels from ConfigMap %s/%s at path %q: %s", cmNamespace, cmName, cmPath, err) + return nil + } + log.Infof("ExternalLabels: loaded %d label(s) from ConfigMap %s/%s (path: %q)", len(labels), cmNamespace, cmName, cmPath) + for k, v := range labels { + log.Infof("ExternalLabels: %s = %s", k, v) + } + return nil + }) + extWatchers.Watch() + } + configWatchers.Watch() // ClusterInfo Provider to provide the cluster map with local and remote cluster data @@ -132,3 +160,46 @@ func NewOpenCostDataSource( return dataSource, cloudProvider } + +// extractLabelsFromConfigMap reads the "config.yaml" key from ConfigMap data, unmarshals it, +// and walks the dot-separated path to return the labels map. If path is empty the entire +// config.yaml content is expected to be a flat map[string]string. +func extractLabelsFromConfigMap(data map[string]string, path string) (map[string]string, error) { + raw, ok := data["config.yaml"] + if !ok { + return nil, fmt.Errorf("key \"config.yaml\" not found in ConfigMap data") + } + + // Unmarshal the YAML blob into a generic map. + var root map[string]interface{} + if err := yaml.Unmarshal([]byte(raw), &root); err != nil { + return nil, fmt.Errorf("failed to unmarshal config.yaml: %w", err) + } + + // Walk the dot-separated path to find the labels node. + var node interface{} = root + if path != "" { + for _, segment := range strings.Split(path, ".") { + m, ok := node.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("path segment %q: parent is not a map", segment) + } + node, ok = m[segment] + if !ok { + return nil, fmt.Errorf("path segment %q not found", segment) + } + } + } + + // The final node must be a map whose values are all strings. + rawMap, ok := node.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("value at path %q is not a map", path) + } + + labels := make(map[string]string, len(rawMap)) + for k, v := range rawMap { + labels[k] = fmt.Sprintf("%v", v) + } + return labels, nil +} diff --git a/pkg/env/unifiedagentenv.go b/pkg/env/unifiedagentenv.go index 7f164175..87c13a99 100644 --- a/pkg/env/unifiedagentenv.go +++ b/pkg/env/unifiedagentenv.go @@ -52,6 +52,11 @@ const ( // ParseMetricDataEnvVar env var for sanitizing k8s resources ParseMetricDataEnvVar = "PARSE_METRIC_DATA" + // External labels ConfigMap configuration + ExternalLabelsConfigMapNameEnvVar = "EXTERNAL_LABELS_CONFIGMAP_NAME" + ExternalLabelsConfigMapNamespaceEnvVar = "EXTERNAL_LABELS_CONFIGMAP_NAMESPACE" + ExternalLabelsConfigMapPathEnvVar = "EXTERNAL_LABELS_CONFIGMAP_PATH" + // Prefixes for CloudabilityPrefix = "CLOUDABILITY_" ) @@ -191,3 +196,19 @@ func getValueWithPotentialPrefixOrDefault[T any](envVariable string, prefix stri return convert(envValue) } + +// GetExternalLabelsConfigMapName returns the name of the ConfigMap containing external labels. +func GetExternalLabelsConfigMapName() string { + return env.Get(ExternalLabelsConfigMapNameEnvVar, "") +} + +// GetExternalLabelsConfigMapNamespace returns the namespace of the ConfigMap containing external labels. +func GetExternalLabelsConfigMapNamespace() string { + return env.Get(ExternalLabelsConfigMapNamespaceEnvVar, "") +} + +// GetExternalLabelsConfigMapPath returns the dot-separated YAML path within the ConfigMap's +// config.yaml key where the external labels map lives (e.g. "prometheusK8s.externalLabels"). +func GetExternalLabelsConfigMapPath() string { + return env.Get(ExternalLabelsConfigMapPathEnvVar, "") +} From 0ea9c3d7ae1f9c14a8649a01dad76089ca682af5 Mon Sep 17 00:00:00 2001 From: Alan Rodrigues Date: Thu, 25 Jun 2026 14:27:56 -0700 Subject: [PATCH 2/4] remove chart --- finops-agent-chart | 1 - 1 file changed, 1 deletion(-) delete mode 120000 finops-agent-chart diff --git a/finops-agent-chart b/finops-agent-chart deleted file mode 120000 index acae6376..00000000 --- a/finops-agent-chart +++ /dev/null @@ -1 +0,0 @@ -/Users/alanrodrigues/devel/finops-agent-chart \ No newline at end of file From cd43c08eb4a54d38d80b5b0d7b5e35b7504e02cc Mon Sep 17 00:00:00 2001 From: Alan Rodrigues Date: Mon, 29 Jun 2026 12:20:20 -0700 Subject: [PATCH 3/4] simplify the approach --- go.mod | 15 ++++++- go.sum | 39 ++++++------------ pkg/core/opencost/opencost.go | 75 ++++------------------------------- pkg/env/unifiedagentenv.go | 25 ++++-------- 4 files changed, 41 insertions(+), 113 deletions(-) diff --git a/go.mod b/go.mod index d6260a2d..ce5dee84 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( k8s.io/client-go v0.36.0 k8s.io/kubelet v0.36.0 sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -81,6 +82,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/digitalocean/godo v1.192.0 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect @@ -121,6 +123,7 @@ require ( github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect @@ -128,8 +131,10 @@ require ( github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -172,6 +177,8 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/stackitcloud/stackit-sdk-go/core v0.24.0 // indirect + github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tinylib/msgp v1.6.3 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -218,5 +225,11 @@ require ( sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace ( + github.com/opencost/opencost => /Users/alanrodrigues/devel/opencost + github.com/opencost/opencost/core => /Users/alanrodrigues/devel/opencost/core + github.com/opencost/opencost/modules/collector-source => /Users/alanrodrigues/devel/opencost/modules/collector-source + github.com/opencost/opencost/modules/prometheus-source => /Users/alanrodrigues/devel/opencost/modules/prometheus-source ) diff --git a/go.sum b/go.sum index 247e889e..22ac1156 100644 --- a/go.sum +++ b/go.sum @@ -150,6 +150,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitalocean/godo v1.192.0 h1:It3AcVa123/Eh/Ol+F9CXXBBlTsWyUIYe8yZhhFZ9Q4= +github.com/digitalocean/godo v1.192.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -260,9 +262,12 @@ github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9U github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -283,12 +288,16 @@ github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= @@ -371,14 +380,6 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/opencost/opencost v1.120.2 h1:+/YvqhI5E6gv4Mx9+kXqgXTM393eZET1Ku9H6cd1gRo= -github.com/opencost/opencost v1.120.2/go.mod h1:AwDKfsgDgocaWsJfFEkynqlIJD4ZlUMePUpb5i6jWmk= -github.com/opencost/opencost/core v1.120.2 h1:/m0Ec4zJ+Zy3+XcKFoU5DM8pPNAUefzlpUZdMxmc/vo= -github.com/opencost/opencost/core v1.120.2/go.mod h1:pJyyuUPIfoZcna0KdhY314mCqk1SHYj78yRzxk/dJf0= -github.com/opencost/opencost/modules/collector-source v1.120.2 h1:IstMtLdfFd9vqpOeCaxU6BmqXQWDD/3oiJinh6NjZSc= -github.com/opencost/opencost/modules/collector-source v1.120.2/go.mod h1:unNdyZLURGoWlZvfP2psudTIeA2ddbYtaOYXYmpvqyc= -github.com/opencost/opencost/modules/prometheus-source v1.120.2 h1:6o5b+E1p09g57RwlozHrOlQO+IoLKLhwv7vLhLmceEQ= -github.com/opencost/opencost/modules/prometheus-source v1.120.2/go.mod h1:g+WkFmnY0AlFCnGXwCwl8EFOZYgDI7+H3ZEh+y391hI= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/oracle/oci-go-sdk/v65 v65.109.0 h1:EsbFVvcV+uid9SoQnFQbTKS6FgqsM9NtoKmUIovKsbk= @@ -434,6 +435,10 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stackitcloud/stackit-sdk-go/core v0.24.0 h1:kHCcezCJ5OGSP7RRuGOxD5rF2wejpkEiRr/OdvNcuPQ= +github.com/stackitcloud/stackit-sdk-go/core v0.24.0/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI= +github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1 h1:U2sBfMeBCdZUvCW+vqPbo+HPtGxMjCF21PYyQncPnpg= +github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1/go.mod h1:Qt/scoasQrONlQ9FauvafUJ/3sP3xIFnhBQC8/Yhqgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -517,10 +522,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -539,8 +540,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -552,8 +551,6 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= @@ -586,12 +583,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1 h1:QNaHp8YvpPswfDNxlCmJyeesxbGOgaKf41iT9/QrErY= -golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1/go.mod h1:NuITXsA9cTiqnXtVk+/wrBT2Ja4X5hsfGOYRJ6kgYjs= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -599,8 +592,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -610,8 +601,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -625,8 +614,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/core/opencost/opencost.go b/pkg/core/opencost/opencost.go index 52b9614c..1dd67947 100644 --- a/pkg/core/opencost/opencost.go +++ b/pkg/core/opencost/opencost.go @@ -2,17 +2,14 @@ package opencost import ( "context" - "fmt" "os" - "strings" "time" - agentenv "github.com/ibm/finops-agent/pkg/env" kcenv "github.com/ibm/finops-agent/kubecost/env" + agentenv "github.com/ibm/finops-agent/pkg/env" "github.com/opencost/opencost/core/pkg/kubeconfig" "github.com/opencost/opencost/core/pkg/storage" "github.com/opencost/opencost/pkg/util/watcher" - "sigs.k8s.io/yaml" "github.com/ibm/finops-agent/pkg/cluster" "github.com/ibm/finops-agent/pkg/nodes" @@ -64,27 +61,11 @@ func NewOpenCostDataSource( configWatchers := watcher.NewConfigMapWatchers(kubeClientset, kcenv.GetFinOpsAgentNamespace()) configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider)) - // If an external labels ConfigMap is configured, watch it and log its labels on every change. - if cmName := agentenv.GetExternalLabelsConfigMapName(); cmName != "" { - cmNamespace := agentenv.GetExternalLabelsConfigMapNamespace() - cmPath := agentenv.GetExternalLabelsConfigMapPath() - - // The watcher monitors ConfigMaps in the agent namespace; for cross-namespace ConfigMaps - // we use a separate watcher scoped to the target namespace. - extWatchers := watcher.NewConfigMapWatchers(kubeClientset, cmNamespace) - extWatchers.Add(cmName, func(_ string, data map[string]string) error { - labels, err := extractLabelsFromConfigMap(data, cmPath) - if err != nil { - log.Warnf("ExternalLabels: failed to extract labels from ConfigMap %s/%s at path %q: %s", cmNamespace, cmName, cmPath, err) - return nil - } - log.Infof("ExternalLabels: loaded %d label(s) from ConfigMap %s/%s (path: %q)", len(labels), cmNamespace, cmName, cmPath) - for k, v := range labels { - log.Infof("ExternalLabels: %s = %s", k, v) - } - return nil - }) - extWatchers.Watch() + // If external labels discovery is enabled, find the ConfigMap labelled + // ibm.kubecost.com/external-labels="true" in the agent namespace and watch it. + var isExternalLabelsEnabled bool + if agentenv.IsExternalLabelsEnabled() { + isExternalLabelsEnabled = agentenv.IsExternalLabelsEnabled() } configWatchers.Watch() @@ -132,6 +113,7 @@ func NewOpenCostDataSource( clusterInfoProvider, clusterCache, nodeClient, + isExternalLabelsEnabled, ) return ds, nil } @@ -160,46 +142,3 @@ func NewOpenCostDataSource( return dataSource, cloudProvider } - -// extractLabelsFromConfigMap reads the "config.yaml" key from ConfigMap data, unmarshals it, -// and walks the dot-separated path to return the labels map. If path is empty the entire -// config.yaml content is expected to be a flat map[string]string. -func extractLabelsFromConfigMap(data map[string]string, path string) (map[string]string, error) { - raw, ok := data["config.yaml"] - if !ok { - return nil, fmt.Errorf("key \"config.yaml\" not found in ConfigMap data") - } - - // Unmarshal the YAML blob into a generic map. - var root map[string]interface{} - if err := yaml.Unmarshal([]byte(raw), &root); err != nil { - return nil, fmt.Errorf("failed to unmarshal config.yaml: %w", err) - } - - // Walk the dot-separated path to find the labels node. - var node interface{} = root - if path != "" { - for _, segment := range strings.Split(path, ".") { - m, ok := node.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("path segment %q: parent is not a map", segment) - } - node, ok = m[segment] - if !ok { - return nil, fmt.Errorf("path segment %q not found", segment) - } - } - } - - // The final node must be a map whose values are all strings. - rawMap, ok := node.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("value at path %q is not a map", path) - } - - labels := make(map[string]string, len(rawMap)) - for k, v := range rawMap { - labels[k] = fmt.Sprintf("%v", v) - } - return labels, nil -} diff --git a/pkg/env/unifiedagentenv.go b/pkg/env/unifiedagentenv.go index 87c13a99..a453e57a 100644 --- a/pkg/env/unifiedagentenv.go +++ b/pkg/env/unifiedagentenv.go @@ -52,10 +52,9 @@ const ( // ParseMetricDataEnvVar env var for sanitizing k8s resources ParseMetricDataEnvVar = "PARSE_METRIC_DATA" - // External labels ConfigMap configuration - ExternalLabelsConfigMapNameEnvVar = "EXTERNAL_LABELS_CONFIGMAP_NAME" - ExternalLabelsConfigMapNamespaceEnvVar = "EXTERNAL_LABELS_CONFIGMAP_NAMESPACE" - ExternalLabelsConfigMapPathEnvVar = "EXTERNAL_LABELS_CONFIGMAP_PATH" + // ExternalLabelsEnabledEnvVar enables auto-discovery of a ConfigMap labelled + // ibm.kubecost.com/external-labels="true" in the agent namespace. + ExternalLabelsEnabledEnvVar = "EXTERNAL_LABELS_ENABLED" // Prefixes for CloudabilityPrefix = "CLOUDABILITY_" @@ -197,18 +196,8 @@ func getValueWithPotentialPrefixOrDefault[T any](envVariable string, prefix stri return convert(envValue) } -// GetExternalLabelsConfigMapName returns the name of the ConfigMap containing external labels. -func GetExternalLabelsConfigMapName() string { - return env.Get(ExternalLabelsConfigMapNameEnvVar, "") -} - -// GetExternalLabelsConfigMapNamespace returns the namespace of the ConfigMap containing external labels. -func GetExternalLabelsConfigMapNamespace() string { - return env.Get(ExternalLabelsConfigMapNamespaceEnvVar, "") -} - -// GetExternalLabelsConfigMapPath returns the dot-separated YAML path within the ConfigMap's -// config.yaml key where the external labels map lives (e.g. "prometheusK8s.externalLabels"). -func GetExternalLabelsConfigMapPath() string { - return env.Get(ExternalLabelsConfigMapPathEnvVar, "") +// IsExternalLabelsEnabled returns true when the agent should auto-discover and watch +// a ConfigMap labelled ibm.kubecost.com/external-labels="true" in the agent namespace. +func IsExternalLabelsEnabled() bool { + return env.GetBool(ExternalLabelsEnabledEnvVar, false) } From 74963d2f02ead208c58c3b65db3bc9791d44be59 Mon Sep 17 00:00:00 2001 From: Alan Rodrigues Date: Mon, 29 Jun 2026 12:23:49 -0700 Subject: [PATCH 4/4] use relative path to just build it for now --- go.mod | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index ce5dee84..9bfade0b 100644 --- a/go.mod +++ b/go.mod @@ -228,8 +228,8 @@ require ( ) replace ( - github.com/opencost/opencost => /Users/alanrodrigues/devel/opencost - github.com/opencost/opencost/core => /Users/alanrodrigues/devel/opencost/core - github.com/opencost/opencost/modules/collector-source => /Users/alanrodrigues/devel/opencost/modules/collector-source - github.com/opencost/opencost/modules/prometheus-source => /Users/alanrodrigues/devel/opencost/modules/prometheus-source + github.com/opencost/opencost => ../opencost + github.com/opencost/opencost/core => ../opencost/core + github.com/opencost/opencost/modules/collector-source => ../opencost/modules/collector-source + github.com/opencost/opencost/modules/prometheus-source => ../opencost/modules/prometheus-source )