diff --git a/cmd/agent/app/reporter/grpc/builder.go b/cmd/agent/app/reporter/grpc/builder.go index a8953a8e644..939b82156ae 100644 --- a/cmd/agent/app/reporter/grpc/builder.go +++ b/cmd/agent/app/reporter/grpc/builder.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry" + "go.opentelemetry.io/collector/config/configtls" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/connectivity" @@ -19,7 +20,6 @@ import ( "google.golang.org/grpc/resolver/manual" "github.com/jaegertracing/jaeger/cmd/agent/app/reporter" - "github.com/jaegertracing/jaeger/pkg/config/tlscfg" "github.com/jaegertracing/jaeger/pkg/discovery" "github.com/jaegertracing/jaeger/pkg/discovery/grpcresolver" "github.com/jaegertracing/jaeger/pkg/metrics" @@ -30,13 +30,11 @@ import ( type ConnBuilder struct { // CollectorHostPorts is list of host:port Jaeger Collectors. CollectorHostPorts []string `yaml:"collectorHostPorts"` - - MaxRetry uint - TLS tlscfg.Options - - DiscoveryMinPeers int - Notifier discovery.Notifier - Discoverer discovery.Discoverer + TLS *configtls.ClientConfig + MaxRetry uint + DiscoveryMinPeers int + Notifier discovery.Notifier + Discoverer discovery.Discoverer AdditionalDialOptions []grpc.DialOption } @@ -50,13 +48,12 @@ func NewConnBuilder() *ConnBuilder { func (b *ConnBuilder) CreateConnection(ctx context.Context, logger *zap.Logger, mFactory metrics.Factory) (*grpc.ClientConn, error) { var dialOptions []grpc.DialOption var dialTarget string - if b.TLS.Enabled { // user requested a secure connection + if b.TLS != nil { // user requested a secure connection logger.Info("Agent requested secure grpc connection to collector(s)") - tlsConf, err := b.TLS.Config(logger) + tlsConf, err := b.TLS.LoadTLSConfig(ctx) if err != nil { return nil, fmt.Errorf("failed to load TLS config: %w", err) } - creds := credentials.NewTLS(tlsConf) dialOptions = append(dialOptions, grpc.WithTransportCredentials(creds)) } else { // insecure connection diff --git a/cmd/agent/app/reporter/grpc/builder_test.go b/cmd/agent/app/reporter/grpc/builder_test.go index 1643fb2960a..a8a9cf952a5 100644 --- a/cmd/agent/app/reporter/grpc/builder_test.go +++ b/cmd/agent/app/reporter/grpc/builder_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/config/configtls" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -151,9 +152,10 @@ func TestProxyBuilder(t *testing.T) { name: "should fail with secure grpc connection and a CA file which does not exist", grpcBuilder: &ConnBuilder{ CollectorHostPorts: []string{"localhost:0000"}, - TLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/not/valid", + TLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/not/valid", + }, }, }, expectError: true, @@ -162,11 +164,12 @@ func TestProxyBuilder(t *testing.T) { name: "should pass with secure grpc connection and valid TLS Client settings", grpcBuilder: &ConnBuilder{ CollectorHostPorts: []string{"localhost:0000"}, - TLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/wrong-CA-cert.pem", - CertPath: testCertKeyLocation + "/example-client-cert.pem", - KeyPath: testCertKeyLocation + "/example-client-key.pem", + TLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/wrong-CA-cert.pem", + CertFile: testCertKeyLocation + "/example-client-cert.pem", + KeyFile: testCertKeyLocation + "/example-client-key.pem", + }, }, }, expectError: false, @@ -197,7 +200,7 @@ func TestProxyBuilder(t *testing.T) { func TestProxyClientTLS(t *testing.T) { tests := []struct { name string - clientTLS tlscfg.Options + clientTLS *configtls.ClientConfig serverTLS tlscfg.Options expectError bool }{ @@ -207,7 +210,7 @@ func TestProxyClientTLS(t *testing.T) { }, { name: "should fail with TLS client to non-TLS server", - clientTLS: tlscfg.Options{Enabled: true}, + clientTLS: &configtls.ClientConfig{}, expectError: true, }, { @@ -217,8 +220,7 @@ func TestProxyClientTLS(t *testing.T) { CertPath: testCertKeyLocation + "/example-server-cert.pem", KeyPath: testCertKeyLocation + "/example-server-key.pem", }, - clientTLS: tlscfg.Options{ - Enabled: true, + clientTLS: &configtls.ClientConfig{ ServerName: "example.com", }, expectError: true, @@ -230,9 +232,10 @@ func TestProxyClientTLS(t *testing.T) { CertPath: testCertKeyLocation + "/example-server-cert.pem", KeyPath: testCertKeyLocation + "/example-server-key.pem", }, - clientTLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/example-CA-cert.pem", + clientTLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/example-CA-cert.pem", + }, }, expectError: true, }, @@ -243,9 +246,10 @@ func TestProxyClientTLS(t *testing.T) { CertPath: testCertKeyLocation + "/example-server-cert.pem", KeyPath: testCertKeyLocation + "/example-server-key.pem", }, - clientTLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/example-CA-cert.pem", + clientTLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/example-CA-cert.pem", + }, ServerName: "example.com", }, expectError: false, @@ -258,9 +262,10 @@ func TestProxyClientTLS(t *testing.T) { KeyPath: testCertKeyLocation + "/example-server-key.pem", ClientCAPath: testCertKeyLocation + "/example-CA-cert.pem", }, - clientTLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/example-CA-cert.pem", + clientTLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/example-CA-cert.pem", + }, ServerName: "example.com", }, expectError: true, @@ -273,12 +278,13 @@ func TestProxyClientTLS(t *testing.T) { KeyPath: testCertKeyLocation + "/example-server-key.pem", ClientCAPath: testCertKeyLocation + "/wrong-CA-cert.pem", // NB: wrong CA }, - clientTLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/example-CA-cert.pem", + clientTLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/example-CA-cert.pem", + CertFile: testCertKeyLocation + "/example-client-cert.pem", + KeyFile: testCertKeyLocation + "/example-client-key.pem", + }, ServerName: "example.com", - CertPath: testCertKeyLocation + "/example-client-cert.pem", - KeyPath: testCertKeyLocation + "/example-client-key.pem", }, expectError: true, }, @@ -290,12 +296,13 @@ func TestProxyClientTLS(t *testing.T) { KeyPath: testCertKeyLocation + "/example-server-key.pem", ClientCAPath: testCertKeyLocation + "/example-CA-cert.pem", }, - clientTLS: tlscfg.Options{ - Enabled: true, - CAPath: testCertKeyLocation + "/example-CA-cert.pem", + clientTLS: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: testCertKeyLocation + "/example-CA-cert.pem", + CertFile: testCertKeyLocation + "/example-client-cert.pem", + KeyFile: testCertKeyLocation + "/example-client-key.pem", + }, ServerName: "example.com", - CertPath: testCertKeyLocation + "/example-client-cert.pem", - KeyPath: testCertKeyLocation + "/example-client-key.pem", }, expectError: false, }, @@ -308,12 +315,10 @@ func TestProxyClientTLS(t *testing.T) { defer cancel() var opts []grpc.ServerOption if test.serverTLS.Enabled { - tlsCfg, err := test.serverTLS.Config(zap.NewNop()) + tlsCfg, err := test.serverTLS.ToOtelServerConfig().LoadTLSConfig(ctx) require.NoError(t, err) opts = []grpc.ServerOption{grpc.Creds(credentials.NewTLS(tlsCfg))} } - - defer test.serverTLS.Close() spanHandler := &mockSpanHandler{} s, addr := initializeGRPCTestServer(t, func(s *grpc.Server) { api_v2.RegisterCollectorServiceServer(s, spanHandler) diff --git a/cmd/agent/app/reporter/grpc/collector_proxy.go b/cmd/agent/app/reporter/grpc/collector_proxy.go index 554926459b0..bb7b3ff64e8 100644 --- a/cmd/agent/app/reporter/grpc/collector_proxy.go +++ b/cmd/agent/app/reporter/grpc/collector_proxy.go @@ -6,7 +6,6 @@ package grpc import ( "context" "errors" - "io" "go.uber.org/zap" "google.golang.org/grpc" @@ -19,10 +18,9 @@ import ( // ProxyBuilder holds objects communicating with collector type ProxyBuilder struct { - reporter *reporter.ClientMetricsReporter - manager configmanager.ClientConfigManager - conn *grpc.ClientConn - tlsCloser io.Closer + reporter *reporter.ClientMetricsReporter + manager configmanager.ClientConfigManager + conn *grpc.ClientConn } // NewCollectorProxy creates ProxyBuilder @@ -40,10 +38,9 @@ func NewCollectorProxy(ctx context.Context, builder *ConnBuilder, agentTags map[ MetricsFactory: mFactory, }) return &ProxyBuilder{ - conn: conn, - reporter: r3, - manager: configmanager.WrapWithMetrics(grpcManager.NewConfigManager(conn), grpcMetrics), - tlsCloser: &builder.TLS, + conn: conn, + reporter: r3, + manager: configmanager.WrapWithMetrics(grpcManager.NewConfigManager(conn), grpcMetrics), }, nil } @@ -66,7 +63,6 @@ func (b ProxyBuilder) GetManager() configmanager.ClientConfigManager { func (b ProxyBuilder) Close() error { return errors.Join( b.reporter.Close(), - b.tlsCloser.Close(), b.GetConn().Close(), ) } diff --git a/cmd/agent/app/reporter/grpc/flags.go b/cmd/agent/app/reporter/grpc/flags.go index 47645c20cde..b19fe6ce421 100644 --- a/cmd/agent/app/reporter/grpc/flags.go +++ b/cmd/agent/app/reporter/grpc/flags.go @@ -44,7 +44,10 @@ func (b *ConnBuilder) InitFromViper(v *viper.Viper) (*ConnBuilder, error) { if err != nil { return b, fmt.Errorf("failed to process TLS options: %w", err) } - b.TLS = tls + if tls.Enabled { + tlsConf := tls.ToOtelClientConfig() + b.TLS = &tlsConf + } b.DiscoveryMinPeers = v.GetInt(discoveryMinPeers) return b, nil } diff --git a/cmd/agent/app/reporter/grpc/flags_test.go b/cmd/agent/app/reporter/grpc/flags_test.go index 4b2783aa4f9..a06e3e0e75f 100644 --- a/cmd/agent/app/reporter/grpc/flags_test.go +++ b/cmd/agent/app/reporter/grpc/flags_test.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/config/configtls" "github.com/jaegertracing/jaeger/pkg/config" ) @@ -32,6 +33,17 @@ func TestBindFlags(t *testing.T) { cOpts: []string{"--reporter.grpc.host-port=localhost:1111,localhost:2222", "--reporter.grpc.discovery.min-peers=5"}, expected: &ConnBuilder{CollectorHostPorts: []string{"localhost:1111", "localhost:2222"}, MaxRetry: defaultMaxRetry, DiscoveryMinPeers: 5}, }, + { + cOpts: []string{"--reporter.grpc.tls.enabled=true"}, + expected: &ConnBuilder{ + TLS: &configtls.ClientConfig{ + Config: configtls.Config{ + IncludeSystemCACertsPool: true, + }, + }, + MaxRetry: defaultMaxRetry, DiscoveryMinPeers: 3, + }, + }, } for _, test := range tests { v := viper.New() diff --git a/pkg/config/tlscfg/cert_watcher.go b/pkg/config/tlscfg/cert_watcher.go deleted file mode 100644 index cff06f4e0a4..00000000000 --- a/pkg/config/tlscfg/cert_watcher.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2020 The Jaeger Authors. -// SPDX-License-Identifier: Apache-2.0 - -package tlscfg - -import ( - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "path/filepath" - "sync" - - "go.uber.org/zap" - - "github.com/jaegertracing/jaeger/pkg/fswatcher" -) - -const ( - logMsgPairReloaded = "Reloaded modified key pair" - logMsgCertReloaded = "Reloaded modified certificate" - logMsgPairNotReloaded = "Failed to reload key pair, using previous versions" - logMsgCertNotReloaded = "Failed to reload certificate, using previous version" -) - -// certWatcher watches filesystem changes on certificates supplied via Options -// The changed RootCAs and ClientCAs certificates are added to x509.CertPool without invalidating the previously used certificate. -// The certificate and key can be obtained via certWatcher.certificate. -// The consumers of this API should use GetCertificate or GetClientCertificate from tls.Config to supply the certificate to the config. -type certWatcher struct { - mu sync.RWMutex - opts Options - logger *zap.Logger - watchers []*fswatcher.FSWatcher - cert *tls.Certificate -} - -var _ io.Closer = (*certWatcher)(nil) - -func newCertWatcher(opts Options, logger *zap.Logger, rootCAs, clientCAs *x509.CertPool) (*certWatcher, error) { - var cert *tls.Certificate - if opts.CertPath != "" && opts.KeyPath != "" { - // load certs at startup to catch missing certs error early - c, err := tls.LoadX509KeyPair(filepath.Clean(opts.CertPath), filepath.Clean(opts.KeyPath)) - if err != nil { - return nil, fmt.Errorf("failed to load server TLS cert and key: %w", err) - } - cert = &c - } - - w := &certWatcher{ - opts: opts, - logger: logger, - cert: cert, - } - - if err := w.watchCertPair(); err != nil { - return nil, err - } - if err := w.watchCert(w.opts.CAPath, rootCAs); err != nil { - return nil, err - } - if err := w.watchCert(w.opts.ClientCAPath, clientCAs); err != nil { - return nil, err - } - - return w, nil -} - -func (w *certWatcher) Close() error { - var errs []error - for _, w := range w.watchers { - errs = append(errs, w.Close()) - } - return errors.Join(errs...) -} - -func (w *certWatcher) certificate() *tls.Certificate { - w.mu.RLock() - defer w.mu.RUnlock() - return w.cert -} - -func (w *certWatcher) watchCertPair() error { - watcher, err := fswatcher.New( - []string{w.opts.CertPath, w.opts.KeyPath}, - w.onCertPairChange, - w.logger, - ) - if err == nil { - w.watchers = append(w.watchers, watcher) - return nil - } - w.Close() - return fmt.Errorf("failed to watch key pair %s and %s: %w", w.opts.KeyPath, w.opts.CertPath, err) -} - -func (w *certWatcher) watchCert(certPath string, certPool *x509.CertPool) error { - onCertChange := func() { w.onCertChange(certPath, certPool) } - - watcher, err := fswatcher.New([]string{certPath}, onCertChange, w.logger) - if err == nil { - w.watchers = append(w.watchers, watcher) - return nil - } - w.Close() - return fmt.Errorf("failed to watch cert %s: %w", certPath, err) -} - -func (w *certWatcher) onCertPairChange() { - cert, err := tls.LoadX509KeyPair(filepath.Clean(w.opts.CertPath), filepath.Clean(w.opts.KeyPath)) - if err == nil { - w.mu.Lock() - w.cert = &cert - w.mu.Unlock() - w.logger.Info( - logMsgPairReloaded, - zap.String("key", w.opts.KeyPath), - zap.String("cert", w.opts.CertPath), - ) - } else { - w.logger.Error( - logMsgPairNotReloaded, - zap.String("key", w.opts.KeyPath), - zap.String("cert", w.opts.CertPath), - zap.Error(err), - ) - } -} - -func (w *certWatcher) onCertChange(certPath string, certPool *x509.CertPool) { - w.mu.Lock() // prevent concurrent updates to the same certPool - if err := addCertToPool(certPath, certPool); err == nil { - w.logger.Info(logMsgCertReloaded, zap.String("cert", certPath)) - } else { - w.logger.Error(logMsgCertNotReloaded, zap.String("cert", certPath), zap.Error(err)) - } - w.mu.Unlock() -} diff --git a/pkg/config/tlscfg/cert_watcher_test.go b/pkg/config/tlscfg/cert_watcher_test.go deleted file mode 100644 index fdeefbadce5..00000000000 --- a/pkg/config/tlscfg/cert_watcher_test.go +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright (c) 2020 The Jaeger Authors. -// SPDX-License-Identifier: Apache-2.0 - -package tlscfg - -import ( - "crypto/tls" - "crypto/x509" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "go.uber.org/zap/zaptest/observer" -) - -const ( - serverCert = "./testdata/example-server-cert.pem" - serverKey = "./testdata/example-server-key.pem" - clientCert = "./testdata/example-client-cert.pem" - clientKey = "./testdata/example-client-key.pem" - - caCert = "./testdata/example-CA-cert.pem" - wrongCaCert = "./testdata/wrong-CA-cert.pem" - badCaCert = "./testdata/bad-CA-cert.txt" -) - -func copyToTempFile(t *testing.T, pattern string, filename string) (file *os.File, closeFn func()) { - tempFile, err := os.CreateTemp("", pattern+"_") - require.NoError(t, err) - - data, err := os.ReadFile(filename) - require.NoError(t, err) - - _, err = tempFile.Write(data) - require.NoError(t, err) - require.NoError(t, tempFile.Close()) - - return tempFile, func() { - // ignore error because some tests may remove the files earlier - _ = os.Remove(tempFile.Name()) - } -} - -func copyFile(t *testing.T, dest string, src string) { - certData, err := os.ReadFile(src) - require.NoError(t, err) - err = syncWrite(dest, certData, 0o644) - require.NoError(t, err) -} - -func TestReloadKeyPair(t *testing.T) { - // copy certs to temp so we can modify them - certFile, certFileCloseFn := copyToTempFile(t, "cert.crt", serverCert) - defer certFileCloseFn() - - keyFile, keyFileCloseFn := copyToTempFile(t, "key.crt", serverKey) - defer keyFileCloseFn() - - zcore, logObserver := observer.New(zapcore.InfoLevel) - logger := zap.New(zcore) - opts := Options{ - CAPath: caCert, - ClientCAPath: caCert, - CertPath: certFile.Name(), - KeyPath: keyFile.Name(), - } - certPool := x509.NewCertPool() - watcher, err := newCertWatcher(opts, logger, certPool, certPool) - require.NoError(t, err) - assert.NotNil(t, watcher.certificate()) - defer watcher.Close() - - require.NoError(t, err) - cert, err := tls.LoadX509KeyPair(serverCert, serverKey) - require.NoError(t, err) - assert.Equal(t, &cert, watcher.certificate()) - - // Replace certificate part of the pair with client's cert, which should fail to load. - copyFile(t, certFile.Name(), clientCert) - - assertLogs(t, logObserver, logMsgPairNotReloaded, [][2]string{{"key", keyFile.Name()}, {"cert", certFile.Name()}}) - assert.Equal(t, &cert, watcher.certificate(), "key pair unchanged") - logObserver.TakeAll() // clean up logs - - // Replace key part with client's private key. Valid pair, should reload. - copyFile(t, keyFile.Name(), clientKey) - - assertLogs(t, logObserver, logMsgPairReloaded, [][2]string{{"key", keyFile.Name()}, {"cert", certFile.Name()}}) - logObserver.TakeAll() // clean up logs - - cert, err = tls.LoadX509KeyPair(filepath.Clean(clientCert), clientKey) - require.NoError(t, err) - assert.Equal(t, &cert, watcher.certificate()) -} - -func TestReload_ca_certs(t *testing.T) { - // copy certs to temp so we can modify them - caFile, caFileCloseFn := copyToTempFile(t, "ca.crt", caCert) - defer caFileCloseFn() - clientCaFile, clientCaFileClostFn := copyToTempFile(t, "client-ca.crt", caCert) - defer clientCaFileClostFn() - - zcore, logObserver := observer.New(zapcore.InfoLevel) - logger := zap.New(zcore) - opts := Options{ - CAPath: caFile.Name(), - ClientCAPath: clientCaFile.Name(), - } - certPool := x509.NewCertPool() - watcher, err := newCertWatcher(opts, logger, certPool, certPool) - require.NoError(t, err) - defer watcher.Close() - - require.NoError(t, err) - - // update the content with different certs to trigger reload. - copyFile(t, caFile.Name(), wrongCaCert) - copyFile(t, clientCaFile.Name(), wrongCaCert) - - assertLogs(t, logObserver, logMsgCertReloaded, [][2]string{{"cert", caFile.Name()}}) - assertLogs(t, logObserver, logMsgCertReloaded, [][2]string{{"cert", clientCaFile.Name()}}) - logObserver.TakeAll() // clean up logs - - // update the content with invalid certs to trigger failed reload. - copyFile(t, caFile.Name(), badCaCert) - copyFile(t, clientCaFile.Name(), badCaCert) - - assertLogs(t, logObserver, logMsgCertNotReloaded, [][2]string{{"cert", caFile.Name()}}) - assertLogs(t, logObserver, logMsgCertNotReloaded, [][2]string{{"cert", clientCaFile.Name()}}) -} - -func TestReload_err_cert_update(t *testing.T) { - // copy certs to temp so we can modify them - certFile, certFileCloseFn := copyToTempFile(t, "cert.crt", serverCert) - defer certFileCloseFn() - keyFile, keyFileCloseFn := copyToTempFile(t, "key.crt", serverKey) - defer keyFileCloseFn() - - zcore, logObserver := observer.New(zapcore.InfoLevel) - logger := zap.New(zcore) - opts := Options{ - CAPath: caCert, - ClientCAPath: caCert, - CertPath: certFile.Name(), - KeyPath: keyFile.Name(), - } - certPool := x509.NewCertPool() - watcher, err := newCertWatcher(opts, logger, certPool, certPool) - require.NoError(t, err) - assert.NotNil(t, watcher.certificate()) - defer watcher.Close() - - require.NoError(t, err) - serverCert, err := tls.LoadX509KeyPair(filepath.Clean(serverCert), filepath.Clean(serverKey)) - require.NoError(t, err) - assert.Equal(t, &serverCert, watcher.certificate()) - - // update the content with bad client certs - copyFile(t, certFile.Name(), badCaCert) - copyFile(t, keyFile.Name(), clientKey) - - assertLogs(t, logObserver, logMsgPairNotReloaded, [][2]string{{"key", opts.KeyPath}, {"cert", opts.CertPath}}) - assert.Equal(t, &serverCert, watcher.certificate(), "values unchanged") -} - -func TestReload_kubernetes_secret_update(t *testing.T) { - mountDir := t.TempDir() - - // Create directory layout before update: - // - // /secret-mountpoint/ca.crt # symbolic link to ..data/ca.crt - // /secret-mountpoint/tls.crt # symbolic link to ..data/tls.crt - // /secret-mountpoint/tls.key # symbolic link to ..data/tls.key - // /secret-mountpoint/..data # symbolic link to ..timestamp-1 - // /secret-mountpoint/..timestamp-1 # directory - // /secret-mountpoint/..timestamp-1/ca.crt # initial version of ca.crt - // /secret-mountpoint/..timestamp-1/tls.crt # initial version of tls.crt - // /secret-mountpoint/..timestamp-1/tls.key # initial version of tls.key - - err := os.Symlink("..timestamp-1", filepath.Join(mountDir, "..data")) - require.NoError(t, err) - err = os.Symlink(filepath.Join("..data", "ca.crt"), filepath.Join(mountDir, "ca.crt")) - require.NoError(t, err) - err = os.Symlink(filepath.Join("..data", "tls.crt"), filepath.Join(mountDir, "tls.crt")) - require.NoError(t, err) - err = os.Symlink(filepath.Join("..data", "tls.key"), filepath.Join(mountDir, "tls.key")) - require.NoError(t, err) - - timestamp1Dir := filepath.Join(mountDir, "..timestamp-1") - createTimestampDir(t, timestamp1Dir, caCert, serverCert, serverKey) - - opts := Options{ - CAPath: filepath.Join(mountDir, "ca.crt"), - ClientCAPath: filepath.Join(mountDir, "ca.crt"), - CertPath: filepath.Join(mountDir, "tls.crt"), - KeyPath: filepath.Join(mountDir, "tls.key"), - } - - zcore, logObserver := observer.New(zapcore.InfoLevel) - logger := zap.New(zcore) - - certPool := x509.NewCertPool() - - watcher, err := newCertWatcher(opts, logger, certPool, certPool) - require.NoError(t, err) - defer watcher.Close() - - require.NoError(t, err) - - expectedCert, err := tls.LoadX509KeyPair(serverCert, serverKey) - require.NoError(t, err) - - assert.Equal(t, - expectedCert.Certificate, - watcher.certificate().Certificate, - "certificate should be updated: %v", logObserver.All(), - ) - - logObserver.TakeAll() // clean logs - - // Create second dir with different key pair and CA. - // After the update, the directory looks like following: - // - // /secret-mountpoint/ca.crt # symbolic link to ..data/ca.crt - // /secret-mountpoint/tls.crt # symbolic link to ..data/tls.crt - // /secret-mountpoint/tls.key # symbolic link to ..data/tls.key - // /secret-mountpoint/..data # symbolic link to ..timestamp-2 - // /secret-mountpoint/..timestamp-2 # new directory - // /secret-mountpoint/..timestamp-2/ca.crt # new version of ca.crt - // /secret-mountpoint/..timestamp-2/tls.crt # new version of tls.crt - // /secret-mountpoint/..timestamp-2/tls.key # new version of tls.key - - timestamp2Dir := filepath.Join(mountDir, "..timestamp-2") - createTimestampDir(t, timestamp2Dir, serverCert, clientCert, clientKey) - - err = os.Symlink("..timestamp-2", filepath.Join(mountDir, "..data_tmp")) - require.NoError(t, err) - - os.Rename(filepath.Join(mountDir, "..data_tmp"), filepath.Join(mountDir, "..data")) - require.NoError(t, err) - err = os.RemoveAll(timestamp1Dir) - require.NoError(t, err) - - assertLogs(t, logObserver, logMsgPairReloaded, [][2]string{{"key", opts.KeyPath}, {"cert", opts.CertPath}}) - assertLogs(t, logObserver, logMsgCertReloaded, [][2]string{{"cert", opts.CAPath}}) - - expectedCert, err = tls.LoadX509KeyPair(clientCert, clientKey) - require.NoError(t, err) - assert.Equal(t, expectedCert.Certificate, watcher.certificate().Certificate, - "certificate should be updated: %v", logObserver.All()) - - logObserver.TakeAll() // clean logs - - // Make third update to make sure that the watcher is still working. - timestamp3Dir := filepath.Join(mountDir, "..timestamp-3") - createTimestampDir(t, timestamp3Dir, caCert, serverCert, serverKey) - err = os.Symlink("..timestamp-3", filepath.Join(mountDir, "..data_tmp")) - require.NoError(t, err) - os.Rename(filepath.Join(mountDir, "..data_tmp"), filepath.Join(mountDir, "..data")) - require.NoError(t, err) - err = os.RemoveAll(timestamp2Dir) - require.NoError(t, err) - - assertLogs(t, logObserver, logMsgPairReloaded, [][2]string{{"key", opts.KeyPath}, {"cert", opts.CertPath}}) - assertLogs(t, logObserver, logMsgCertReloaded, [][2]string{{"cert", opts.CAPath}}) - - expectedCert, err = tls.LoadX509KeyPair(serverCert, serverKey) - require.NoError(t, err) - assert.Equal(t, - expectedCert.Certificate, - watcher.certificate().Certificate, - "certificate should be updated", - ) -} - -func createTimestampDir(t *testing.T, dir string, ca, cert, key string) { - t.Helper() - err := os.MkdirAll(dir, 0o700) - require.NoError(t, err) - - data, err := os.ReadFile(ca) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "ca.crt"), data, 0o600) - require.NoError(t, err) - data, err = os.ReadFile(cert) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "tls.crt"), data, 0o600) - require.NoError(t, err) - data, err = os.ReadFile(key) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, "tls.key"), data, 0o600) - require.NoError(t, err) -} - -func TestAddCertsToWatch_err(t *testing.T) { - tests := []struct { - opts Options - }{ - { - opts: Options{ - CAPath: "doesnotexists", - }, - }, - { - opts: Options{ - CAPath: caCert, - ClientCAPath: "doesnotexists", - }, - }, - { - opts: Options{ - CAPath: caCert, - ClientCAPath: caCert, - CertPath: "doesnotexists", - }, - }, - { - opts: Options{ - CAPath: caCert, - ClientCAPath: caCert, - CertPath: serverCert, - KeyPath: "doesnotexists", - }, - }, - } - for _, test := range tests { - watcher, err := newCertWatcher(test.opts, nil, nil, nil) - require.ErrorContains(t, err, "no such file or directory") - assert.Nil(t, watcher) - } -} - -func assertLogs(t *testing.T, - logs *observer.ObservedLogs, - logMsg string, - fields [][2]string, -) { - errMsg := "Expecting log '" + logMsg + "'" - for _, field := range fields { - errMsg = errMsg + " " + field[0] + "=" + field[1] - } - fn := func() bool { - l := logs - if logMsg != "" { - l = l.FilterMessageSnippet(logMsg) - } - for _, field := range fields { - l = l.FilterField(zap.String(field[0], field[1])) - } - return l.Len() > 0 - } - ok := assert.Eventuallyf(t, fn, 5*time.Second, 10*time.Millisecond, errMsg) - if !ok { - for _, log := range logs.All() { - t.Log(log) - } - } -} - -// syncWrite ensures data is written to the given filename and flushed to disk. -// This ensures that any watchers looking for file system changes can be reliably alerted. -func syncWrite(filename string, data []byte, perm os.FileMode) error { - f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, perm) - if err != nil { - return err - } - defer f.Close() - if _, err = f.Write(data); err != nil { - return err - } - return f.Sync() -} diff --git a/pkg/config/tlscfg/certpool_unix.go b/pkg/config/tlscfg/certpool_unix.go deleted file mode 100644 index 15943a21c97..00000000000 --- a/pkg/config/tlscfg/certpool_unix.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2021 The Jaeger Authors. -// SPDX-License-Identifier: Apache-2.0 - -//go:build !windows -// +build !windows - -package tlscfg - -import ( - "crypto/x509" -) - -func loadSystemCertPool() (*x509.CertPool, error) { - return systemCertPool() -} diff --git a/pkg/config/tlscfg/certpool_windows.go b/pkg/config/tlscfg/certpool_windows.go deleted file mode 100644 index fae3a4b2ebf..00000000000 --- a/pkg/config/tlscfg/certpool_windows.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2021 The Jaeger Authors. -// SPDX-License-Identifier: Apache-2.0 - -//go:build windows -// +build windows - -package tlscfg - -import ( - "crypto/x509" - "syscall" - "unsafe" -) - -const ( - // CRYPT_E_NOT_FOUND is an error code specific to windows cert pool. - // See https://github.com/golang/go/issues/16736#issuecomment-540373689. - CRYPT_E_NOT_FOUND = 0x80092004 -) - -// workaround https://github.com/golang/go/issues/16736 -// fix borrowed from Sensu: https://github.com/sensu/sensu-go/pull/4018 -func appendCerts(rootCAs *x509.CertPool) (*x509.CertPool, error) { - name, _ := syscall.UTF16PtrFromString("Root") - storeHandle, err := syscall.CertOpenSystemStore(0, name) - if err != nil { - return nil, err - } - - var cert *syscall.CertContext - for { - cert, err = syscall.CertEnumCertificatesInStore(storeHandle, cert) - if err != nil { - if errno, ok := err.(syscall.Errno); ok { - if errno == CRYPT_E_NOT_FOUND { - break - } - } - return nil, err - } - if cert == nil { - break - } - // Copy the buf, since ParseCertificate does not create its own copy. - buf := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:] - buf2 := make([]byte, cert.Length) - copy(buf2, buf) - if c, err := x509.ParseCertificate(buf2); err == nil { - rootCAs.AddCert(c) - } - } - return rootCAs, nil -} - -func loadSystemCertPool() (*x509.CertPool, error) { - certPool, err := systemCertPool() - if err != nil { - return nil, err - } - if certPool == nil { - certPool = x509.NewCertPool() - } - return appendCerts(certPool) -} diff --git a/pkg/config/tlscfg/ciphersuites.go b/pkg/config/tlscfg/ciphersuites.go deleted file mode 100644 index dc667deed02..00000000000 --- a/pkg/config/tlscfg/ciphersuites.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2022 The Jaeger Authors. -// SPDX-License-Identifier: Apache-2.0 - -package tlscfg - -import ( - "crypto/tls" - "fmt" -) - -// https://pkg.go.dev/crypto/tls#pkg-constants -var versions = map[string]uint16{ - "1.0": tls.VersionTLS10, - "1.1": tls.VersionTLS11, - "1.2": tls.VersionTLS12, - "1.3": tls.VersionTLS13, -} - -func allCiphers() map[string]uint16 { - acceptedCiphers := make(map[string]uint16) - for _, suite := range tls.CipherSuites() { - acceptedCiphers[suite.Name] = suite.ID - } - return acceptedCiphers -} - -// CipherSuiteNamesToIDs returns a list of cipher suite IDs from the cipher suite names passed. -func CipherSuiteNamesToIDs(cipherNames []string) ([]uint16, error) { - var ciphersIDs []uint16 - possibleCiphers := allCiphers() - for _, cipher := range cipherNames { - intValue, ok := possibleCiphers[cipher] - if !ok { - return nil, fmt.Errorf("cipher suite %s not supported or doesn't exist", cipher) - } - ciphersIDs = append(ciphersIDs, intValue) - } - return ciphersIDs, nil -} - -// VersionNameToID returns the version ID from version name -func VersionNameToID(versionName string) (uint16, error) { - if version, ok := versions[versionName]; ok { - return version, nil - } - return 0, fmt.Errorf("unknown tls version %q", versionName) -} diff --git a/pkg/config/tlscfg/ciphersuites_test.go b/pkg/config/tlscfg/ciphersuites_test.go deleted file mode 100644 index 006a9ab9ed0..00000000000 --- a/pkg/config/tlscfg/ciphersuites_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 The Jaeger Authors. -// SPDX-License-Identifier: Apache-2.0 - -package tlscfg - -import ( - "crypto/tls" - "reflect" - "testing" -) - -func TestCipherSuiteNamesToIDs(t *testing.T) { - tests := []struct { - flag []string - expected []uint16 - expectedError bool - }{ - { - // Happy case - flag: []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}, - expected: []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384}, - expectedError: false, - }, - { - // One flag only - flag: []string{"TLS_AES_128_GCM_SHA256"}, - expected: []uint16{tls.TLS_AES_128_GCM_SHA256}, - expectedError: false, - }, - { - // Empty flag - flag: []string{}, - expected: nil, - expectedError: false, - }, - { - // Duplicated flag - flag: []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"}, - expected: []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_AES_128_GCM_SHA256}, - expectedError: false, - }, - { - // Invalid flag - flag: []string{"TLS_INVALID_CIPHER_SUITE"}, - expected: nil, - expectedError: true, - }, - } - - for i, test := range tests { - uIntFlags, err := CipherSuiteNamesToIDs(test.flag) - if !reflect.DeepEqual(uIntFlags, test.expected) { - t.Errorf("%d: expected %+v, got %+v", i, test.expected, uIntFlags) - } - if test.expectedError && err == nil { - t.Errorf("%d: expecting error, got %+v", i, err) - } - } -} - -func TestVersionNameToID(t *testing.T) { - tests := []struct { - flag string - expected uint16 - expectedError bool - }{ - { - // Happy case - flag: "1.1", - expected: tls.VersionTLS11, - expectedError: false, - }, - { - // Invalid flag - flag: "Invalid", - expected: 0, - expectedError: true, - }, - } - - for i, test := range tests { - uIntFlag, err := VersionNameToID(test.flag) - if !reflect.DeepEqual(uIntFlag, test.expected) { - t.Errorf("%d: expected %+v, got %+v", i, test.expected, uIntFlag) - } - if test.expectedError && err == nil { - t.Errorf("%d: expecting error, got %+v", i, err) - } - } -} diff --git a/pkg/config/tlscfg/options.go b/pkg/config/tlscfg/options.go index c67dcb2b2ef..2ca55f5f7b4 100644 --- a/pkg/config/tlscfg/options.go +++ b/pkg/config/tlscfg/options.go @@ -4,17 +4,9 @@ package tlscfg import ( - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "os" - "path/filepath" "time" "go.opentelemetry.io/collector/config/configtls" - "go.uber.org/zap" ) // Options describes the configuration properties for TLS Connections. @@ -30,100 +22,6 @@ type Options struct { MaxVersion string `mapstructure:"max_version"` SkipHostVerify bool `mapstructure:"skip_host_verify"` ReloadInterval time.Duration `mapstructure:"reload_interval"` - certWatcher *certWatcher -} - -var systemCertPool = x509.SystemCertPool // to allow overriding in unit test - -// Config loads TLS certificates and returns a TLS Config. -func (o *Options) Config(logger *zap.Logger) (*tls.Config, error) { - var minVersionId, maxVersionId uint16 - - certPool, err := o.loadCertPool() - if err != nil { - return nil, fmt.Errorf("failed to load CA CertPool: %w", err) - } - - cipherSuiteIds, err := CipherSuiteNamesToIDs(o.CipherSuites) - if err != nil { - return nil, fmt.Errorf("failed to get cipher suite ids from cipher suite names: %w", err) - } - - if o.MinVersion != "" { - minVersionId, err = VersionNameToID(o.MinVersion) - if err != nil { - return nil, fmt.Errorf("failed to get minimum tls version: %w", err) - } - } - - if o.MaxVersion != "" { - maxVersionId, err = VersionNameToID(o.MaxVersion) - if err != nil { - return nil, fmt.Errorf("failed to get maximum tls version: %w", err) - } - } - - if o.MinVersion != "" && o.MaxVersion != "" { - if minVersionId > maxVersionId { - return nil, errors.New("minimum tls version can't be greater than maximum tls version") - } - } - - tlsCfg := &tls.Config{ - RootCAs: certPool, - ServerName: o.ServerName, - InsecureSkipVerify: o.SkipHostVerify, /* #nosec G402*/ - CipherSuites: cipherSuiteIds, - MinVersion: minVersionId, - MaxVersion: maxVersionId, - } - - if o.ClientCAPath != "" { - // TODO this should be moved to certWatcher, since it already loads key pair - certPool := x509.NewCertPool() - if err := addCertToPool(o.ClientCAPath, certPool); err != nil { - return nil, err - } - tlsCfg.ClientCAs = certPool - tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert - } - - certWatcher, err := newCertWatcher(*o, logger, tlsCfg.RootCAs, tlsCfg.ClientCAs) - if err != nil { - return nil, err - } - o.certWatcher = certWatcher - - if (o.CertPath == "" && o.KeyPath != "") || (o.CertPath != "" && o.KeyPath == "") { - return nil, errors.New("for client auth via TLS, either both client certificate and key must be supplied, or neither") - } - if o.CertPath != "" && o.KeyPath != "" { - tlsCfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { - return o.certWatcher.certificate(), nil - } - // GetClientCertificate is used on the client side when server is configured with tls.RequireAndVerifyClientCert e.g. mTLS - tlsCfg.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { - return o.certWatcher.certificate(), nil - } - } - - return tlsCfg, nil -} - -func (o Options) loadCertPool() (*x509.CertPool, error) { - if len(o.CAPath) == 0 { // no truststore given, use SystemCertPool - certPool, err := loadSystemCertPool() - if err != nil { - return nil, fmt.Errorf("failed to load SystemCertPool: %w", err) - } - return certPool, nil - } - certPool := x509.NewCertPool() - // setup user specified truststore - if err := addCertToPool(o.CAPath, certPool); err != nil { - return nil, err - } - return certPool, nil } func (o *Options) ToOtelClientConfig() configtls.ClientConfig { @@ -172,25 +70,3 @@ func (o *Options) ToOtelServerConfig() *configtls.ServerConfig { return cfg } - -func addCertToPool(caPath string, certPool *x509.CertPool) error { - caPEM, err := os.ReadFile(filepath.Clean(caPath)) - if err != nil { - return fmt.Errorf("failed to load CA %s: %w", caPath, err) - } - - if !certPool.AppendCertsFromPEM(caPEM) { - return fmt.Errorf("failed to parse CA %s", caPath) - } - return nil -} - -var _ io.Closer = (*Options)(nil) - -// Close shuts down the embedded certificate watcher. -func (o *Options) Close() error { - if o.certWatcher != nil { - return o.certWatcher.Close() - } - return nil -} diff --git a/pkg/config/tlscfg/options_test.go b/pkg/config/tlscfg/options_test.go index 4a1d5d46555..342c1ff23f5 100644 --- a/pkg/config/tlscfg/options_test.go +++ b/pkg/config/tlscfg/options_test.go @@ -4,179 +4,13 @@ package tlscfg import ( - "crypto/tls" - "crypto/x509" - "errors" - "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/config/configtls" - "go.uber.org/zap" ) -var testCertKeyLocation = "./testdata" - -func TestOptionsToConfig(t *testing.T) { - tests := []struct { - name string - options Options - fakeSysPool bool - expectError string - }{ - { - name: "should load system CA", - options: Options{CAPath: ""}, - }, - { - name: "should fail with fake system CA", - fakeSysPool: true, - options: Options{CAPath: ""}, - expectError: "fake system pool", - }, - { - name: "should load custom CA", - options: Options{CAPath: testCertKeyLocation + "/example-CA-cert.pem"}, - }, - { - name: "should fail with invalid CA file path", - options: Options{CAPath: testCertKeyLocation + "/not/valid"}, - expectError: "failed to load CA", - }, - { - name: "should fail with invalid CA file content", - options: Options{CAPath: testCertKeyLocation + "/bad-CA-cert.txt"}, - expectError: "failed to parse CA", - }, - { - name: "should load valid TLS Client settings", - options: Options{ - CAPath: testCertKeyLocation + "/example-CA-cert.pem", - CertPath: testCertKeyLocation + "/example-client-cert.pem", - KeyPath: testCertKeyLocation + "/example-client-key.pem", - }, - }, - { - name: "should fail with missing TLS Client Key", - options: Options{ - CAPath: testCertKeyLocation + "/example-CA-cert.pem", - CertPath: testCertKeyLocation + "/example-client-cert.pem", - }, - expectError: "both client certificate and key must be supplied", - }, - { - name: "should fail with invalid TLS Client Key", - options: Options{ - CAPath: testCertKeyLocation + "/example-CA-cert.pem", - CertPath: testCertKeyLocation + "/example-client-cert.pem", - KeyPath: testCertKeyLocation + "/not/valid", - }, - expectError: "failed to load server TLS cert and key", - }, - { - name: "should fail with missing TLS Client Cert", - options: Options{ - CAPath: testCertKeyLocation + "/example-CA-cert.pem", - KeyPath: testCertKeyLocation + "/example-client-key.pem", - }, - expectError: "both client certificate and key must be supplied", - }, - { - name: "should fail with invalid TLS Client Cert", - options: Options{ - CAPath: testCertKeyLocation + "/example-CA-cert.pem", - CertPath: testCertKeyLocation + "/not/valid", - KeyPath: testCertKeyLocation + "/example-client-key.pem", - }, - expectError: "failed to load server TLS cert and key", - }, - { - name: "should fail with invalid TLS Client CA", - options: Options{ - ClientCAPath: testCertKeyLocation + "/not/valid", - }, - expectError: "failed to load CA", - }, - { - name: "should fail with invalid TLS Client CA pool", - options: Options{ - ClientCAPath: testCertKeyLocation + "/bad-CA-cert.txt", - }, - expectError: "failed to parse CA", - }, - { - name: "should pass with valid TLS Client CA pool", - options: Options{ - ClientCAPath: testCertKeyLocation + "/example-CA-cert.pem", - }, - }, - { - name: "should fail with invalid TLS Cipher Suite", - options: Options{ - CipherSuites: []string{"TLS_INVALID_CIPHER_SUITE"}, - }, - expectError: "failed to get cipher suite ids from cipher suite names: cipher suite TLS_INVALID_CIPHER_SUITE not supported or doesn't exist", - }, - { - name: "should fail with invalid TLS Min Version", - options: Options{ - MinVersion: "Invalid", - }, - expectError: "failed to get minimum tls version", - }, - { - name: "should fail with invalid TLS Max Version", - options: Options{ - MaxVersion: "Invalid", - }, - expectError: "failed to get maximum tls version", - }, - { - name: "should fail with TLS Min Version greater than TLS Max Version error", - options: Options{ - MinVersion: "1.2", - MaxVersion: "1.1", - }, - expectError: "minimum tls version can't be greater than maximum tls version", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.fakeSysPool { - saveSystemCertPool := systemCertPool - systemCertPool = func() (*x509.CertPool, error) { - return nil, errors.New("fake system pool") - } - defer func() { - systemCertPool = saveSystemCertPool - }() - } - cfg, err := test.options.Config(zap.NewNop()) - if test.expectError != "" { - require.ErrorContains(t, err, test.expectError) - } else { - require.NoError(t, err) - assert.NotNil(t, cfg) - - if test.options.CertPath != "" && test.options.KeyPath != "" { - c, e := tls.LoadX509KeyPair(filepath.Clean(test.options.CertPath), filepath.Clean(test.options.KeyPath)) - require.NoError(t, e) - cert, err := cfg.GetCertificate(&tls.ClientHelloInfo{}) - require.NoError(t, err) - assert.Equal(t, &c, cert) - cert, err = cfg.GetClientCertificate(&tls.CertificateRequestInfo{}) - require.NoError(t, err) - assert.Equal(t, &c, cert) - } - } - require.NoError(t, test.options.Close()) - }) - } -} - func TestToOtelClientConfig(t *testing.T) { testCases := []struct { name string