Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject K8S_NODE_NAME environment variable when using the kubeletstats receiver #3389

Merged
merged 11 commits into from
Nov 1, 2024
16 changes: 16 additions & 0 deletions .chloggen/2779-kubeletstatsreiver-inject-en-vars.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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. collector, target allocator, auto-instrumentation, opamp, github action)
component: collector

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Inject environment K8S_NODE_NAME environment variable for the Kubelet Stats Receiver.

# One or more tracking issues related to the change
issues: [2779]

# (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:
40 changes: 40 additions & 0 deletions apis/v1beta1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,42 @@ func (c *Config) getPortsForComponentKinds(logger logr.Logger, componentKinds ..
return ports, nil
}

// getEnvironmentVariablesForComponentKinds gets the environment variables for the given ComponentKind(s).
func (c *Config) getEnvironmentVariablesForComponentKinds(logger logr.Logger, componentKinds ...ComponentKind) ([]corev1.EnvVar, error) {
jaronoff97 marked this conversation as resolved.
Show resolved Hide resolved
var envVars []corev1.EnvVar = []corev1.EnvVar{}
enabledComponents := c.GetEnabledComponents()
for _, componentKind := range componentKinds {
var retriever components.ParserRetriever
var cfg AnyConfig

switch componentKind {
case KindReceiver:
retriever = receivers.ReceiverFor
cfg = c.Receivers
case KindExporter:
continue
case KindProcessor:
continue
case KindExtension:
continue
}
for componentName := range enabledComponents[componentKind] {
parser := retriever(componentName)
if parsedEnvVars, err := parser.GetEnvironmentVariables(logger, cfg.Object[componentName]); err != nil {
return nil, err
} else {
envVars = append(envVars, parsedEnvVars...)
}
}
}

sort.Slice(envVars, func(i, j int) bool {
return envVars[i].Name < envVars[j].Name
})

return envVars, nil
}

// applyDefaultForComponentKinds applies defaults to the endpoints for the given ComponentKind(s).
func (c *Config) applyDefaultForComponentKinds(logger logr.Logger, componentKinds ...ComponentKind) error {
if err := c.Service.ApplyDefaults(); err != nil {
Expand Down Expand Up @@ -286,6 +322,10 @@ func (c *Config) GetAllPorts(logger logr.Logger) ([]corev1.ServicePort, error) {
return c.getPortsForComponentKinds(logger, KindReceiver, KindExporter)
}

func (c *Config) GetEnvironmentVariables(logger logr.Logger) ([]corev1.EnvVar, error) {
return c.getEnvironmentVariablesForComponentKinds(logger, KindReceiver)
}

func (c *Config) GetAllRbacRules(logger logr.Logger) ([]rbacv1.PolicyRule, error) {
return c.getRbacRulesForComponentKinds(logger, KindReceiver, KindExporter, KindProcessor)
}
Expand Down
60 changes: 60 additions & 0 deletions apis/v1beta1/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,66 @@ func TestConfig_GetEnabledComponents(t *testing.T) {
}
}

func TestConfig_getEnvironmentVariablesForComponentKinds(t *testing.T) {
tests := []struct {
name string
config *Config
componentKinds []ComponentKind
envVarsLen int
}{
{
name: "no env vars",
config: &Config{
Receivers: AnyConfig{
Object: map[string]interface{}{
"myreceiver": map[string]interface{}{
"env": "test",
},
},
},
Service: Service{
Pipelines: map[string]*Pipeline{
"test": {
Receivers: []string{"myreceiver"},
},
},
},
},
componentKinds: []ComponentKind{KindReceiver},
envVarsLen: 0,
},
{
name: "kubeletstats env vars",
config: &Config{
Receivers: AnyConfig{
Object: map[string]interface{}{
"kubeletstats": map[string]interface{}{},
},
},
Service: Service{
Pipelines: map[string]*Pipeline{
"test": {
Receivers: []string{"kubeletstats"},
},
},
},
},
componentKinds: []ComponentKind{KindReceiver},
envVarsLen: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := logr.Discard()
envVars, err := tt.config.GetEnvironmentVariables(logger)

assert.NoError(t, err)
assert.Len(t, envVars, tt.envVarsLen)
})
}
}

