diff --git a/.chloggen/resourcedetection-local-cluster.yaml b/.chloggen/resourcedetection-local-cluster.yaml new file mode 100644 index 000000000000..2227c7ef6d22 --- /dev/null +++ b/.chloggen/resourcedetection-local-cluster.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: resourcedetectionprocessor + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Introduce kubeadm detector to retrieve local cluster name." + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [35116] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/internal/metadataproviders/kubeadm/metadata.go b/internal/metadataproviders/kubeadm/metadata.go new file mode 100644 index 000000000000..2b594e54614c --- /dev/null +++ b/internal/metadataproviders/kubeadm/metadata.go @@ -0,0 +1,56 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubeadm // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/kubeadm" + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig" +) + +type Provider interface { + // ClusterName returns the current K8S cluster name + ClusterName(ctx context.Context) (string, error) +} + +type LocalCache struct { + ClusterName string +} + +type kubeadmProvider struct { + kubeadmClient kubernetes.Interface + configMapName string + configMapNamespace string + cache LocalCache +} + +func NewProvider(configMapName string, configMapNamespace string, apiConf k8sconfig.APIConfig) (Provider, error) { + k8sAPIClient, err := k8sconfig.MakeClient(apiConf) + if err != nil { + return nil, fmt.Errorf("failed to create K8s API client: %w", err) + } + return &kubeadmProvider{ + kubeadmClient: k8sAPIClient, + configMapName: configMapName, + configMapNamespace: configMapNamespace, + }, nil +} + +func (k *kubeadmProvider) ClusterName(ctx context.Context) (string, error) { + if k.cache.ClusterName != "" { + return k.cache.ClusterName, nil + } + configmap, err := k.kubeadmClient.CoreV1().ConfigMaps(k.configMapNamespace).Get(ctx, k.configMapName, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("failed to fetch ConfigMap with name %s and namespace %s from K8s API: %w", k.configMapName, k.configMapNamespace, err) + } + + k.cache.ClusterName = configmap.Data["clusterName"] + + return k.cache.ClusterName, nil +} diff --git a/internal/metadataproviders/kubeadm/metadata_test.go b/internal/metadataproviders/kubeadm/metadata_test.go new file mode 100644 index 000000000000..7f383b203a7e --- /dev/null +++ b/internal/metadataproviders/kubeadm/metadata_test.go @@ -0,0 +1,87 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubeadm + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig" +) + +func TestNewProvider(t *testing.T) { + // set k8s cluster env variables to make the API client happy + t.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") + t.Setenv("KUBERNETES_SERVICE_PORT", "6443") + + _, err := NewProvider("name", "ns", k8sconfig.APIConfig{AuthType: k8sconfig.AuthTypeNone}) + assert.NoError(t, err) +} + +func TestClusterName(t *testing.T) { + client := fake.NewSimpleClientset() + err := setupConfigMap(client) + assert.NoError(t, err) + + tests := []struct { + testName string + CMname string + CMnamespace string + clusterName string + errMsg string + }{ + { + testName: "valid", + CMname: "cm", + CMnamespace: "ns", + clusterName: "myClusterName", + errMsg: "", + }, + { + testName: "configmap not found", + CMname: "cm2", + CMnamespace: "ns", + errMsg: "failed to fetch ConfigMap with name cm2 and namespace ns from K8s API: configmaps \"cm2\" not found", + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + kubeadmP := &kubeadmProvider{ + kubeadmClient: client, + configMapName: tt.CMname, + configMapNamespace: tt.CMnamespace, + } + clusterName, err := kubeadmP.ClusterName(context.Background()) + if tt.errMsg != "" { + assert.EqualError(t, err, tt.errMsg) + } else { + assert.NoError(t, err) + assert.Equal(t, clusterName, tt.clusterName) + } + }) + } +} + +func setupConfigMap(client *fake.Clientset) error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cm", + Namespace: "ns", + }, + Data: map[string]string{ + "clusterName": "myClusterName", + }, + } + _, err := client.CoreV1().ConfigMaps("ns").Create(context.Background(), cm, metav1.CreateOptions{}) + if err != nil { + return err + } + return nil +} diff --git a/internal/metadataproviders/kubeadm/package_test.go b/internal/metadataproviders/kubeadm/package_test.go new file mode 100644 index 000000000000..48baf7f293d0 --- /dev/null +++ b/internal/metadataproviders/kubeadm/package_test.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubeadm + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index cf16a5abc9e8..ba5820ecf854 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -426,6 +426,43 @@ processors: override: false ``` +### Kubeadm Metadata + +Queries the K8S API server to retrieve kubeadm resource attributes: + +The list of the populated resource attributes can be found at [kubeadm Detector Resource Attributes](./internal/kubeadm/documentation.md). + +The following permissions are required: +```yaml +kind: Role +metadata: + name: otel-collector + namespace: kube-system +rules: + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["kubeadm-config"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: otel-collector-rolebinding + namespace: kube-system +subjects: +- kind: ServiceAccount + name: default + namespace: default +roleRef: + kind: Role + name: otel-collector + apiGroup: rbac.authorization.k8s.io +``` + +| Name | Type | Required | Default | Docs | +| ---- | ---- |----------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| auth_type | string | No | `serviceAccount` | How to authenticate to the K8s API server. This can be one of `none` (for no auth), `serviceAccount` (to use the standard service account token provided to the agent pod), or `kubeConfig` to use credentials from `~/.kube/config`. | + ### K8S Node Metadata Queries the K8S api server to retrieve node resource attributes. diff --git a/processor/resourcedetectionprocessor/config.go b/processor/resourcedetectionprocessor/config.go index 78fb07a423ba..06cb8526dc1a 100644 --- a/processor/resourcedetectionprocessor/config.go +++ b/processor/resourcedetectionprocessor/config.go @@ -19,6 +19,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/heroku" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/k8snode" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/kubeadm" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" ) @@ -85,6 +86,9 @@ type DetectorConfig struct { // K8SNode contains user-specified configurations for the K8SNode detector K8SNodeConfig k8snode.Config `mapstructure:"k8snode"` + + // Kubeadm contains user-specified configurations for the Kubeadm detector + KubeadmConfig kubeadm.Config `mapstructure:"kubeadm"` } func detectorCreateDefaultConfig() DetectorConfig { @@ -103,6 +107,7 @@ func detectorCreateDefaultConfig() DetectorConfig { SystemConfig: system.CreateDefaultConfig(), OpenShiftConfig: openshift.CreateDefaultConfig(), K8SNodeConfig: k8snode.CreateDefaultConfig(), + KubeadmConfig: kubeadm.CreateDefaultConfig(), } } @@ -136,6 +141,8 @@ func (d *DetectorConfig) GetConfigFromType(detectorType internal.DetectorType) i return d.OpenShiftConfig case k8snode.TypeStr: return d.K8SNodeConfig + case kubeadm.TypeStr: + return d.KubeadmConfig default: return nil } diff --git a/processor/resourcedetectionprocessor/doc.go b/processor/resourcedetectionprocessor/doc.go index 67a30eb31cf3..91c606b11568 100644 --- a/processor/resourcedetectionprocessor/doc.go +++ b/processor/resourcedetectionprocessor/doc.go @@ -16,6 +16,7 @@ //go:generate mdatagen internal/openshift/metadata.yaml //go:generate mdatagen internal/system/metadata.yaml //go:generate mdatagen internal/k8snode/metadata.yaml +//go:generate mdatagen internal/kubeadm/metadata.yaml // package resourcedetectionprocessor implements a processor // which can be used to detect resource information from the host, diff --git a/processor/resourcedetectionprocessor/factory.go b/processor/resourcedetectionprocessor/factory.go index 05ff7934f81b..116d431b981e 100644 --- a/processor/resourcedetectionprocessor/factory.go +++ b/processor/resourcedetectionprocessor/factory.go @@ -32,6 +32,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/heroku" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/k8snode" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/kubeadm" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" @@ -66,6 +67,7 @@ func NewFactory() processor.Factory { system.TypeStr: system.NewDetector, openshift.TypeStr: openshift.NewDetector, k8snode.TypeStr: k8snode.NewDetector, + kubeadm.TypeStr: kubeadm.NewDetector, }) f := &factory{ diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/config.go b/processor/resourcedetectionprocessor/internal/kubeadm/config.go new file mode 100644 index 000000000000..c9932dc332d5 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/config.go @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubeadm // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/kubeadm" + +import ( + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata" +) + +type Config struct { + k8sconfig.APIConfig `mapstructure:",squash"` + ResourceAttributes metadata.ResourceAttributesConfig `mapstructure:"resource_attributes"` +} + +func CreateDefaultConfig() Config { + return Config{ + APIConfig: k8sconfig.APIConfig{AuthType: k8sconfig.AuthTypeServiceAccount}, + ResourceAttributes: metadata.DefaultResourceAttributesConfig(), + } +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/documentation.md b/processor/resourcedetectionprocessor/internal/kubeadm/documentation.md new file mode 100644 index 000000000000..5e2c0561a2ad --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/documentation.md @@ -0,0 +1,11 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# resourcedetectionprocessor/kubeadm + +**Parent Component:** resourcedetection + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| k8s.cluster.name | The Kubernetes cluster name | Any Str | true | diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/generated_package_test.go b/processor/resourcedetectionprocessor/internal/kubeadm/generated_package_test.go new file mode 100644 index 000000000000..4dc8cc67e4da --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/generated_package_test.go @@ -0,0 +1,13 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package kubeadm + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_config.go b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_config.go new file mode 100644 index 000000000000..cb9d4839877d --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_config.go @@ -0,0 +1,39 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/confmap" +) + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(rac) + if err != nil { + return err + } + rac.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// ResourceAttributesConfig provides config for resourcedetectionprocessor/kubeadm resource attributes. +type ResourceAttributesConfig struct { + K8sClusterName ResourceAttributeConfig `mapstructure:"k8s.cluster.name"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + K8sClusterName: ResourceAttributeConfig{ + Enabled: true, + }, + } +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_config_test.go b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_config_test.go new file mode 100644 index 000000000000..c2ed830cc2ad --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_config_test.go @@ -0,0 +1,56 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func TestResourceAttributesConfig(t *testing.T) { + tests := []struct { + name string + want ResourceAttributesConfig + }{ + { + name: "default", + want: DefaultResourceAttributesConfig(), + }, + { + name: "all_set", + want: ResourceAttributesConfig{ + K8sClusterName: ResourceAttributeConfig{Enabled: true}, + }, + }, + { + name: "none_set", + want: ResourceAttributesConfig{ + K8sClusterName: ResourceAttributeConfig{Enabled: false}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + sub, err = sub.Sub("resource_attributes") + require.NoError(t, err) + cfg := DefaultResourceAttributesConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_resource.go b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_resource.go new file mode 100644 index 000000000000..83834dfd62c8 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_resource.go @@ -0,0 +1,36 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. +// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. +type ResourceBuilder struct { + config ResourceAttributesConfig + res pcommon.Resource +} + +// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. +func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { + return &ResourceBuilder{ + config: rac, + res: pcommon.NewResource(), + } +} + +// SetK8sClusterName sets provided value as "k8s.cluster.name" attribute. +func (rb *ResourceBuilder) SetK8sClusterName(val string) { + if rb.config.K8sClusterName.Enabled { + rb.res.Attributes().PutStr("k8s.cluster.name", val) + } +} + +// Emit returns the built resource and resets the internal builder state. +func (rb *ResourceBuilder) Emit() pcommon.Resource { + r := rb.res + rb.res = pcommon.NewResource() + return r +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_resource_test.go b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_resource_test.go new file mode 100644 index 000000000000..c43337278947 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/generated_resource_test.go @@ -0,0 +1,40 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourceBuilder(t *testing.T) { + for _, tt := range []string{"default", "all_set", "none_set"} { + t.Run(tt, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt) + rb := NewResourceBuilder(cfg) + rb.SetK8sClusterName("k8s.cluster.name-val") + + res := rb.Emit() + assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource + + switch tt { + case "default": + assert.Equal(t, 1, res.Attributes().Len()) + case "all_set": + assert.Equal(t, 1, res.Attributes().Len()) + case "none_set": + assert.Equal(t, 0, res.Attributes().Len()) + return + default: + assert.Failf(t, "unexpected test case: %s", tt) + } + + val, ok := res.Attributes().Get("k8s.cluster.name") + assert.True(t, ok) + if ok { + assert.EqualValues(t, "k8s.cluster.name-val", val.Str()) + } + }) + } +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/package_test.go b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/package_test.go new file mode 100644 index 000000000000..1aba5ec4bb0b --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/package_test.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package metadata + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/testdata/config.yaml b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/testdata/config.yaml new file mode 100644 index 000000000000..4017b2f2932c --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata/testdata/config.yaml @@ -0,0 +1,9 @@ +default: +all_set: + resource_attributes: + k8s.cluster.name: + enabled: true +none_set: + resource_attributes: + k8s.cluster.name: + enabled: false diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/kubeadm.go b/processor/resourcedetectionprocessor/internal/kubeadm/kubeadm.go new file mode 100644 index 000000000000..ca39c6dde262 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/kubeadm.go @@ -0,0 +1,61 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubeadm // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/kubeadm" + +import ( + "context" + "fmt" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/processor" + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + "go.uber.org/zap" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/kubeadm" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/kubeadm/internal/metadata" +) + +const ( + TypeStr = "kubeadm" + defaultConfigMapName = "kubeadm-config" + defaultConfigMapNamespace = "kube-system" +) + +var _ internal.Detector = (*detector)(nil) + +type detector struct { + provider kubeadm.Provider + logger *zap.Logger + ra *metadata.ResourceAttributesConfig + rb *metadata.ResourceBuilder +} + +func NewDetector(set processor.Settings, dcfg internal.DetectorConfig) (internal.Detector, error) { + cfg := dcfg.(Config) + + kubeadmProvider, err := kubeadm.NewProvider(defaultConfigMapName, defaultConfigMapNamespace, cfg.APIConfig) + if err != nil { + return nil, fmt.Errorf("failed creating kubeadm provider: %w", err) + } + + return &detector{ + provider: kubeadmProvider, + logger: set.Logger, + ra: &cfg.ResourceAttributes, + rb: metadata.NewResourceBuilder(cfg.ResourceAttributes), + }, nil +} + +func (d *detector) Detect(ctx context.Context) (resource pcommon.Resource, schemaURL string, err error) { + if d.ra.K8sClusterName.Enabled { + clusterName, err := d.provider.ClusterName(ctx) + if err != nil { + return pcommon.NewResource(), "", fmt.Errorf("failed getting k8s cluster name: %w", err) + } + d.rb.SetK8sClusterName(clusterName) + } + + return d.rb.Emit(), conventions.SchemaURL, nil +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/kubeadm_test.go b/processor/resourcedetectionprocessor/internal/kubeadm/kubeadm_test.go new file mode 100644 index 000000000000..a7125552bc2a --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/kubeadm_test.go @@ -0,0 +1,75 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kubeadm + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/processor/processortest" + conventions "go.opentelemetry.io/collector/semconv/v1.6.1" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig" + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/kubeadm" +) + +var _ kubeadm.Provider = (*mockMetadata)(nil) + +type mockMetadata struct { + mock.Mock +} + +func (m *mockMetadata) ClusterName(_ context.Context) (string, error) { + args := m.MethodCalled("ClusterName") + return args.String(0), args.Error(1) +} + +func TestDetect(t *testing.T) { + md := &mockMetadata{} + md.On("ClusterName").Return("cluster-1", nil) + cfg := CreateDefaultConfig() + // set k8s cluster env variables and auth type to create a dummy API client + cfg.APIConfig.AuthType = k8sconfig.AuthTypeNone + t.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") + t.Setenv("KUBERNETES_SERVICE_PORT", "6443") + + k8sDetector, err := NewDetector(processortest.NewNopSettings(), cfg) + require.NoError(t, err) + k8sDetector.(*detector).provider = md + res, schemaURL, err := k8sDetector.Detect(context.Background()) + require.NoError(t, err) + assert.Equal(t, conventions.SchemaURL, schemaURL) + md.AssertExpectations(t) + + expected := map[string]any{ + conventions.AttributeK8SClusterName: "cluster-1", + } + + assert.Equal(t, expected, res.Attributes().AsRaw()) +} + +func TestDetectDisabledResourceAttributes(t *testing.T) { + md := &mockMetadata{} + cfg := CreateDefaultConfig() + cfg.ResourceAttributes.K8sClusterName.Enabled = false + // set k8s cluster env variables and auth type to create a dummy API client + cfg.APIConfig.AuthType = k8sconfig.AuthTypeNone + t.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") + t.Setenv("KUBERNETES_SERVICE_PORT", "6443") + + k8sDetector, err := NewDetector(processortest.NewNopSettings(), cfg) + require.NoError(t, err) + k8sDetector.(*detector).provider = md + res, schemaURL, err := k8sDetector.Detect(context.Background()) + require.NoError(t, err) + assert.Equal(t, conventions.SchemaURL, schemaURL) + md.AssertExpectations(t) + + expected := map[string]any{} + + assert.Equal(t, expected, res.Attributes().AsRaw()) +} diff --git a/processor/resourcedetectionprocessor/internal/kubeadm/metadata.yaml b/processor/resourcedetectionprocessor/internal/kubeadm/metadata.yaml new file mode 100644 index 000000000000..ed2518a06686 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/kubeadm/metadata.yaml @@ -0,0 +1,9 @@ +type: resourcedetectionprocessor/kubeadm + +parent: resourcedetection + +resource_attributes: + k8s.cluster.name: + description: The Kubernetes cluster name + type: string + enabled: true