Skip to content
Closed
10 changes: 10 additions & 0 deletions .chloggen/tls.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
change_type: "enhancement"
component: "dockerstatsreceiver"
note: "Fixed TLS configuration resolution in metricsReceiver to properly handle enabled/disabled TLS states."
issues: [33557]
subtext: |
- The start function now correctly determines whether TLS is enabled or not.
- If TLS is disabled, it logs a message and proceeds without it.
- Ensures secure TLS configuration handling to prevent misconfigurations.
- The `tlsConfig` is now correctly injected into the Docker client wherever applicable.
change_logs: ["user"]
66 changes: 58 additions & 8 deletions internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ package docker // import "github.com/open-telemetry/opentelemetry-collector-cont

import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -45,21 +49,61 @@ type Client struct {
logger *zap.Logger
}

func NewDockerClient(config *Config, logger *zap.Logger, opts ...docker.Opt) (*Client, error) {
func NewDockerClient(config *Config, logger *zap.Logger, tlsConfig *tls.Config, opts ...docker.Opt) (*Client, error) {
version := minimumRequiredDockerAPIVersion
if config.DockerAPIVersion != "" {
var err error
if version, err = NewAPIVersion(config.DockerAPIVersion); err != nil {
return nil, err
}
}
client, err := docker.NewClientWithOpts(
append([]docker.Opt{
docker.WithHost(config.Endpoint),
docker.WithVersion(version),
docker.WithHTTPHeaders(map[string]string{"User-Agent": userAgent}),
}, opts...)...,
)

// Basic options
clientOpts := []docker.Opt{
docker.WithHost(config.Endpoint),
docker.WithVersion(version),
docker.WithHTTPHeaders(map[string]string{"User-Agent": userAgent}),
}

// Use the provided TLS configuration if available
if tlsConfig != nil {
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
clientOpts = append(clientOpts, docker.WithHTTPClient(httpClient))
logger.Debug("Using provided TLS configuration for Docker client")
} else if strings.HasPrefix(config.Endpoint, "https://") {
// Look for certificates in default locations or from environment variables
certPath := os.Getenv("DOCKER_CERT_PATH")
if certPath == "" {
homeDir, err := os.UserHomeDir()
if err == nil {
certPath = filepath.Join(homeDir, ".docker")
}
}

if certPath != "" {
certFile := filepath.Join(certPath, "cert.pem")
keyFile := filepath.Join(certPath, "key.pem")
caFile := filepath.Join(certPath, "ca.pem")

if fileExists(certFile) && fileExists(keyFile) && fileExists(caFile) {
clientOpts = append(clientOpts, docker.WithTLSClientConfig(caFile, certFile, keyFile))
logger.Debug("Loaded TLS certificates from default location",
zap.String("cert", certFile),
zap.String("key", keyFile),
zap.String("ca", caFile))
} else {
logger.Warn("TLS certificates not found at expected location",
zap.String("cert", certFile),
zap.String("key", keyFile),
zap.String("ca", caFile))
}
}
}

// Add any additional options
clientOpts = append(clientOpts, opts...)

client, err := docker.NewClientWithOpts(clientOpts...)
if err != nil {
return nil, fmt.Errorf("could not create docker client: %w", err)
}
Expand All @@ -81,6 +125,12 @@ func NewDockerClient(config *Config, logger *zap.Logger, opts ...docker.Opt) (*C
return dc, nil
}

// Helper function to check if a file exists
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}

// Containers provides a slice of Container to use for individual FetchContainerStats calls.
func (dc *Client) Containers() []Container {
dc.containersLock.Lock()
Expand Down
16 changes: 8 additions & 8 deletions internal/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestInvalidEndpoint(t *testing.T) {
config := &Config{
Endpoint: "$notavalidendpoint*",
}
cli, err := NewDockerClient(config, zap.NewNop())
cli, err := NewDockerClient(config, zap.NewNop(), nil)
assert.Nil(t, cli)
require.Error(t, err)
assert.Equal(t, "could not create docker client: unable to parse docker host `$notavalidendpoint*`", err.Error())
Expand All @@ -37,7 +37,7 @@ func TestInvalidEndpoint(t *testing.T) {
func TestInvalidExclude(t *testing.T) {
config := NewDefaultConfig()
config.ExcludedImages = []string{"["}
cli, err := NewDockerClient(config, zap.NewNop())
cli, err := NewDockerClient(config, zap.NewNop(), nil)
assert.Nil(t, cli)
require.Error(t, err)
assert.Equal(t, "could not determine docker client excluded images: invalid glob item: unexpected end of input", err.Error())
Expand All @@ -54,7 +54,7 @@ func TestWatchingTimeouts(t *testing.T) {
Timeout: 50 * time.Millisecond,
}

cli, err := NewDockerClient(config, zap.NewNop())
cli, err := NewDockerClient(config, zap.NewNop(), nil)
assert.NotNil(t, cli)
assert.NoError(t, err)

Expand All @@ -65,7 +65,7 @@ func TestWatchingTimeouts(t *testing.T) {
err = cli.LoadContainerList(context.Background())
assert.ErrorContains(t, err, expectedError)
observed, logs := observer.New(zapcore.WarnLevel)
cli, err = NewDockerClient(config, zap.New(observed))
cli, err = NewDockerClient(config, zap.New(observed), nil)
assert.NotNil(t, cli)
assert.NoError(t, err)

Expand Down Expand Up @@ -95,7 +95,7 @@ func TestFetchingTimeouts(t *testing.T) {
Timeout: 50 * time.Millisecond,
}

cli, err := NewDockerClient(config, zap.NewNop())
cli, err := NewDockerClient(config, zap.NewNop(), nil)
assert.NotNil(t, cli)
assert.NoError(t, err)

Expand All @@ -104,7 +104,7 @@ func TestFetchingTimeouts(t *testing.T) {
shouldHaveTaken := time.Now().Add(50 * time.Millisecond).UnixNano()

observed, logs := observer.New(zapcore.WarnLevel)
cli, err = NewDockerClient(config, zap.New(observed))
cli, err = NewDockerClient(config, zap.New(observed), nil)
assert.NotNil(t, cli)
assert.NoError(t, err)

Expand Down Expand Up @@ -145,7 +145,7 @@ func TestToStatsJSONErrorHandling(t *testing.T) {
Timeout: 50 * time.Millisecond,
}

cli, err := NewDockerClient(config, zap.NewNop())
cli, err := NewDockerClient(config, zap.NewNop(), nil)
assert.NotNil(t, cli)
assert.NoError(t, err)

Expand Down Expand Up @@ -192,7 +192,7 @@ func TestEventLoopHandlesError(t *testing.T) {
Timeout: 50 * time.Millisecond,
}

cli, err := NewDockerClient(config, zap.New(observed))
cli, err := NewDockerClient(config, zap.New(observed), nil)
assert.NotNil(t, cli)
assert.NoError(t, err)

Expand Down
3 changes: 3 additions & 0 deletions receiver/dockerstatsreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package dockerstatsreceiver // import "github.com/open-telemetry/opentelemetry-c

import (
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configtls"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/scraper/scraperhelper"

Expand Down Expand Up @@ -36,6 +37,8 @@ type Config struct {

// MetricsBuilderConfig config. Enable or disable stats by name.
metadata.MetricsBuilderConfig `mapstructure:",squash"`
// TLS configuration
TLSConfig *configtls.ClientConfig `mapstructure:"tls,omitempty"`
}

func (config Config) Validate() error {
Expand Down
18 changes: 12 additions & 6 deletions receiver/dockerstatsreceiver/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ require (
go.uber.org/zap v1.27.0
)

require (
github.com/fsnotify/fsnotify v1.8.0 // indirect
go.opentelemetry.io/collector/config/configopaque v1.26.0 // indirect
go.opentelemetry.io/collector/featuregate v1.27.1-0.20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.0.0-20250306210537-36eaf6a917b5 // indirect
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
Expand Down Expand Up @@ -84,12 +91,11 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.121.1-0.20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.121.1-0.20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/featuregate v1.27.1-0.20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/config/configtls v1.26.0
go.opentelemetry.io/collector/consumer/consumererror v0.121.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.121.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.121.1-0.20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.0.0-20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.121.1-0.20250307194215-7d3e03e500b0 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.121.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
Expand All @@ -102,7 +108,7 @@ require (
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
26 changes: 16 additions & 10 deletions receiver/dockerstatsreceiver/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion receiver/dockerstatsreceiver/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package dockerstatsreceiver // import "github.com/open-telemetry/opentelemetry-c

import (
"context"
"crypto/tls"
"fmt"
"strconv"
"strings"
Expand All @@ -20,6 +21,7 @@ import (
"go.opentelemetry.io/collector/receiver"
"go.opentelemetry.io/collector/scraper/scrapererror"
"go.uber.org/multierr"
"go.uber.org/zap"

"github.com/open-telemetry/opentelemetry-collector-contrib/internal/docker"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/dockerstatsreceiver/internal/metadata"
Expand Down Expand Up @@ -52,9 +54,29 @@ func newMetricsReceiver(set receiver.Settings, config *Config) *metricsReceiver
}
}

func (c *Config) resolveTLSConfig(ctx context.Context) (*tls.Config, error) {
if c.TLSConfig == nil {
return nil, nil
}

return c.TLSConfig.LoadTLSConfig(ctx)
}

func (r *metricsReceiver) start(ctx context.Context, _ component.Host) error {
tlsConfig, tlsErr := r.config.resolveTLSConfig(ctx)
if tlsErr != nil {
r.settings.Logger.Error("Failed to resolve TLS config", zap.Error(tlsErr))
return fmt.Errorf("failed to resolve TLS config: %w", tlsErr)
}

if tlsConfig == nil {
r.settings.Logger.Debug("TLS is not enabled, running without TLS.")
} else {
r.settings.Logger.Debug("TLS is enabled.")
}

var err error
r.client, err = docker.NewDockerClient(&r.config.Config, r.settings.Logger)
r.client, err = docker.NewDockerClient(&r.config.Config, r.settings.Logger, tlsConfig)
if err != nil {
return err
}
Expand Down
Loading