Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new env config options for OTLP exporter #3363

Merged
merged 13 commits into from Nov 8, 2022
10 changes: 10 additions & 0 deletions CHANGELOG.md
Expand Up @@ -14,6 +14,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
This option is used to configure the view(s) a `MeterProvider` will use for all `Reader`s that are registered with it. (#3387)
- Add Instrumentation Scope and Version as info metric and label in Prometheus exporter.
This can be disabled using the `WithoutScopeInfo()` option added to that package.(#3273, #3357)
- OTLP exporters now recognize: (#3363)
- `OTEL_EXPORTER_OTLP_INSECURE`
- `OTEL_EXPORTER_OTLP_TRACES_INSECURE`
- `OTEL_EXPORTER_OTLP_METRICS_INSECURE`
- `OTEL_EXPORTER_OTLP_CLIENT_KEY`
- `OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY`
- `OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY`
- `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE`
- `OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE`
- `OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE`

### Changed

Expand Down
81 changes: 66 additions & 15 deletions exporters/otlp/internal/envconfig/envconfig.go
Expand Up @@ -23,6 +23,8 @@ import (
"strconv"
"strings"
"time"

"go.opentelemetry.io/otel/internal/global"
)

// ConfigFn is the generic function used to set a config.
Expand Down Expand Up @@ -59,13 +61,26 @@ func WithString(n string, fn func(string)) func(e *EnvOptionsReader) {
}
}

// WithBool returns a ConfigFn that reads the environment variable n and if it exists passes its parsed bool value to fn.
func WithBool(n string, fn func(bool)) ConfigFn {
return func(e *EnvOptionsReader) {
if v, ok := e.GetEnvValue(n); ok {
b := strings.ToLower(v) == "true"
fn(b)
}
}
}

// WithDuration retrieves the specified config and passes it to ConfigFn as a duration.
func WithDuration(n string, fn func(time.Duration)) func(e *EnvOptionsReader) {
return func(e *EnvOptionsReader) {
if v, ok := e.GetEnvValue(n); ok {
if d, err := strconv.Atoi(v); err == nil {
fn(time.Duration(d) * time.Millisecond)
d, err := strconv.Atoi(v)
if err != nil {
global.Error(err, "parse duration", "input", v)
return
}
fn(time.Duration(d) * time.Millisecond)
}
}
}
Expand All @@ -83,23 +98,59 @@ func WithHeaders(n string, fn func(map[string]string)) func(e *EnvOptionsReader)
func WithURL(n string, fn func(*url.URL)) func(e *EnvOptionsReader) {
return func(e *EnvOptionsReader) {
if v, ok := e.GetEnvValue(n); ok {
if u, err := url.Parse(v); err == nil {
fn(u)
u, err := url.Parse(v)
if err != nil {
global.Error(err, "parse url", "input", v)
return
}
fn(u)
}
}
}

// WithTLSConfig retrieves the specified config and passes it to ConfigFn as a crypto/tls.Config.
func WithTLSConfig(n string, fn func(*tls.Config)) func(e *EnvOptionsReader) {
// WithCertPool returns a ConfigFn that reads the environment variable n as a filepath to a TLS certificate pool. If it exists, it is parsed as a crypto/x509.CertPool and it is passed to fn.
func WithCertPool(n string, fn func(*x509.CertPool)) ConfigFn {
return func(e *EnvOptionsReader) {
if v, ok := e.GetEnvValue(n); ok {
if b, err := e.ReadFile(v); err == nil {
if c, err := createTLSConfig(b); err == nil {
fn(c)
}
b, err := e.ReadFile(v)
if err != nil {
global.Error(err, "read tls ca cert file", "file", v)
return
}
c, err := createCertPool(b)
if err != nil {
global.Error(err, "create tls cert pool")
return
}
fn(c)
}
}
}

// WithClientCert returns a ConfigFn that reads the environment variable nc and nk as filepaths to a client certificate and key pair. If they exists, they are parsed as a crypto/tls.Certificate and it is passed to fn.
func WithClientCert(nc, nk string, fn func(tls.Certificate)) ConfigFn {
return func(e *EnvOptionsReader) {
vc, okc := e.GetEnvValue(nc)
vk, okk := e.GetEnvValue(nk)
if !okc || !okk {
return
}
cert, err := e.ReadFile(vc)
if err != nil {
global.Error(err, "read tls client cert", "file", vc)
return
}
key, err := e.ReadFile(vk)
if err != nil {
global.Error(err, "read tls client key", "file", vk)
return
}
crt, err := tls.X509KeyPair(cert, key)
if err != nil {
seankhliao marked this conversation as resolved.
Show resolved Hide resolved
global.Error(err, "create tls client key pair")
return
}
fn(crt)
}
}

Expand All @@ -117,15 +168,18 @@ func stringToHeader(value string) map[string]string {
for _, header := range headersPairs {
nameValue := strings.SplitN(header, "=", 2)
if len(nameValue) < 2 {
global.Error(errors.New("missing '="), "parse headers", "input", nameValue)
continue
}
name, err := url.QueryUnescape(nameValue[0])
if err != nil {
global.Error(err, "escape header key", "key", nameValue[0])
continue
}
trimmedName := strings.TrimSpace(name)
value, err := url.QueryUnescape(nameValue[1])
if err != nil {
global.Error(err, "escape header value", "value", nameValue[1])
continue
}
trimmedValue := strings.TrimSpace(value)
Expand All @@ -136,13 +190,10 @@ func stringToHeader(value string) map[string]string {
return headers
}

func createTLSConfig(certBytes []byte) (*tls.Config, error) {
func createCertPool(certBytes []byte) (*x509.CertPool, error) {
cp := x509.NewCertPool()
if ok := cp.AppendCertsFromPEM(certBytes); !ok {
return nil, errors.New("failed to append certificate to the cert pool")
}

return &tls.Config{
RootCAs: cp,
}, nil
return cp, nil
}
142 changes: 128 additions & 14 deletions exporters/otlp/internal/envconfig/envconfig_test.go
Expand Up @@ -16,29 +16,40 @@ package envconfig // import "go.opentelemetry.io/otel/exporters/otlp/internal/en

import (
"crypto/tls"
"crypto/x509"
"errors"
"net/url"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

const WeakKey = `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEbrSPmnlSOXvVzxCyv+VR3a0HDeUTvOcqrdssZ2k4gFoAoGCCqGSM49
AwEHoUQDQgAEDMTfv75J315C3K9faptS9iythKOMEeV/Eep73nWX531YAkmmwBSB
2dXRD/brsgLnfG57WEpxZuY7dPRbxu33BA==
-----END EC PRIVATE KEY-----
`

const WeakCertificate = `
-----BEGIN CERTIFICATE-----
MIIBhzCCASygAwIBAgIRANHpHgAWeTnLZpTSxCKs0ggwCgYIKoZIzj0EAwIwEjEQ
MA4GA1UEChMHb3RlbC1nbzAeFw0yMTA0MDExMzU5MDNaFw0yMTA0MDExNDU5MDNa
MBIxEDAOBgNVBAoTB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS9
nWSkmPCxShxnp43F+PrOtbGV7sNfkbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0Z
sJCLHGogQsYnWJBXUZOVo2MwYTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI
KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAA
AAAAAAAAAAAAAAAAAAGHBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhANwZVVKvfvQ/
1HXsTvgH+xTQswOwSSKYJ1cVHQhqK7ZbAiEAus8NxpTRnp5DiTMuyVmhVNPB+bVH
Lhnm4N/QDk5rek0=
MIIBjjCCATWgAwIBAgIUKQSMC66MUw+kPp954ZYOcyKAQDswCgYIKoZIzj0EAwIw
EjEQMA4GA1UECgwHb3RlbC1nbzAeFw0yMjEwMTkwMDA5MTlaFw0yMzEwMTkwMDA5
MTlaMBIxEDAOBgNVBAoMB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AAQMxN+/vknfXkLcr19qm1L2LK2Eo4wR5X8R6nvedZfnfVgCSabAFIHZ1dEP9uuy
Aud8bntYSnFm5jt09FvG7fcEo2kwZzAdBgNVHQ4EFgQUicGuhnTTkYLZwofXMNLK
SHFeCWgwHwYDVR0jBBgwFoAUicGuhnTTkYLZwofXMNLKSHFeCWgwDwYDVR0TAQH/
BAUwAwEB/zAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDRwAwRAIg
Lfma8FnnxeSOi6223AsFfYwsNZ2RderNsQrS0PjEHb0CIBkrWacqARUAu7uT4cGu
jVcIxYQqhId5L8p/mAv2PWZS
-----END CERTIFICATE-----
`

type testOption struct {
TestString string
TestBool bool
TestDuration time.Duration
TestHeaders map[string]string
TestURL *url.URL
Expand Down Expand Up @@ -134,6 +145,56 @@ func TestEnvConfig(t *testing.T) {
},
expectedOptions: []testOption{},
},
{
name: "with a bool config",
reader: EnvOptionsReader{
GetEnv: func(n string) string {
if n == "HELLO" {
return "true"
} else if n == "WORLD" {
return "false"
}
return ""
},
},
configs: []ConfigFn{
WithBool("HELLO", func(b bool) {
options = append(options, testOption{TestBool: b})
}),
WithBool("WORLD", func(b bool) {
options = append(options, testOption{TestBool: b})
}),
},
expectedOptions: []testOption{
{
TestBool: true,
},
{
TestBool: false,
},
},
},
{
name: "with an invalid bool config",
reader: EnvOptionsReader{
GetEnv: func(n string) string {
if n == "HELLO" {
return "world"
}
return ""
},
},
configs: []ConfigFn{
WithBool("HELLO", func(b bool) {
options = append(options, testOption{TestBool: b})
}),
},
expectedOptions: []testOption{
{
TestBool: false,
},
},
},
{
name: "with a duration config",
reader: EnvOptionsReader{
Expand Down Expand Up @@ -265,7 +326,7 @@ func TestEnvConfig(t *testing.T) {
}

func TestWithTLSConfig(t *testing.T) {
tlsCert, err := createTLSConfig([]byte(WeakCertificate))
pool, err := createCertPool([]byte(WeakCertificate))
assert.NoError(t, err)

reader := EnvOptionsReader{
Expand All @@ -285,12 +346,65 @@ func TestWithTLSConfig(t *testing.T) {

var option testOption
reader.Apply(
WithTLSConfig("CERTIFICATE", func(v *tls.Config) {
option = testOption{TestTLS: v}
}))
WithCertPool("CERTIFICATE", func(cp *x509.CertPool) {
option = testOption{TestTLS: &tls.Config{RootCAs: cp}}
}),
)

// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
assert.Equal(t, tlsCert.RootCAs.Subjects(), option.TestTLS.RootCAs.Subjects())
assert.Equal(t, pool.Subjects(), option.TestTLS.RootCAs.Subjects())
}

func TestWithClientCert(t *testing.T) {
cert, err := tls.X509KeyPair([]byte(WeakCertificate), []byte(WeakKey))
assert.NoError(t, err)

reader := EnvOptionsReader{
GetEnv: func(n string) string {
switch n {
case "CLIENT_CERTIFICATE":
return "/path/tls.crt"
case "CLIENT_KEY":
return "/path/tls.key"
}
return ""
},
ReadFile: func(n string) ([]byte, error) {
switch n {
case "/path/tls.crt":
return []byte(WeakCertificate), nil
case "/path/tls.key":
return []byte(WeakKey), nil
}
return []byte{}, nil
},
}

var option testOption
reader.Apply(
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
}),
)
assert.Equal(t, cert, option.TestTLS.Certificates[0])

reader.ReadFile = func(s string) ([]byte, error) { return nil, errors.New("oops") }
option.TestTLS = nil
reader.Apply(
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
}),
)
assert.Nil(t, option.TestTLS)

reader.GetEnv = func(s string) string { return "" }
option.TestTLS = nil
reader.Apply(
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
}),
)
assert.Nil(t, option.TestTLS)
}

func TestStringToHeader(t *testing.T) {
Expand Down
27 changes: 25 additions & 2 deletions exporters/otlp/otlpmetric/internal/oconf/envconfig.go
Expand Up @@ -16,6 +16,7 @@ package oconf // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/inte

import (
"crypto/tls"
"crypto/x509"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -53,6 +54,7 @@ func ApplyHTTPEnvConfigs(cfg Config) Config {
func getOptionsFromEnv() []GenericOption {
opts := []GenericOption{}

tlsConf := &tls.Config{}
DefaultEnvOptionsReader.Apply(
envconfig.WithURL("ENDPOINT", func(u *url.URL) {
opts = append(opts, withEndpointScheme(u))
Expand Down Expand Up @@ -81,8 +83,13 @@ func getOptionsFromEnv() []GenericOption {
return cfg
}, withEndpointForGRPC(u)))
}),
envconfig.WithTLSConfig("CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
envconfig.WithTLSConfig("METRICS_CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
envconfig.WithCertPool("CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }),
envconfig.WithCertPool("METRICS_CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }),
envconfig.WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }),
envconfig.WithClientCert("METRICS_CLIENT_CERTIFICATE", "METRICS_CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }),
envconfig.WithBool("INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }),
envconfig.WithBool("METRICS_INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }),
withTLSConfig(tlsConf, func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
envconfig.WithHeaders("HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }),
envconfig.WithHeaders("METRICS_HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }),
WithEnvCompression("COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }),
Expand Down Expand Up @@ -125,3 +132,19 @@ func withEndpointScheme(u *url.URL) GenericOption {
return WithSecure()
}
}

// revive:disable-next-line:flag-parameter
func withInsecure(b bool) GenericOption {
if b {
return WithInsecure()
}
return WithSecure()
}

func withTLSConfig(c *tls.Config, fn func(*tls.Config)) func(e *envconfig.EnvOptionsReader) {
return func(e *envconfig.EnvOptionsReader) {
if c.RootCAs != nil || len(c.Certificates) > 0 {
fn(c)
}
}
}