func TestConfig_GetReceiverPorts(t *testing.T) {
tests := []struct {
name string
Expand Down
8 changes: 7 additions & 1 deletion internal/components/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Settings[ComponentConfigType any] struct {
livenessGen ProbeGenerator[ComponentConfigType]
readinessGen ProbeGenerator[ComponentConfigType]
defaultsApplier Defaulter[ComponentConfigType]
envVarGen EnvVarGenerator[ComponentConfigType]
}

func NewEmptySettings[ComponentConfigType any]() *Settings[ComponentConfigType] {
Expand Down Expand Up @@ -124,7 +125,11 @@ func (b Builder[ComponentConfigType]) WithReadinessGen(readinessGen ProbeGenerat
o.readinessGen = readinessGen
})
}

func (b Builder[ComponentConfigType]) WithEnvVarGen(envVarGen EnvVarGenerator[ComponentConfigType]) Builder[ComponentConfigType] {
return append(b, func(o *Settings[ComponentConfigType]) {
o.envVarGen = envVarGen
})
}
func (b Builder[ComponentConfigType]) WithDefaultsApplier(defaultsApplier Defaulter[ComponentConfigType]) Builder[ComponentConfigType] {
return append(b, func(o *Settings[ComponentConfigType]) {
o.defaultsApplier = defaultsApplier
Expand All @@ -141,6 +146,7 @@ func (b Builder[ComponentConfigType]) Build() (*GenericParser[ComponentConfigTyp
name: o.name,
portParser: o.portParser,
rbacGen: o.rbacGen,
envVarGen: o.envVarGen,
livenessGen: o.livenessGen,
readinessGen: o.readinessGen,
defaultsApplier: o.defaultsApplier,
Expand Down
7 changes: 7 additions & 0 deletions internal/components/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type RBACRuleGenerator[ComponentConfigType any] func(logger logr.Logger, config
// It's expected that type Config is the configuration used by a parser.
type ProbeGenerator[ComponentConfigType any] func(logger logr.Logger, config ComponentConfigType) (*corev1.Probe, error)

// EnvVarGenerator is a function that generates a list of environment variables for a given config.
// It's expected that type Config is the configuration used by a parser.
type EnvVarGenerator[ComponentConfigType any] func(logger logr.Logger, config ComponentConfigType) ([]corev1.EnvVar, error)

// Defaulter is a function that applies given defaults to the passed Config.
// It's expected that type Config is the configuration used by a parser.
type Defaulter[ComponentConfigType any] func(logger logr.Logger, defaultAddr string, defaultPort int32, config ComponentConfigType) (map[string]interface{}, error)
Expand Down Expand Up @@ -105,6 +109,9 @@ type Parser interface {
// GetLivenessProbe returns a liveness probe set for the collector
GetLivenessProbe(logger logr.Logger, config interface{}) (*corev1.Probe, error)

// GetEnvironmentVariables returns a list of environment variables for the collector
GetEnvironmentVariables(logger logr.Logger, config interface{}) ([]corev1.EnvVar, error)

// GetReadinessProbe returns a readiness probe set for the collector
GetReadinessProbe(logger logr.Logger, config interface{}) (*corev1.Probe, error)

Expand Down
12 changes: 12 additions & 0 deletions internal/components/generic_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type GenericParser[T any] struct {
settings *Settings[T]
portParser PortParser[T]
rbacGen RBACRuleGenerator[T]
envVarGen EnvVarGenerator[T]
livenessGen ProbeGenerator[T]
readinessGen ProbeGenerator[T]
defaultsApplier Defaulter[T]
Expand Down Expand Up @@ -88,6 +89,17 @@ func (g *GenericParser[T]) GetRBACRules(logger logr.Logger, config interface{})
return g.rbacGen(logger, parsed)
}

func (g *GenericParser[T]) GetEnvironmentVariables(logger logr.Logger, config interface{}) ([]corev1.EnvVar, error) {
if g.envVarGen == nil {
return nil, nil
}
var parsed T
if err := mapstructure.Decode(config, &parsed); err != nil {
return nil, err
}
return g.envVarGen(logger, parsed)
}

func (g *GenericParser[T]) Ports(logger logr.Logger, name string, config interface{}) ([]corev1.ServicePort, error) {
if g.portParser == nil {
return nil, nil
Expand Down
4 changes: 4 additions & 0 deletions internal/components/multi_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ func (m *MultiPortReceiver) GetRBACRules(logr.Logger, interface{}) ([]rbacv1.Pol
return nil, nil
}

func (m *MultiPortReceiver) GetEnvironmentVariables(logger logr.Logger, config interface{}) ([]corev1.EnvVar, error) {
return nil, nil
}

type MultiPortBuilder[ComponentConfigType any] []Builder[ComponentConfigType]

func NewMultiPortReceiverBuilder(name string) MultiPortBuilder[*MultiProtocolEndpointConfig] {
Expand Down
1 change: 1 addition & 0 deletions internal/components/receivers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ var (
MustBuild(),
components.NewBuilder[kubeletStatsConfig]().WithName("kubeletstats").
WithRbacGen(generateKubeletStatsRbacRules).
WithEnvVarGen(generateKubeletStatsEnvVars).
MustBuild(),
NewScraperParser("prometheus"),
NewScraperParser("sshcheck"),
Expand Down
10 changes: 10 additions & 0 deletions internal/components/receivers/kubeletstats.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package receivers

import (
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
)

Expand All @@ -42,6 +43,15 @@ type kubeletStatsConfig struct {
AuthType string `mapstructure:"auth_type"`
}

func generateKubeletStatsEnvVars(_ logr.Logger, config kubeletStatsConfig) ([]corev1.EnvVar, error) {
// The documentation mentions that the K8S_NODE_NAME environment variable is required when using the serviceAccount auth type.
// Also, it mentions that it is a good idea to use it for the Read Only Endpoint. Added always to make it easier for users.
// https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/kubeletstatsreceiver/README.md
return []corev1.EnvVar{
{Name: "K8S_NODE_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "spec.nodeName"}}},
}, nil
}

func generateKubeletStatsRbacRules(_ logr.Logger, config kubeletStatsConfig) ([]rbacv1.PolicyRule, error) {
// The Kubelet Stats Receiver needs get permissions on the nodes/stats resources always.
prs := []rbacv1.PolicyRule{
Expand Down
6 changes: 6 additions & 0 deletions internal/manifests/collector/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ func Container(cfg config.Config, logger logr.Logger, otelcol v1beta1.OpenTeleme
)
}

if configEnvVars, err := otelcol.Spec.Config.GetEnvironmentVariables(logger); err != nil {
logger.Error(err, "could not get the environment variables from the config")
} else {
envVars = append(envVars, configEnvVars...)
}

envVars = append(envVars, proxy.ReadProxyVarsFromEnv()...)
return corev1.Container{
Name: naming.Container(),
Expand Down
21 changes: 21 additions & 0 deletions tests/e2e-automatic-rbac/receiver-kubeletstats/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,24 @@ subjects:
- kind: ServiceAccount
name: simplest-collector
namespace: chainsaw-kubeletstats
---
apiVersion: v1
kind: Pod
metadata:
labels:
app.kubernetes.io/component: opentelemetry-collector
app.kubernetes.io/instance: chainsaw-kubeletstats.simplest
app.kubernetes.io/managed-by: opentelemetry-operator
app.kubernetes.io/name: simplest-collector
app.kubernetes.io/part-of: opentelemetry
app.kubernetes.io/version: latest
namespace: chainsaw-kubeletstats
spec:
containers:
- name: otc-container
env:
- name: POD_NAME
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
Loading