Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .chloggen/fix-anyconfig-deep-copy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: bug_fix

# 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: operator

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Fix AnyConfig.DeepCopyInto performing shallow copy, causing TargetAllocator Deployment infinite reconciliation loop"

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

# (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: |
AnyConfig.DeepCopyInto used maps.Copy which only copied top-level map entries, leaving nested
maps as shared references. When ApplyDefaults injected TLS profile settings (min_version) into
the collector's scrape config, it mutated the informer cache through the shared reference. This
caused the TargetAllocator config hash to alternate between two values on every reconciliation,
triggering an infinite Deployment update loop. The fix uses JSON round-tripping for a true deep copy.
19 changes: 19 additions & 0 deletions .chloggen/fix-events-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: bug_fix

# 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: operator

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add missing RBAC permission for events.k8s.io API group

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

# (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: |
The operator uses k8s.io/client-go/tools/events which targets the events.k8s.io API group,
but the ClusterRole only granted permission for the core API group. This caused "Server rejected
event" errors when recording events on managed resources in other namespaces.
22 changes: 18 additions & 4 deletions apis/v1beta1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,27 @@ type AnyConfig struct {
Object map[string]any `json:"-" yaml:",inline"`
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
// DeepCopyInto copies all properties into another AnyConfig instance.
// It performs a true deep copy by JSON round-tripping to ensure nested maps/slices
// are fully independent of the source, preventing shared-reference mutations.
func (c *AnyConfig) DeepCopyInto(out *AnyConfig) {
*out = *c
if c.Object != nil {
in, out := &c.Object, &out.Object
*out = make(map[string]any, len(*in))
maps.Copy((*out), *in)
b, err := json.Marshal(c.Object)
if err != nil {
// Fallback to shallow copy if JSON marshal fails.
in, out := &c.Object, &out.Object
*out = make(map[string]any, len(*in))
maps.Copy((*out), *in)
return
}
out.Object = make(map[string]any)
if err = json.Unmarshal(b, &out.Object); err != nil {
// Fallback to shallow copy if JSON unmarshal fails.
in, out := &c.Object, &out.Object
*out = make(map[string]any, len(*in))
maps.Copy((*out), *in)
}
}
}

Expand Down
66 changes: 66 additions & 0 deletions apis/v1beta1/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1480,3 +1480,69 @@ func TestTelemetryIncompleteConfigAppliesDefaults(t *testing.T) {
require.Equal(t, "0.0.0.0", *telemetry.Metrics.Readers[0].Pull.Exporter.Prometheus.Host)
require.Equal(t, 8888, *telemetry.Metrics.Readers[0].Pull.Exporter.Prometheus.Port)
}

func TestAnyConfigDeepCopyInto_NestedMapIndependence(t *testing.T) {
src := AnyConfig{Object: map[string]any{
"prometheus": map[string]any{
"config": map[string]any{
"scrape_configs": []any{
map[string]any{
"job_name": "kubelet",
"tls_config": map[string]any{
"ca_file": "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
"insecure_skip_verify": true,
},
},
},
},
},
}}

dst := src.DeepCopy()

// Mutate a nested map in the copy (simulates TLS profile injection).
scrapeConfigs := dst.Object["prometheus"].(map[string]any)["config"].(map[string]any)["scrape_configs"].([]any)
tlsConfig := scrapeConfigs[0].(map[string]any)["tls_config"].(map[string]any)
tlsConfig["min_version"] = "TLS12"

// Source nested map must be unaffected.
srcTLS := src.Object["prometheus"].(map[string]any)["config"].(map[string]any)["scrape_configs"].([]any)[0].(map[string]any)["tls_config"].(map[string]any)
assert.NotContains(t, srcTLS, "min_version", "DeepCopy must produce independent nested maps; source was mutated through the copy")
}

func TestAnyConfigDeepCopyInto_NilObject(t *testing.T) {
src := AnyConfig{Object: nil}
dst := src.DeepCopy()
assert.Nil(t, dst.Object)
}

func TestAnyConfigDeepCopyInto_EmptyObject(t *testing.T) {
src := AnyConfig{Object: map[string]any{}}
dst := src.DeepCopy()
assert.NotNil(t, dst.Object)
assert.Empty(t, dst.Object)
// Mutating dst should not affect src.
dst.Object["key"] = "value"
assert.Empty(t, src.Object)
}

func TestAnyConfigDeepCopyInto_PreservesValues(t *testing.T) {
src := AnyConfig{Object: map[string]any{
"string_val": "hello",
"number_val": float64(42),
"bool_val": true,
"nested": map[string]any{
"inner": "value",
"list": []any{"a", "b"},
},
}}

dst := src.DeepCopy()

assert.Equal(t, "hello", dst.Object["string_val"])
assert.Equal(t, float64(42), dst.Object["number_val"])
assert.Equal(t, true, dst.Object["bool_val"])
nested := dst.Object["nested"].(map[string]any)
assert.Equal(t, "value", nested["inner"])
assert.Equal(t, []any{"a", "b"}, nested["list"])
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ metadata:
categories: Logging & Tracing,Monitoring,Observability
certified: "false"
containerImage: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator
createdAt: "2026-03-27T16:38:21Z"
createdAt: "2026-04-10T09:34:47Z"
description: Provides the OpenTelemetry components, including the Collector
operators.operatorframework.io/builder: operator-sdk-v1.29.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
Expand Down Expand Up @@ -315,13 +315,6 @@ spec:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand All @@ -331,6 +324,14 @@ spec:
- get
- list
- watch
- apiGroups:
- ""
- events.k8s.io
resources:
- events
verbs:
- create
- patch
- apiGroups:
- apps
resources:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ metadata:
categories: Logging & Tracing,Monitoring,Observability
certified: "false"
containerImage: ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator
createdAt: "2026-03-27T16:38:22Z"
createdAt: "2026-04-10T09:34:52Z"
description: Provides the OpenTelemetry components, including the Collector
operators.operatorframework.io/builder: operator-sdk-v1.29.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
Expand Down Expand Up @@ -315,13 +315,6 @@ spec:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand All @@ -331,6 +324,14 @@ spec:
- get
- list
- watch
- apiGroups:
- ""
- events.k8s.io
resources:
- events
verbs:
- create
- patch
- apiGroups:
- apps
resources:
Expand Down
15 changes: 8 additions & 7 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand All @@ -39,6 +32,14 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
- events.k8s.io
resources:
- events
verbs:
- create
- patch
- apiGroups:
- apps
resources:
Expand Down
1 change: 1 addition & 0 deletions internal/controllers/clusterobservability_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func NewClusterObservabilityReconciler(params ClusterObservabilityReconcilerPara
//+kubebuilder:rbac:groups=opentelemetry.io,resources=instrumentations,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
//+kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func NewReconciler(p Params) *OpenTelemetryCollectorReconciler {

// +kubebuilder:rbac:groups="",resources=pods;configmaps;services;serviceaccounts,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=daemonsets;deployments;statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete
Expand Down
1 change: 1 addition & 0 deletions internal/controllers/targetallocator_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func NewTargetAllocatorReconciler(

// +kubebuilder:rbac:groups="",resources=pods;configmaps;services;serviceaccounts,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors;podmonitors,verbs=get;list;watch;create;update;patch;delete
Expand Down
13 changes: 13 additions & 0 deletions tests/e2e/smoke-simplest/00-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,16 @@ spec:
port: 4318
protocol: TCP
targetPort: 4318
---
# Verify the operator can emit events via the events.k8s.io API group.
apiVersion: events.k8s.io/v1
kind: Event
metadata:
namespace: ($namespace)
reason: Info
note: applied status changes
reportingController: opentelemetry-operator
regarding:
kind: OpenTelemetryCollector
name: simplest
type: Normal
Loading