Repository: open-telemetry/opentelemetry-operator

Issue:
Jaeger query config error in operator 0.122.0
### Component(s)

collector

### What happened?

My apologies if this is posted to the wrong project. I feel that it could belong on any of `opentelemetry-collector`, `opentelemetry-collector-contrib`, `jaeger`, or `jaeger-operator`, but the problem is most narrowly reproduced when I upgrade the operator, so I am posting here

## Description

Upon upgrading the opentelemetry operator from v0.121.0 to v0.122.0, a previously functional configuration running the jaeger query extension begins to fail due to:
```
2025/04/14 17:52:46 failed to get config: cannot unmarshal the configuration: decoding failed due to the following error(s):

error decoding 'extensions': error reading configuration for "jaeger_query": decoding failed due to the following error(s):

'' has invalid keys: endpoint
stream closed EOF for default/jaeger-query-only-collector-f449f48b9-f4w74 (otc-container)
```

even though no `endpoint` field is configured. This appears to be some sort of re-occurrence of https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/5081.

## Steps to Reproduce

Beginning with a fresh minikube:

1. kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.1/cert-manager.yaml
2. kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/download/v0.121.0/opentelemetry-operator.yaml
3. kubectl apply -f ./basic-reproduction.yml
4. Observe that the basic version works on v0.121.0
5. kubectl delete -f ./basic-reproduction.yml
6. kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/download/v0.122.0/opentelemetry-operator.yaml
7. kubectl apply -f ./basic-reproduction.yml
8. Observe that it no longer works


```yaml
# basic-reproduction.yml
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: jaeger-query-only
spec:
  image: jaegertracing/jaeger:2.5.0
  config:
    service:
      extensions: [jaeger_query, jaeger_storage]
# The OLTP receiver pipeline is only present because the collector crashes if it has no pipelines. It has no meaning on the main error as far as I know.
      pipelines: 
        traces:
          receivers: [otlp]
          exporters: [otlp]
    extensions:
      jaeger_query:
        storage:
          traces: local_memory
      jaeger_storage:
        backends:
          local_memory:
            memory:
              max_traces: 10000
    processors: {}
    receivers: 
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4137
    exporters: 
      otlp:
        endpoint: otelcol:4137

```

## Expected Result

The chart to cleanly install on v0.122.0

## Actual Result

The above error message and a crash.

### Kubernetes Version

1.30.1

### Operator version

v0.122.0

### Collector version

Not sure, jaeger 2.5.0. 

### Environment information

_No response_

### Log output

```shell
2025/04/14 17:55:35 application version: git-commit=, git-version=, build-date=
Error: failed to get config: cannot unmarshal the configuration: decoding failed due to the following error(s):

error decoding 'extensions': error reading configuration for "jaeger_query": decoding failed due to the following error(s):

'' has invalid keys: endpoint
2025/04/14 17:55:35 failed to get config: cannot unmarshal the configuration: decoding failed due to the following error(s):

error decoding 'extensions': error reading configuration for "jaeger_query": decoding failed due to the following error(s):

'' has invalid keys: endpoint
stream closed EOF for default/jaeger-query-only-collector-f449f48b9-f4w74 (otc-container)
```

### Additional context

Sorry that I can't tell you what collector version is running under the hood when I install jaeger 2.5.0. The release notes don't make that obvious to me. https://github.com/jaegertracing/jaeger/releases/tag/v1.68.0

Hints from issue comments:
@pavolloffay: Could you please paste here collecto config map? @pavolloffay: ```yaml
k get cm jaeger-query-only-collector-7892e3cf -o yaml                                                                                                                                                                                                                                                               ploffay@fedora
apiVersion: v1
data:
  collector.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4137
    exporters:
      otlp:
        endpoint: otelcol:4137
    processors: {}
    extensions:
      jaeger_query:
        endpoint: 0.0.0.0:16686
        storage:
          traces: local_memory
      jaeger_storage:
        backends:
          local_memory:
            memory:
              max_traces: 10000
    service:
      extensions:
        - jaeger_query
        - jaeger_storage
      telemetry:
        metrics:
          readers:
            - pull:
                exporter:
                  prometheus:
                    host: 0.0.0.0
                    port: 8888
      pipelines:
        traces:
          exporters:
            - otlp
          receivers:
            - otlp
kind: ConfigMap
metadata:
  creationTimestamp: "2025-04-15T09:30:59Z"
  labels:
    app.kubernetes.io/component: opentelemetry-collector
    app.kubernetes.io/instance: default.jaeger-query-only
    app.kubernetes.io/managed-by: opentelemetry-operator
    app.kubernetes.io/name: jaeger-query-only-collector
    app.kubernetes.io/part-of: opentelemetry
    app.kubernetes.io/version: 2.5.0
  name: jaeger-query-only-collector-7892e3cf
  namespace: default
  ownerReferences:
  - apiVersion: opentelemetry.io/v1beta1
    blockOwnerDeletion: true
    controller: true
    kind: OpenTelemetryCollector
    name: jaeger-query-only
    uid: e2705b15-1d73-4e03-b2a4-e4de5e044e6c
  resourceVersion: "1904"
  uid: 2c151611-da74-4e55-b302-70cad4e2a36
```

