diff --git a/apis/v1alpha1/opentelemetrycollector_types.go b/apis/v1alpha1/opentelemetrycollector_types.go index 197162ac47..8688ca69be 100644 --- a/apis/v1alpha1/opentelemetrycollector_types.go +++ b/apis/v1alpha1/opentelemetrycollector_types.go @@ -83,7 +83,7 @@ type OpenTelemetryCollectorSpec struct { VolumeMounts []v1.VolumeMount `json:"volumeMounts,omitempty"` // Ports allows a set of ports to be exposed by the underlying v1.Service. By default, the operator // will attempt to infer the required ports by parsing the .Spec.Config property but this property can be - // used to open aditional ports that can't be inferred by the operator, like for custom receivers. + // used to open additional ports that can't be inferred by the operator, like for custom receivers. // +optional // +listType=atomic Ports []v1.ServicePort `json:"ports,omitempty"` diff --git a/apis/v1alpha1/opentelemetrycollector_webhook.go b/apis/v1alpha1/opentelemetrycollector_webhook.go index 78e9719628..2c09add4ce 100644 --- a/apis/v1alpha1/opentelemetrycollector_webhook.go +++ b/apis/v1alpha1/opentelemetrycollector_webhook.go @@ -18,6 +18,7 @@ import ( "fmt" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -120,6 +121,16 @@ func (r *OpenTelemetryCollector) validateCRDSpec() error { } } + // validator port config + for _, p := range r.Spec.Ports { + nameErrs := validation.IsValidPortName(p.Name) + numErrs := validation.IsValidPortNum(int(p.Port)) + if len(nameErrs) > 0 || len(numErrs) > 0 { + return fmt.Errorf("the OpenTelemetry Spec Ports configuration is incorrect, port name '%s' errors: %s, num '%d' errors: %s", + p.Name, nameErrs, p.Port, numErrs) + } + } + // validate autoscale with horizontal pod autoscaler if r.Spec.MaxReplicas != nil { if *r.Spec.MaxReplicas < int32(1) { diff --git a/apis/v1alpha1/opentelemetrycollector_webhook_test.go b/apis/v1alpha1/opentelemetrycollector_webhook_test.go index 39dc709551..7396b335ee 100644 --- a/apis/v1alpha1/opentelemetrycollector_webhook_test.go +++ b/apis/v1alpha1/opentelemetrycollector_webhook_test.go @@ -18,6 +18,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + autoscalingv2 "k8s.io/api/autoscaling/v2" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -76,3 +78,255 @@ func TestOTELColDefaultingWebhook(t *testing.T) { }) } } + +func TestOTELColValidatingWebhook(t *testing.T) { + zero := int32(0) + one := int32(1) + three := int32(3) + five := int32(5) + + tests := []struct { //nolint:govet + name string + otelcol OpenTelemetryCollector + expectedErr string + }{ + { + name: "valid empty spec", + otelcol: OpenTelemetryCollector{}, + }, + { + name: "valid full spec", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeStatefulSet, + MinReplicas: &one, + Replicas: &three, + MaxReplicas: &five, + UpgradeStrategy: "adhoc", + TargetAllocator: OpenTelemetryTargetAllocator{ + Enabled: true, + }, + Config: `receivers: + examplereceiver: + endpoint: "0.0.0.0:12345" + examplereceiver/settings: + endpoint: "0.0.0.0:12346" + prometheus: + config: + scrape_config: + job_name: otel-collector + scrape_interval: 10s + jaeger/custom: + protocols: + thrift_http: + endpoint: 0.0.0.0:15268 +`, + Ports: []v1.ServicePort{ + { + Name: "port1", + Port: 5555, + }, + { + Name: "port2", + Port: 5554, + Protocol: v1.ProtocolUDP, + }, + }, + Autoscaler: &AutoscalerSpec{ + Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &three, + }, + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &five, + }, + }, + TargetCPUUtilization: &five, + }, + }, + }, + }, + { + name: "invalid mode with volume claim templates", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeSidecar, + VolumeClaimTemplates: []v1.PersistentVolumeClaim{{}, {}}, + }, + }, + expectedErr: "does not support the attribute 'volumeClaimTemplates'", + }, + { + name: "invalid mode with tolerations", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeSidecar, + Tolerations: []v1.Toleration{{}, {}}, + }, + }, + expectedErr: "does not support the attribute 'tolerations'", + }, + { + name: "invalid mode with target allocator", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeDeployment, + TargetAllocator: OpenTelemetryTargetAllocator{ + Enabled: true, + }, + }, + }, + expectedErr: "does not support the target allocation deployment", + }, + { + name: "invalid target allocator config", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Mode: ModeStatefulSet, + TargetAllocator: OpenTelemetryTargetAllocator{ + Enabled: true, + }, + }, + }, + expectedErr: "the OpenTelemetry Spec Prometheus configuration is incorrect", + }, + { + name: "invalid port name", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Ports: []v1.ServicePort{ + { + // this port name contains a non alphanumeric character, which is invalid. + Name: "-test🦄port", + Port: 12345, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + expectedErr: "the OpenTelemetry Spec Ports configuration is incorrect", + }, + { + name: "invalid port name, too long", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Ports: []v1.ServicePort{ + { + Name: "aaaabbbbccccdddd", // len: 16, too long + Port: 5555, + }, + }, + }, + }, + expectedErr: "the OpenTelemetry Spec Ports configuration is incorrect", + }, + { + name: "invalid port num", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + Ports: []v1.ServicePort{ + { + Name: "aaaabbbbccccddd", // len: 15 + // no port set means it's 0, which is invalid + }, + }, + }, + }, + expectedErr: "the OpenTelemetry Spec Ports configuration is incorrect", + }, + { + name: "invalid max replicas", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &zero, + }, + }, + expectedErr: "maxReplicas should be defined and more than one", + }, + { + name: "invalid replicas, greater than max", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &three, + Replicas: &five, + }, + }, + expectedErr: "replicas must not be greater than maxReplicas", + }, + { + name: "invalid min replicas, greater than max", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &three, + MinReplicas: &five, + }, + }, + expectedErr: "minReplicas must not be greater than maxReplicas", + }, + { + name: "invalid min replicas, lesser than 1", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &three, + MinReplicas: &zero, + }, + }, + expectedErr: "minReplicas should be one or more", + }, + { + name: "invalid autoscaler scale down", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &three, + Autoscaler: &AutoscalerSpec{ + Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &zero, + }, + }, + }, + }, + }, + expectedErr: "scaleDown should be one or more", + }, + { + name: "invalid autoscaler scale up", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &three, + Autoscaler: &AutoscalerSpec{ + Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &zero, + }, + }, + }, + }, + }, + expectedErr: "scaleUp should be one or more", + }, + { + name: "invalid autoscaler target cpu utilization", + otelcol: OpenTelemetryCollector{ + Spec: OpenTelemetryCollectorSpec{ + MaxReplicas: &three, + Autoscaler: &AutoscalerSpec{ + TargetCPUUtilization: &zero, + }, + }, + }, + expectedErr: "targetCPUUtilization should be greater than 0 and less than 100", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.otelcol.validateCRDSpec() + if test.expectedErr == "" { + assert.NoError(t, err) + return + } + assert.ErrorContains(t, err, test.expectedErr) + }) + } +} diff --git a/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml b/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml index c1e4aa95ae..d9efcacdde 100644 --- a/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml +++ b/bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml @@ -556,7 +556,7 @@ spec: description: Ports allows a set of ports to be exposed by the underlying v1.Service. By default, the operator will attempt to infer the required ports by parsing the .Spec.Config property but this property can - be used to open aditional ports that can't be inferred by the operator, + be used to open additional ports that can't be inferred by the operator, like for custom receivers. items: description: ServicePort contains information on service's port. diff --git a/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml b/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml index 291ce4177e..16a0124882 100644 --- a/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml +++ b/config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml @@ -554,7 +554,7 @@ spec: description: Ports allows a set of ports to be exposed by the underlying v1.Service. By default, the operator will attempt to infer the required ports by parsing the .Spec.Config property but this property can - be used to open aditional ports that can't be inferred by the operator, + be used to open additional ports that can't be inferred by the operator, like for custom receivers. items: description: ServicePort contains information on service's port. diff --git a/docs/api.md b/docs/api.md index cfcf113717..aa9e100583 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1792,7 +1792,7 @@ OpenTelemetryCollectorSpec defines the desired state of OpenTelemetryCollector. ports []object - Ports allows a set of ports to be exposed by the underlying v1.Service. By default, the operator will attempt to infer the required ports by parsing the .Spec.Config property but this property can be used to open aditional ports that can't be inferred by the operator, like for custom receivers.
+ Ports allows a set of ports to be exposed by the underlying v1.Service. By default, the operator will attempt to infer the required ports by parsing the .Spec.Config property but this property can be used to open additional ports that can't be inferred by the operator, like for custom receivers.
false diff --git a/pkg/collector/adapters/config_to_ports_test.go b/pkg/collector/adapters/config_to_ports_test.go index 84c10e3d83..fbc6989552 100644 --- a/pkg/collector/adapters/config_to_ports_test.go +++ b/pkg/collector/adapters/config_to_ports_test.go @@ -31,8 +31,7 @@ import ( var logger = logf.Log.WithName("unit-tests") -func TestExtractPortsFromConfig(t *testing.T) { - configStr := `receivers: +var portConfigStr = `receivers: examplereceiver: endpoint: "0.0.0.0:12345" examplereceiver/settings: @@ -77,8 +76,9 @@ service: exporters: [logging] ` +func TestExtractPortsFromConfig(t *testing.T) { // prepare - config, err := adapters.ConfigFromString(configStr) + config, err := adapters.ConfigFromString(portConfigStr) require.NoError(t, err) require.NotEmpty(t, config) @@ -94,18 +94,20 @@ service: targetPort4317 := intstr.IntOrString{Type: 0, IntVal: 4317, StrVal: ""} targetPort4318 := intstr.IntOrString{Type: 0, IntVal: 4318, StrVal: ""} - assert.Len(t, ports, 11) - assert.Equal(t, corev1.ServicePort{Name: "examplereceiver", Port: int32(12345)}, ports[0]) - assert.Equal(t, corev1.ServicePort{Name: "examplereceiver-settings", Port: int32(12346)}, ports[1]) - assert.Equal(t, corev1.ServicePort{Name: "jaeger-custom-thrift-http", AppProtocol: &httpAppProtocol, Protocol: "TCP", Port: int32(15268), TargetPort: targetPortZero}, ports[2]) - assert.Equal(t, corev1.ServicePort{Name: "jaeger-grpc", AppProtocol: &grpcAppProtocol, Protocol: "TCP", Port: int32(14250)}, ports[3]) - assert.Equal(t, corev1.ServicePort{Name: "jaeger-thrift-binary", Protocol: "UDP", Port: int32(6833)}, ports[4]) - assert.Equal(t, corev1.ServicePort{Name: "jaeger-thrift-compact", Protocol: "UDP", Port: int32(6831)}, ports[5]) - assert.Equal(t, corev1.ServicePort{Name: "otlp-2-grpc", AppProtocol: &grpcAppProtocol, Protocol: "TCP", Port: int32(55555)}, ports[6]) - assert.Equal(t, corev1.ServicePort{Name: "otlp-grpc", AppProtocol: &grpcAppProtocol, Port: int32(4317), TargetPort: targetPort4317}, ports[7]) - assert.Equal(t, corev1.ServicePort{Name: "otlp-http", AppProtocol: &httpAppProtocol, Port: int32(4318), TargetPort: targetPort4318}, ports[8]) - assert.Equal(t, corev1.ServicePort{Name: "otlp-http-legacy", AppProtocol: &httpAppProtocol, Port: int32(55681), TargetPort: targetPort4318}, ports[9]) - assert.Equal(t, corev1.ServicePort{Name: "zipkin", AppProtocol: &httpAppProtocol, Protocol: "TCP", Port: int32(9411)}, ports[10]) + expectedPorts := []corev1.ServicePort{ + {Name: "examplereceiver", Port: 12345}, + {Name: "examplereceiver-settings", Port: 12346}, + {Name: "jaeger-custom-thrift-http", AppProtocol: &httpAppProtocol, Protocol: "TCP", Port: 15268, TargetPort: targetPortZero}, + {Name: "jaeger-grpc", AppProtocol: &grpcAppProtocol, Protocol: "TCP", Port: 14250}, + {Name: "jaeger-thrift-binary", Protocol: "UDP", Port: 6833}, + {Name: "jaeger-thrift-compact", Protocol: "UDP", Port: 6831}, + {Name: "otlp-2-grpc", AppProtocol: &grpcAppProtocol, Protocol: "TCP", Port: 55555}, + {Name: "otlp-grpc", AppProtocol: &grpcAppProtocol, Port: 4317, TargetPort: targetPort4317}, + {Name: "otlp-http", AppProtocol: &httpAppProtocol, Port: 4318, TargetPort: targetPort4318}, + {Name: "otlp-http-legacy", AppProtocol: &httpAppProtocol, Port: 55681, TargetPort: targetPort4318}, + {Name: "zipkin", AppProtocol: &httpAppProtocol, Protocol: "TCP", Port: 9411}, + } + assert.ElementsMatch(t, expectedPorts, ports) } func TestNoPortsParsed(t *testing.T) { diff --git a/pkg/collector/container.go b/pkg/collector/container.go index 5c0548feb4..32434ee427 100644 --- a/pkg/collector/container.go +++ b/pkg/collector/container.go @@ -15,10 +15,16 @@ package collector import ( + "errors" "fmt" + "net" + "sort" + "strconv" "github.com/go-logr/logr" + "github.com/mitchellh/mapstructure" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" "github.com/open-telemetry/opentelemetry-operator/internal/config" @@ -26,6 +32,12 @@ import ( "github.com/open-telemetry/opentelemetry-operator/pkg/naming" ) +// maxPortLen allows us to truncate a port name according to what is considered valid port syntax: +// https://pkg.go.dev/k8s.io/apimachinery/pkg/util/validation#IsValidPortName +const maxPortLen = 15 + +var errInvalidPort = errors.New("invalid port name/num") + // Container builds a container for the given collector. func Container(cfg config.Config, logger logr.Logger, otelcol v1alpha1.OpenTelemetryCollector) corev1.Container { image := otelcol.Spec.Image @@ -33,6 +45,16 @@ func Container(cfg config.Config, logger logr.Logger, otelcol v1alpha1.OpenTelem image = cfg.CollectorImage() } + // build container ports from service ports + ports := getConfigContainerPorts(logger, otelcol.Spec.Config) + for _, p := range otelcol.Spec.Ports { + ports[p.Name] = corev1.ContainerPort{ + Name: p.Name, + ContainerPort: p.Port, + Protocol: p.Protocol, + } + } + argsMap := otelcol.Spec.Args if argsMap == nil { argsMap = map[string]string{} @@ -89,6 +111,7 @@ func Container(cfg config.Config, logger logr.Logger, otelcol v1alpha1.OpenTelem Name: naming.Container(), Image: image, ImagePullPolicy: otelcol.Spec.ImagePullPolicy, + Ports: portMapToList(ports), VolumeMounts: volumeMounts, Args: args, Env: envVars, @@ -98,3 +121,93 @@ func Container(cfg config.Config, logger logr.Logger, otelcol v1alpha1.OpenTelem LivenessProbe: livenessProbe, } } + +func getConfigContainerPorts(logger logr.Logger, cfg string) map[string]corev1.ContainerPort { + ports := map[string]corev1.ContainerPort{} + c, err := adapters.ConfigFromString(cfg) + if err != nil { + logger.Error(err, "couldn't extract the configuration") + return ports + } + ps, err := adapters.ConfigToReceiverPorts(logger, c) + if err != nil { + logger.Error(err, "couldn't build container ports from configuration") + } else { + for _, p := range ps { + truncName := naming.Truncate(p.Name, maxPortLen) + if p.Name != truncName { + logger.Info("truncating container port name", + "port.name.prev", p.Name, "port.name.new", truncName) + } + nameErrs := validation.IsValidPortName(truncName) + numErrs := validation.IsValidPortNum(int(p.Port)) + if len(nameErrs) > 0 || len(numErrs) > 0 { + logger.Error(errInvalidPort, "dropping container port", "port.name", truncName, "port.num", p.Port, + "port.name.errs", nameErrs, "num.errs", numErrs) + continue + } + ports[truncName] = corev1.ContainerPort{ + Name: truncName, + ContainerPort: p.Port, + Protocol: p.Protocol, + } + } + } + + metricsPort, err := getMetricsPort(c) + if err != nil { + logger.Error(err, "couldn't determine metrics port from configuration, using 8888 default value") + metricsPort = 8888 + } + ports["metrics"] = corev1.ContainerPort{ + Name: "metrics", + ContainerPort: metricsPort, + Protocol: corev1.ProtocolTCP, + } + return ports +} + +// getMetricsPort gets the port number for the metrics endpoint from the collector config if it has been set. +func getMetricsPort(c map[interface{}]interface{}) (int32, error) { + // we don't need to unmarshal the whole config, just follow the keys down to + // the metrics address. + type metricsCfg struct { + Address string + } + type telemetryCfg struct { + Metrics metricsCfg + } + type serviceCfg struct { + Telemetry telemetryCfg + } + type cfg struct { + Service serviceCfg + } + var cOut cfg + err := mapstructure.Decode(c, &cOut) + if err != nil { + return 0, err + } + + _, port, err := net.SplitHostPort(cOut.Service.Telemetry.Metrics.Address) + if err != nil { + return 0, err + } + i64, err := strconv.ParseInt(port, 10, 32) + if err != nil { + return 0, err + } + + return int32(i64), nil +} + +func portMapToList(portMap map[string]corev1.ContainerPort) []corev1.ContainerPort { + ports := make([]corev1.ContainerPort, 0, len(portMap)) + for _, p := range portMap { + ports = append(ports, p) + } + sort.Slice(ports, func(i, j int) bool { + return ports[i].Name < ports[j].Name + }) + return ports +} diff --git a/pkg/collector/container_test.go b/pkg/collector/container_test.go index 60820c9c2b..a0d7155e59 100644 --- a/pkg/collector/container_test.go +++ b/pkg/collector/container_test.go @@ -29,6 +29,12 @@ import ( var logger = logf.Log.WithName("unit-tests") +var metricContainerPort = corev1.ContainerPort{ + Name: "metrics", + ContainerPort: 8888, + Protocol: corev1.ProtocolTCP, +} + func TestContainerNewDefault(t *testing.T) { // prepare otelcol := v1alpha1.OpenTelemetryCollector{} @@ -39,6 +45,7 @@ func TestContainerNewDefault(t *testing.T) { // verify assert.Equal(t, "default-image", c.Image) + assert.Equal(t, []corev1.ContainerPort{metricContainerPort}, c.Ports) } func TestContainerWithImageOverridden(t *testing.T) { @@ -57,6 +64,141 @@ func TestContainerWithImageOverridden(t *testing.T) { assert.Equal(t, "overridden-image", c.Image) } +func TestContainerPorts(t *testing.T) { + var goodConfig = `receivers: + examplereceiver: + endpoint: "0.0.0.0:12345" +service: + pipelines: + metrics: + receivers: [examplereceiver] + exporters: [logging] +` + + tests := []struct { + description string + specConfig string + specPorts []corev1.ServicePort + expectedPorts []corev1.ContainerPort + }{ + { + description: "bad spec config", + specConfig: "🦄", + specPorts: nil, + expectedPorts: []corev1.ContainerPort{}, + }, + { + description: "couldn't build ports from spec config", + specConfig: "", + specPorts: nil, + expectedPorts: []corev1.ContainerPort{metricContainerPort}, + }, + { + description: "ports in spec Config", + specConfig: goodConfig, + specPorts: nil, + expectedPorts: []corev1.ContainerPort{ + { + Name: "examplereceiver", + ContainerPort: 12345, + }, + metricContainerPort, + }, + }, + { + description: "ports in spec ContainerPorts", + specPorts: []corev1.ServicePort{ + { + Name: "testport1", + Port: 12345, + }, + }, + expectedPorts: []corev1.ContainerPort{ + metricContainerPort, + { + Name: "testport1", + ContainerPort: 12345, + }, + }, + }, + { + description: "ports in spec Config and ContainerPorts", + specConfig: goodConfig, + specPorts: []corev1.ServicePort{ + { + Name: "testport1", + Port: 12345, + }, + { + Name: "testport2", + Port: 54321, + Protocol: corev1.ProtocolUDP, + }, + }, + expectedPorts: []corev1.ContainerPort{ + { + Name: "examplereceiver", + ContainerPort: 12345, + }, + metricContainerPort, + { + Name: "testport1", + ContainerPort: 12345, + }, + { + Name: "testport2", + ContainerPort: 54321, + Protocol: corev1.ProtocolUDP, + }, + }, + }, + { + description: "duplicate port name", + specConfig: goodConfig, + specPorts: []corev1.ServicePort{ + { + Name: "testport1", + Port: 12345, + }, + { + Name: "testport1", + Port: 11111, + }, + }, + expectedPorts: []corev1.ContainerPort{ + { + Name: "examplereceiver", + ContainerPort: 12345, + }, + metricContainerPort, + { + Name: "testport1", + ContainerPort: 11111, + }, + }, + }, + } + + for _, testCase := range tests { + t.Run(testCase.description, func(t *testing.T) { + // prepare + otelcol := v1alpha1.OpenTelemetryCollector{ + Spec: v1alpha1.OpenTelemetryCollectorSpec{ + Config: testCase.specConfig, + Ports: testCase.specPorts, + }, + } + cfg := config.New(config.WithCollectorImage("default-image")) + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.ElementsMatch(t, testCase.expectedPorts, c.Ports) + }) + } +} + func TestContainerConfigFlagIsIgnored(t *testing.T) { // prepare otelcol := v1alpha1.OpenTelemetryCollector{