From 6ae647a5b210b459b42167a87d28cc7f471ab48a Mon Sep 17 00:00:00 2001 From: Ankit Kurmi Date: Wed, 27 Nov 2024 22:44:36 +0530 Subject: [PATCH] Create service for extensions (#3403) * feat: create service for extensions Signed-off-by: Ankit152 * chore: added extension service in manifest factories Signed-off-by: Ankit152 * chore: added unit test for extension service function Signed-off-by: Ankit152 * chore: added e2e tests for extensions Signed-off-by: Ankit152 --------- Signed-off-by: Ankit152 --- .chloggen/service-extension.yaml | 16 ++ apis/v1beta1/config.go | 17 +- internal/components/extensions/helpers.go | 3 + internal/manifests/collector/collector.go | 1 + internal/manifests/collector/service.go | 38 +++- internal/manifests/collector/service_test.go | 201 +++++++++++++++++++ internal/naming/main.go | 5 + tests/e2e/extension/00-assert.yaml | 140 +++++++++++++ tests/e2e/extension/00-install.yaml | 30 +++ tests/e2e/extension/chainsaw-test.yaml | 14 ++ 10 files changed, 461 insertions(+), 4 deletions(-) create mode 100755 .chloggen/service-extension.yaml create mode 100644 tests/e2e/extension/00-assert.yaml create mode 100644 tests/e2e/extension/00-install.yaml create mode 100644 tests/e2e/extension/chainsaw-test.yaml diff --git a/.chloggen/service-extension.yaml b/.chloggen/service-extension.yaml new file mode 100755 index 0000000000..d182754f46 --- /dev/null +++ b/.chloggen/service-extension.yaml @@ -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: support for creating a service for extensions when ports are specified. + +# One or more tracking issues related to the change +issues: [3460] + +# (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: diff --git a/apis/v1beta1/config.go b/apis/v1beta1/config.go index 82fd9a1325..5cb9150513 100644 --- a/apis/v1beta1/config.go +++ b/apis/v1beta1/config.go @@ -206,7 +206,12 @@ func (c *Config) getPortsForComponentKinds(logger logr.Logger, componentKinds .. case KindProcessor: continue case KindExtension: - continue + retriever = extensions.ParserFor + if c.Extensions == nil { + cfg = AnyConfig{} + } else { + cfg = *c.Extensions + } } for componentName := range enabledComponents[componentKind] { // TODO: Clean up the naming here and make it simpler to use a retriever. @@ -318,10 +323,18 @@ func (c *Config) GetExporterPorts(logger logr.Logger) ([]corev1.ServicePort, err return c.getPortsForComponentKinds(logger, KindExporter) } -func (c *Config) GetAllPorts(logger logr.Logger) ([]corev1.ServicePort, error) { +func (c *Config) GetExtensionPorts(logger logr.Logger) ([]corev1.ServicePort, error) { + return c.getPortsForComponentKinds(logger, KindExtension) +} + +func (c *Config) GetReceiverAndExporterPorts(logger logr.Logger) ([]corev1.ServicePort, error) { return c.getPortsForComponentKinds(logger, KindReceiver, KindExporter) } +func (c *Config) GetAllPorts(logger logr.Logger) ([]corev1.ServicePort, error) { + return c.getPortsForComponentKinds(logger, KindReceiver, KindExporter, KindExtension) +} + func (c *Config) GetEnvironmentVariables(logger logr.Logger) ([]corev1.EnvVar, error) { return c.getEnvironmentVariablesForComponentKinds(logger, KindReceiver) } diff --git a/internal/components/extensions/helpers.go b/internal/components/extensions/helpers.go index d05a04f3d9..87708a60e1 100644 --- a/internal/components/extensions/helpers.go +++ b/internal/components/extensions/helpers.go @@ -55,6 +55,9 @@ var ( return components.ParseSingleEndpointSilent(logger, name, defaultPort, &config.SingleEndpointConfig) }). MustBuild(), + components.NewSinglePortParserBuilder("jaeger_query", 16686). + WithTargetPort(16686). + MustBuild(), } ) diff --git a/internal/manifests/collector/collector.go b/internal/manifests/collector/collector.go index df294d4d47..0e4cc414d5 100644 --- a/internal/manifests/collector/collector.go +++ b/internal/manifests/collector/collector.go @@ -53,6 +53,7 @@ func Build(params manifests.Params) ([]client.Object, error) { manifests.Factory(Service), manifests.Factory(HeadlessService), manifests.Factory(MonitoringService), + manifests.Factory(ExtensionService), manifests.Factory(Ingress), }...) diff --git a/internal/manifests/collector/service.go b/internal/manifests/collector/service.go index a7b8813f94..7e27eb752c 100644 --- a/internal/manifests/collector/service.go +++ b/internal/manifests/collector/service.go @@ -42,10 +42,11 @@ const ( BaseServiceType ServiceType = iota HeadlessServiceType MonitoringServiceType + ExtensionServiceType ) func (s ServiceType) String() string { - return [...]string{"base", "headless", "monitoring"}[s] + return [...]string{"base", "headless", "monitoring", "extension"}[s] } func HeadlessService(params manifests.Params) (*corev1.Service, error) { @@ -108,6 +109,39 @@ func MonitoringService(params manifests.Params) (*corev1.Service, error) { }, nil } +func ExtensionService(params manifests.Params) (*corev1.Service, error) { + name := naming.ExtensionService(params.OtelCol.Name) + labels := manifestutils.Labels(params.OtelCol.ObjectMeta, name, params.OtelCol.Spec.Image, ComponentOpenTelemetryCollector, []string{}) + labels[serviceTypeLabel] = ExtensionServiceType.String() + + annotations, err := manifestutils.Annotations(params.OtelCol, params.Config.AnnotationsFilter()) + if err != nil { + return nil, err + } + + ports, err := params.OtelCol.Spec.Config.GetExtensionPorts(params.Log) + if err != nil { + return nil, err + } + + if len(ports) == 0 { + return nil, nil + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: params.OtelCol.Namespace, + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.ServiceSpec{ + Ports: ports, + Selector: manifestutils.SelectorLabels(params.OtelCol.ObjectMeta, ComponentOpenTelemetryCollector), + }, + }, nil +} + func Service(params manifests.Params) (*corev1.Service, error) { name := naming.Service(params.OtelCol.Name) labels := manifestutils.Labels(params.OtelCol.ObjectMeta, name, params.OtelCol.Spec.Image, ComponentOpenTelemetryCollector, []string{}) @@ -118,7 +152,7 @@ func Service(params manifests.Params) (*corev1.Service, error) { return nil, err } - ports, err := params.OtelCol.Spec.Config.GetAllPorts(params.Log) + ports, err := params.OtelCol.Spec.Config.GetReceiverAndExporterPorts(params.Log) if err != nil { return nil, err } diff --git a/internal/manifests/collector/service_test.go b/internal/manifests/collector/service_test.go index 11ac981585..7a9695e594 100644 --- a/internal/manifests/collector/service_test.go +++ b/internal/manifests/collector/service_test.go @@ -26,6 +26,7 @@ import ( "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/manifests" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" + "github.com/open-telemetry/opentelemetry-operator/internal/naming" ) func TestExtractPortNumbersAndNames(t *testing.T) { @@ -321,6 +322,206 @@ func TestMonitoringService(t *testing.T) { }) } +func TestExtensionService(t *testing.T) { + testCases := []struct { + name string + params manifests.Params + expectedPorts []v1.ServicePort + }{ + { + name: "when the extension has http endpoint", + params: manifests.Params{ + Config: config.Config{}, + Log: logger, + OtelCol: v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.OpenTelemetryCollectorSpec{ + Config: v1beta1.Config{ + Service: v1beta1.Service{ + Extensions: []string{"jaeger_query"}, + }, + Extensions: &v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "jaeger_query": map[string]interface{}{ + "http": map[string]interface{}{ + "endpoint": "0.0.0.0:16686", + }, + }, + }, + }, + }, + }, + }, + }, + expectedPorts: []v1.ServicePort{ + { + Name: "jaeger-query", + Port: 16686, + TargetPort: intstr.IntOrString{ + IntVal: 16686, + }, + }, + }, + }, + { + name: "when the extension has grpc endpoint", + params: manifests.Params{ + Config: config.Config{}, + Log: logger, + OtelCol: v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.OpenTelemetryCollectorSpec{ + Config: v1beta1.Config{ + Service: v1beta1.Service{ + Extensions: []string{"jaeger_query"}, + }, + Extensions: &v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "jaeger_query": map[string]interface{}{ + "http": map[string]interface{}{ + "endpoint": "0.0.0.0:16686", + }, + }, + }, + }, + }, + }, + }, + }, + expectedPorts: []v1.ServicePort{ + { + Name: "jaeger-query", + Port: 16686, + TargetPort: intstr.IntOrString{ + IntVal: 16686, + }, + }, + }, + }, + { + name: "when the extension has both http and grpc endpoint", + params: manifests.Params{ + Config: config.Config{}, + Log: logger, + OtelCol: v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.OpenTelemetryCollectorSpec{ + Config: v1beta1.Config{ + Service: v1beta1.Service{ + Extensions: []string{"jaeger_query"}, + }, + Extensions: &v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "jaeger_query": map[string]interface{}{ + "http": map[string]interface{}{ + "endpoint": "0.0.0.0:16686", + }, + "grpc": map[string]interface{}{ + "endpoint": "0.0.0.0:16686", + }, + }, + }, + }, + }, + }, + }, + }, + expectedPorts: []v1.ServicePort{ + { + Name: "jaeger-query", + Port: 16686, + TargetPort: intstr.IntOrString{ + IntVal: 16686, + }, + }, + }, + }, + { + name: "when the extension has no extensions defined", + params: manifests.Params{ + Config: config.Config{}, + Log: logger, + OtelCol: v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.OpenTelemetryCollectorSpec{ + Config: v1beta1.Config{ + Service: v1beta1.Service{ + Extensions: []string{"jaeger_query"}, + }, + Extensions: &v1beta1.AnyConfig{ + Object: map[string]interface{}{}, + }, + }, + }, + }, + }, + expectedPorts: []v1.ServicePort{}, + }, + { + name: "when the extension has no endpoint defined", + params: manifests.Params{ + Config: config.Config{}, + Log: logger, + OtelCol: v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.OpenTelemetryCollectorSpec{ + Config: v1beta1.Config{ + Service: v1beta1.Service{ + Extensions: []string{"jaeger_query"}, + }, + Extensions: &v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "jaeger_query": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + expectedPorts: []v1.ServicePort{ + { + Name: "jaeger-query", + Port: 16686, + TargetPort: intstr.IntOrString{ + IntVal: 16686, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + actual, err := ExtensionService(tc.params) + assert.NoError(t, err) + + if len(tc.expectedPorts) > 0 { + assert.NotNil(t, actual) + assert.Equal(t, actual.Name, naming.ExtensionService(tc.params.OtelCol.Name)) + // ports assertion + assert.Equal(t, len(tc.expectedPorts), len(actual.Spec.Ports)) + assert.Equal(t, tc.expectedPorts[0].Name, actual.Spec.Ports[0].Name) + assert.Equal(t, tc.expectedPorts[0].Port, actual.Spec.Ports[0].Port) + assert.Equal(t, tc.expectedPorts[0].TargetPort.IntVal, actual.Spec.Ports[0].TargetPort.IntVal) + } else { + // no ports, no service + assert.Nil(t, actual) + } + }) + } +} + func service(name string, ports []v1beta1.PortsSpec) v1.Service { return serviceWithInternalTrafficPolicy(name, ports, v1.ServiceInternalTrafficPolicyCluster) } diff --git a/internal/naming/main.go b/internal/naming/main.go index 8642f618c3..149a9f9d5a 100644 --- a/internal/naming/main.go +++ b/internal/naming/main.go @@ -116,6 +116,11 @@ func MonitoringService(otelcol string) string { return DNSName(Truncate("%s-monitoring", 63, Service(otelcol))) } +// ExtensionService builds the name for the extension service based on the instance. +func ExtensionService(otelcol string) string { + return DNSName(Truncate("%s-extension", 63, Service(otelcol))) +} + // Service builds the service name based on the instance. func Service(otelcol string) string { return DNSName(Truncate("%s-collector", 63, otelcol)) diff --git a/tests/e2e/extension/00-assert.yaml b/tests/e2e/extension/00-assert.yaml new file mode 100644 index 0000000000..c62406a1f3 --- /dev/null +++ b/tests/e2e/extension/00-assert.yaml @@ -0,0 +1,140 @@ +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 +metadata: + resourceVersion: "" +--- +apiVersion: v1 +kind: Service +metadata: + name: jaeger-inmemory-collector +spec: + ports: + - appProtocol: grpc + name: otlp-grpc + port: 4317 + protocol: TCP + targetPort: 4317 + - appProtocol: http + name: otlp-http + port: 4318 + protocol: TCP + targetPort: 4318 +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + service.beta.openshift.io/serving-cert-secret-name: jaeger-inmemory-collector-headless-tls + labels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/name: jaeger-inmemory-collector + app.kubernetes.io/part-of: opentelemetry + app.kubernetes.io/version: latest + operator.opentelemetry.io/collector-headless-service: Exists + operator.opentelemetry.io/collector-service-type: headless + name: jaeger-inmemory-collector-headless + ownerReferences: + - apiVersion: opentelemetry.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: OpenTelemetryCollector + name: jaeger-inmemory +spec: + clusterIP: None + clusterIPs: + - None + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - appProtocol: grpc + name: otlp-grpc + port: 4317 + protocol: TCP + targetPort: 4317 + - appProtocol: http + name: otlp-http + port: 4318 + protocol: TCP + targetPort: 4318 + selector: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/part-of: opentelemetry + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/name: jaeger-inmemory-collector-monitoring + app.kubernetes.io/part-of: opentelemetry + app.kubernetes.io/version: latest + operator.opentelemetry.io/collector-monitoring-service: Exists + operator.opentelemetry.io/collector-service-type: monitoring + name: jaeger-inmemory-collector-monitoring +spec: + ports: + - name: monitoring + port: 8888 + protocol: TCP + targetPort: 8888 + selector: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/part-of: opentelemetry + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: jaeger-inmemory-collector-extension + labels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/part-of: opentelemetry + app.kubernetes.io/version: latest + operator.opentelemetry.io/collector-service-type: extension +spec: + selector: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/part-of: opentelemetry + ports: + - name: jaeger-query + port: 16686 + targetPort: 16686 +status: + loadBalancer: {} diff --git a/tests/e2e/extension/00-install.yaml b/tests/e2e/extension/00-install.yaml new file mode 100644 index 0000000000..43e27fa9b2 --- /dev/null +++ b/tests/e2e/extension/00-install.yaml @@ -0,0 +1,30 @@ +apiVersion: opentelemetry.io/v1beta1 +kind: OpenTelemetryCollector +metadata: + name: jaeger-inmemory +spec: + image: jaegertracing/jaeger:latest + config: + service: + extensions: [jaeger_storage, jaeger_query] + pipelines: + traces: + receivers: [otlp] + exporters: [jaeger_storage_exporter] + extensions: + jaeger_query: + storage: + traces: memstore + jaeger_storage: + backends: + memstore: + memory: + max_traces: 100000 + receivers: + otlp: + protocols: + grpc: + http: + exporters: + jaeger_storage_exporter: + trace_storage: memstore diff --git a/tests/e2e/extension/chainsaw-test.yaml b/tests/e2e/extension/chainsaw-test.yaml new file mode 100644 index 0000000000..488a76359b --- /dev/null +++ b/tests/e2e/extension/chainsaw-test.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + creationTimestamp: null + name: extension-test +spec: + steps: + - name: step-00 + try: + - apply: + file: 00-install.yaml + - assert: + file: 00-assert.yaml