The `endpoint` in the `jaeger_query` extension is under `http` or `grpc` https://github.com/jaegertracing/jaeger/blob/d3f6682741762ec69cad715c8e656982e1e48ea4/cmd/query/app/flags.go#L70 

This should be fixed on the operator. We should also add e2e test for Jaeger.@pavolloffay: I have submitted a fix https://github.com/open-telemetry/opentelemetry-operator/pull/3908 @dberardo-com: is there any workaround available ? i would like to try out jaeger collector today and i am a bit stuck ... is there anything i can downgrade to make it work while waiting for the new release?

Solution patch:
diff --git a/.chloggen/fix-jaeger.yaml b/.chloggen/fix-jaeger.yaml
new file mode 100755
index 00000000..1ac7ab1d
--- /dev/null
+++ b/.chloggen/fix-jaeger.yaml
@@ -0,0 +1,18 @@
+# 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: collector
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Fix Jaeger query extension default endpoint
+
+# One or more tracking issues related to the change
+issues: [3906]
+
+# (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 0.122.0 introduced a fix to add a default endpoint. However, for Jaeger query extension the endpoint
+  was put in the wrong place, causing collector to fail to start due to an incorrect config.
diff --git a/internal/components/extensions/helpers.go b/internal/components/extensions/helpers.go
index 3b8ff264..9d7cbd59 100644
--- a/internal/components/extensions/helpers.go
+++ b/internal/components/extensions/helpers.go
@@ -23,8 +23,7 @@ var registry = map[string]components.Parser{
 			return components.ParseSingleEndpointSilent(logger, name, defaultPort, &config.SingleEndpointConfig)
 		}).
 		MustBuild(),
-	"jaeger_query": components.NewSinglePortParserBuilder("jaeger_query", 16686).
-		WithTargetPort(16686).
+	"jaeger_query": NewJaegerQueryExtensionParserBuilder().
 		MustBuild(),
 }
 
diff --git a/internal/components/extensions/jaeger_query_extension.go b/internal/components/extensions/jaeger_query_extension.go
new file mode 100644
index 00000000..60735a9c
--- /dev/null
+++ b/internal/components/extensions/jaeger_query_extension.go
@@ -0,0 +1,88 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensions
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/go-logr/logr"
+	"github.com/mitchellh/mapstructure"
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/open-telemetry/opentelemetry-operator/internal/components"
+	"github.com/open-telemetry/opentelemetry-operator/internal/naming"
+)
+
+const (
+	name = "jaeger_query"
+	port = 16686
+)
+
+var (
+	_ components.Parser = &components.GenericParser[*JaegerQueryExtensionConfig]{}
+)
+
+type JaegerQueryExtensionConfig struct {
+	HTTP jaegerHTTPAddress `mapstructure:"http,omitempty" yaml:"http,omitempty"`
+}
+
+type jaegerHTTPAddress struct {
+	Endpoint string `mapstructure:"endpoint,omitempty" yaml:"endpoint,omitempty"`
+}
+
+func (g *JaegerQueryExtensionConfig) GetPortNumOrDefault(logger logr.Logger, p int32) int32 {
+	num, err := g.GetPortNum()
+	if err != nil {
+		logger.V(3).Info("no port set, using default: %d", p)
+		return p
+	}
+	return num
+}
+
+// GetPortNum attempts to get the port for the given config. If it cannot, the UnsetPort and the given missingPortError
+// are returned.
+func (g *JaegerQueryExtensionConfig) GetPortNum() (int32, error) {
+	if len(g.HTTP.Endpoint) > 0 {
+		return components.PortFromEndpoint(g.HTTP.Endpoint)
+	}
+	return components.UnsetPort, components.PortNotFoundErr
+}
+
+func ParseJaegerQueryExtensionConfig(logger logr.Logger, name string, defaultPort *corev1.ServicePort, cfg *JaegerQueryExtensionConfig) ([]corev1.ServicePort, error) {
+	if cfg == nil {
+		return nil, nil
+	}
+	if _, err := cfg.GetPortNum(); err != nil && defaultPort.Port == components.UnsetPort {
+		logger.WithValues("receiver", defaultPort.Name).Error(err, "couldn't parse the endpoint's port and no default port set")
+		return []corev1.ServicePort{}, err
+	}
+	port := cfg.GetPortNumOrDefault(logger, defaultPort.Port)
+	svcPort := defaultPort
+	svcPort.Name = naming.PortName(name, port)
+	return []corev1.ServicePort{components.ConstructServicePort(svcPort, port)}, nil
+}
+
+func NewJaegerQueryExtensionParserBuilder() components.Builder[*JaegerQueryExtensionConfig] {
+	return components.NewBuilder[*JaegerQueryExtensionConfig]().WithPort(port).WithName(name).WithPortParser(ParseJaegerQueryExtensionConfig).WithDefaultsApplier(endpointDefaulter).WithDefaultRecAddress(components.DefaultRecAddress).WithTargetPort(port)
+}
+
+func endpointDefaulter(_ logr.Logger, defaultRecAddr string, port int32, config *JaegerQueryExtensionConfig) (map[string]interface{}, error) {
+	if config == nil {
+		config = &JaegerQueryExtensionConfig{}
+	}
+
+	if config.HTTP.Endpoint == "" {
+		config.HTTP.Endpoint = fmt.Sprintf("%s:%d", defaultRecAddr, port)
+	} else {
+		v := strings.Split(config.HTTP.Endpoint, ":")
+		if len(v) < 2 || v[0] == "" {
+			config.HTTP.Endpoint = fmt.Sprintf("%s:%s", defaultRecAddr, v[len(v)-1])
+		}
+	}
+
+	res := make(map[string]interface{})
+	err := mapstructure.Decode(config, &res)
+	return res, err
+}



Test patch:
diff --git a/internal/components/extensions/jaeger_query_extension_test.go b/internal/components/extensions/jaeger_query_extension_test.go
new file mode 100644
index 00000000..e0c9e7e1
--- /dev/null
+++ b/internal/components/extensions/jaeger_query_extension_test.go
@@ -0,0 +1,64 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package extensions
+
+import (
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func TestJaegerQueryExtensionParser(t *testing.T) {
+	jaegerBuilder := NewJaegerQueryExtensionParserBuilder()
+	genericBuilder, err := jaegerBuilder.Build()
+	require.NoError(t, err)
+
+	assert.Equal(t, "jaeger_query", genericBuilder.ParserType())
+	assert.Equal(t, "__jaeger_query", genericBuilder.ParserName())
+
+	defaultCfg, err := genericBuilder.GetDefaultConfig(logr.Discard(), nil)
+	require.NoError(t, err)
+
+	ports, err := genericBuilder.Ports(logr.Discard(), "jaeger_query", defaultCfg)
+	require.NoError(t, err)
+	assert.Equal(t, []corev1.ServicePort{{
+		Name:       "jaeger-query",
+		Port:       16686,
+		TargetPort: intstr.FromInt32(16686),
+	}}, ports)
+}
+
+func TestJaegerQueryExtensionParser_config(t *testing.T) {
+	jaegerBuilder := NewJaegerQueryExtensionParserBuilder()
+	genericBuilder, err := jaegerBuilder.Build()
+	require.NoError(t, err)
+
+	tests := []struct {
+		name   string
+		config interface{}
+		want   interface{}
+	}{
+		{
+			name:   "valid config",
+			config: map[string]interface{}{"http": map[string]interface{}{"endpoint": "127.0.0.0:16686"}},
+			want:   map[string]interface{}{"http": map[string]interface{}{"endpoint": "127.0.0.0:16686"}},
+		},
+		{
+			name: "missing config",
+			want: map[string]interface{}{"http": map[string]interface{}{"endpoint": "0.0.0.0:16686"}},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			cfg, errCfg := genericBuilder.GetDefaultConfig(logr.Discard(), test.config)
+			assert.Equal(t, test.want, cfg)
+			require.NoError(t, errCfg)
+		})
+	}
+}
diff --git a/tests/e2e/extension/00-assert-jaeger-extension.yaml b/tests/e2e/extension/00-assert-jaeger-extension.yaml
index c62406a1..d6904d28 100644
--- a/tests/e2e/extension/00-assert-jaeger-extension.yaml
+++ b/tests/e2e/extension/00-assert-jaeger-extension.yaml
@@ -1,29 +1,26 @@
-apiVersion: v1
-items:
-- apiVersion: apps/v1
-  kind: Deployment
-  metadata:
-    name: jaeger-inmemory-collector
-  spec:
-    template:
-      spec:
-        containers:
-        - ports:
-          - containerPort: 16686
-            name: jaeger-query
-            protocol: TCP
-          - containerPort: 8888
-            name: metrics
-            protocol: TCP
-          - containerPort: 4317
-            name: otlp-grpc
-            protocol: TCP
-          - containerPort: 4318
-            name: otlp-http
-            protocol: TCP
-kind: List
+apiVersion: apps/v1
+kind: Deployment
 metadata:
-  resourceVersion: ""
+  name: jaeger-inmemory-collector
+spec:
+  template:
+    spec:
+      containers:
+      - ports:
+        - containerPort: 16686
+          name: jaeger-query
+          protocol: TCP
+        - containerPort: 8888
+          name: metrics
+          protocol: TCP
+        - containerPort: 4317
+          name: otlp-grpc
+          protocol: TCP
+        - containerPort: 4318
+          name: otlp-http
+          protocol: TCP
+status:
+  readyReplicas: 1
 ---
 apiVersion: v1
 kind: Service