From cf25636420ffca21088bc0fec0051d56b3f940c6 Mon Sep 17 00:00:00 2001 From: Brandon Johnson Date: Wed, 16 Oct 2024 14:33:39 -0400 Subject: [PATCH] [extension/opamp]: Support auth extensions (#35508) --- .../feat_opampextension-support-auth.yaml | 13 ++ extension/opampextension/README.md | 2 + extension/opampextension/auth.go | 80 +++++++++++ extension/opampextension/auth_test.go | 135 ++++++++++++++++++ extension/opampextension/config.go | 13 ++ extension/opampextension/go.mod | 8 +- extension/opampextension/go.sum | 12 +- extension/opampextension/opamp_agent.go | 24 ++++ extension/opampextension/opamp_agent_test.go | 37 +++++ 9 files changed, 317 insertions(+), 7 deletions(-) create mode 100644 .chloggen/feat_opampextension-support-auth.yaml create mode 100644 extension/opampextension/auth.go create mode 100644 extension/opampextension/auth_test.go diff --git a/.chloggen/feat_opampextension-support-auth.yaml b/.chloggen/feat_opampextension-support-auth.yaml new file mode 100644 index 000000000000..75b4f9cec381 --- /dev/null +++ b/.chloggen/feat_opampextension-support-auth.yaml @@ -0,0 +1,13 @@ +# Use this changelog template to create an entry for release notes. + +# 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. filelogreceiver) +component: opampextension + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Support using auth extensions for authenticating with opamp servers" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [35507] diff --git a/extension/opampextension/README.md b/extension/opampextension/README.md index b12f1869d949..59d7284fc643 100644 --- a/extension/opampextension/README.md +++ b/extension/opampextension/README.md @@ -25,6 +25,7 @@ The following settings are optional for the websocket client: - `ws`: The OpAMP websocket transport settings. - `tls`: TLS settings. - `headers`: HTTP headers to set. + - `auth`: The ID of an auth extension to use for authentication. The following settings are optional for the HTTP client: @@ -33,6 +34,7 @@ The following settings are optional for the HTTP client: - `tls`: TLS settings. - `headers`: HTTP headers to set. - `polling_interval`: The interval at which the extension will poll the server. Defaults to 30s. + - `auth`: The ID of an auth extension to use for authentication. The following settings are optional for both transports: diff --git a/extension/opampextension/auth.go b/extension/opampextension/auth.go new file mode 100644 index 000000000000..0477a718da7e --- /dev/null +++ b/extension/opampextension/auth.go @@ -0,0 +1,80 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package opampextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/opampextension" + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension/auth" + "go.uber.org/zap" +) + +// headerCaptureRoundTripper is a RoundTripper that captures the headers of the request +// that passes through it. +type headerCaptureRoundTripper struct { + lastHeader http.Header +} + +func (h *headerCaptureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + h.lastHeader = req.Header.Clone() + // Dummy response is recorded here + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Body: io.NopCloser(&bytes.Buffer{}), + Request: req, + }, nil +} + +func makeHeadersFunc(logger *zap.Logger, serverCfg *OpAMPServer, host component.Host) (func(http.Header) http.Header, error) { + var emptyComponentID component.ID + if serverCfg == nil || serverCfg.GetAuthExtensionID() == emptyComponentID { + return nil, nil + } + + extID := serverCfg.GetAuthExtensionID() + ext, ok := host.GetExtensions()[extID] + if !ok { + return nil, fmt.Errorf("could not find auth extension %q", extID) + } + + authExt, ok := ext.(auth.Client) + if !ok { + return nil, fmt.Errorf("auth extension %q is not an auth.Client", extID) + } + + hcrt := &headerCaptureRoundTripper{} + rt, err := authExt.RoundTripper(hcrt) + if err != nil { + return nil, fmt.Errorf("could not create roundtripper for authentication: %w", err) + } + + return func(h http.Header) http.Header { + // This is a workaround while websocket authentication is being worked on. + // Currently, we are waiting on the auth module to be stabilized. + // See for more info: https://github.com/open-telemetry/opentelemetry-collector/issues/10864 + dummyReq, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + logger.Error("Failed to create dummy request for authentication.", zap.Error(err)) + return h + } + + dummyReq.Header = h + + _, err = rt.RoundTrip(dummyReq) + if err != nil { + logger.Error("Error while performing round-trip for authentication.", zap.Error(err)) + return h + } + + return hcrt.lastHeader + }, nil +} diff --git a/extension/opampextension/auth_test.go b/extension/opampextension/auth_test.go new file mode 100644 index 000000000000..15f2c64bb9da --- /dev/null +++ b/extension/opampextension/auth_test.go @@ -0,0 +1,135 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package opampextension + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.uber.org/zap" + "google.golang.org/grpc/credentials" +) + +func TestMakeHeadersFunc(t *testing.T) { + t.Run("Nil server config", func(t *testing.T) { + headersFunc, err := makeHeadersFunc(zap.NewNop(), nil, nil) + require.NoError(t, err) + require.Nil(t, headersFunc) + }) + + t.Run("No auth extension specified", func(t *testing.T) { + headersFunc, err := makeHeadersFunc(zap.NewNop(), &OpAMPServer{ + WS: &commonFields{}, + }, nil) + require.NoError(t, err) + require.Nil(t, headersFunc) + }) + + t.Run("Extension does not exist", func(t *testing.T) { + nopHost := componenttest.NewNopHost() + headersFunc, err := makeHeadersFunc(zap.NewNop(), &OpAMPServer{ + WS: &commonFields{ + Auth: component.NewID(component.MustNewType("bearerauth")), + }, + }, nopHost) + require.EqualError(t, err, `could not find auth extension "bearerauth"`) + require.Nil(t, headersFunc) + }) + + t.Run("Extension is not an auth extension", func(t *testing.T) { + authComponent := component.NewID(component.MustNewType("bearerauth")) + host := &mockHost{ + extensions: map[component.ID]component.Component{ + authComponent: mockComponent{}, + }, + } + headersFunc, err := makeHeadersFunc(zap.NewNop(), &OpAMPServer{ + WS: &commonFields{ + Auth: authComponent, + }, + }, host) + + require.EqualError(t, err, `auth extension "bearerauth" is not an auth.Client`) + require.Nil(t, headersFunc) + }) + + t.Run("Headers func extracts headers from extension", func(t *testing.T) { + authComponent := component.NewID(component.MustNewType("bearerauth")) + h := http.Header{} + h.Set("Authorization", "Bearer user:pass") + + host := &mockHost{ + extensions: map[component.ID]component.Component{ + authComponent: mockAuthClient{ + header: h, + }, + }, + } + headersFunc, err := makeHeadersFunc(zap.NewNop(), &OpAMPServer{ + WS: &commonFields{ + Auth: authComponent, + }, + }, host) + + require.NoError(t, err) + headersOut := headersFunc(http.Header{ + "OtherHeader": []string{"OtherValue"}, + }) + + require.Equal(t, http.Header{ + "OtherHeader": []string{"OtherValue"}, + "Authorization": []string{"Bearer user:pass"}, + }, headersOut) + }) +} + +type mockHost struct { + extensions map[component.ID]component.Component +} + +func (m mockHost) GetExtensions() map[component.ID]component.Component { + return m.extensions +} + +type mockComponent struct{} + +func (mockComponent) Start(_ context.Context, _ component.Host) error { return nil } +func (mockComponent) Shutdown(_ context.Context) error { return nil } + +type mockAuthClient struct { + header http.Header +} + +func (mockAuthClient) Start(_ context.Context, _ component.Host) error { return nil } +func (mockAuthClient) Shutdown(_ context.Context) error { return nil } +func (m mockAuthClient) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) { + return mockRoundTripper{ + header: m.header, + base: base, + }, nil +} +func (mockAuthClient) PerRPCCredentials() (credentials.PerRPCCredentials, error) { + return nil, nil +} + +type mockRoundTripper struct { + header http.Header + base http.RoundTripper +} + +func (m mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + reqClone := req.Clone(req.Context()) + + for k, vals := range m.header { + for _, val := range vals { + reqClone.Header.Add(k, val) + } + } + + return m.base.RoundTrip(reqClone) +} diff --git a/extension/opampextension/config.go b/extension/opampextension/config.go index 202bfdbb6d8a..a06adee4fab8 100644 --- a/extension/opampextension/config.go +++ b/extension/opampextension/config.go @@ -10,6 +10,7 @@ import ( "github.com/open-telemetry/opamp-go/client" "github.com/open-telemetry/opamp-go/protobufs" + "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/config/configtls" "go.uber.org/zap" @@ -70,6 +71,7 @@ type commonFields struct { Endpoint string `mapstructure:"endpoint"` TLSSetting configtls.ClientConfig `mapstructure:"tls,omitempty"` Headers map[string]configopaque.String `mapstructure:"headers,omitempty"` + Auth component.ID `mapstructure:"auth,omitempty"` } func (c *commonFields) Scheme() string { @@ -148,6 +150,17 @@ func (s OpAMPServer) GetEndpoint() string { return "" } +func (s OpAMPServer) GetAuthExtensionID() component.ID { + if s.WS != nil { + return s.WS.Auth + } else if s.HTTP != nil { + return s.HTTP.Auth + } + + var emptyComponentID component.ID + return emptyComponentID +} + func (s OpAMPServer) GetPollingInterval() time.Duration { if s.HTTP != nil && s.HTTP.PollingInterval > 0 { return s.HTTP.PollingInterval diff --git a/extension/opampextension/go.mod b/extension/opampextension/go.mod index 99ee8a4a67d3..4bc064b766df 100644 --- a/extension/opampextension/go.mod +++ b/extension/opampextension/go.mod @@ -5,7 +5,7 @@ go 1.22.0 require ( github.com/google/uuid v1.6.0 github.com/oklog/ulid/v2 v2.1.0 - github.com/open-telemetry/opamp-go v0.15.0 + github.com/open-telemetry/opamp-go v0.17.0 github.com/open-telemetry/opentelemetry-collector-contrib/extension/opampcustommessages v0.111.0 github.com/shirou/gopsutil/v4 v4.24.9 github.com/stretchr/testify v1.9.0 @@ -15,10 +15,13 @@ require ( go.opentelemetry.io/collector/config/configtls v1.17.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/confmap v1.17.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/extension v0.111.1-0.20241008154146-ea48c09c31ae + go.opentelemetry.io/collector/extension/auth v0.111.1-0.20241008154146-ea48c09c31ae + go.opentelemetry.io/collector/extension/extensioncapabilities v0.111.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/semconv v0.111.1-0.20241008154146-ea48c09c31ae go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + google.golang.org/grpc v1.67.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -32,7 +35,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/providers/confmap v0.1.0 // indirect github.com/knadh/koanf/v2 v2.1.1 // indirect @@ -59,7 +62,6 @@ require ( golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/extension/opampextension/go.sum b/extension/opampextension/go.sum index aed476a3e376..13c87b480fa0 100644 --- a/extension/opampextension/go.sum +++ b/extension/opampextension/go.sum @@ -22,8 +22,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= @@ -44,8 +44,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/open-telemetry/opamp-go v0.15.0 h1:X2TWhEsGQ8GP7Uos3Ic9v/1aFUqoECZXKS7xAF5HqsA= -github.com/open-telemetry/opamp-go v0.15.0/go.mod h1:QyPeN56JXlcZt5yG5RMdZ50Ju+zMFs1Ihy/hwHyF8Oo= +github.com/open-telemetry/opamp-go v0.17.0 h1:3R4+B/6Sy8mknLBbzO3gqloqwTT02rCSRcr4ac2B124= +github.com/open-telemetry/opamp-go v0.17.0/go.mod h1:SGDhUoAx7uGutO4ENNMQla/tiSujxgZmMPJXIOPGBdk= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -79,6 +79,10 @@ go.opentelemetry.io/collector/confmap v1.17.1-0.20241008154146-ea48c09c31ae h1:m go.opentelemetry.io/collector/confmap v1.17.1-0.20241008154146-ea48c09c31ae/go.mod h1:GrIZ12P/9DPOuTpe2PIS51a0P/ZM6iKtByVee1Uf3+k= go.opentelemetry.io/collector/extension v0.111.1-0.20241008154146-ea48c09c31ae h1:akinGdBSY01CI/Jme2QKbYnwqRgLLWKx9U+93KLqZI0= go.opentelemetry.io/collector/extension v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:nX8HLUqkaWKJ4gCpPdba/x4+G3emnnjzdmpF6LgtZzg= +go.opentelemetry.io/collector/extension/auth v0.111.1-0.20241008154146-ea48c09c31ae h1:+fhQtSMttCey4XozohoN3BF+I5s+UVyAFIEO16uWLgw= +go.opentelemetry.io/collector/extension/auth v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:67BknwUsRrFcpJln0gyrWbmR0kZbUy198Ke65CyeJx4= +go.opentelemetry.io/collector/extension/extensioncapabilities v0.111.1-0.20241008154146-ea48c09c31ae h1:0KiSFDm2VxzQJ6vz1Pec/eTMGPZF4moOcM2+Ku0UZmA= +go.opentelemetry.io/collector/extension/extensioncapabilities v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:Ha8rgNgcbPTlJ+Ld5285qtOqJPP71l7nBfnMVt2uYBE= go.opentelemetry.io/collector/internal/globalsignal v0.111.1-0.20241008154146-ea48c09c31ae h1:fublc0EO06p79/OWw2jWVPSPNBMiBcB+0QpLes587DU= go.opentelemetry.io/collector/internal/globalsignal v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:GqMXodPWOxK5uqpX8MaMXC2389y2XJTa5nPwf8FYDK8= go.opentelemetry.io/collector/pdata v1.17.1-0.20241008154146-ea48c09c31ae h1:PcwZe1RD8tC4SZExhf0f5HqK+ZuWGsowHaBBU4PiUv0= diff --git a/extension/opampextension/opamp_agent.go b/extension/opampextension/opamp_agent.go index c481c2cc396e..db1ef789e738 100644 --- a/extension/opampextension/opamp_agent.go +++ b/extension/opampextension/opamp_agent.go @@ -23,6 +23,7 @@ import ( "go.opentelemetry.io/collector/component/componentstatus" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/extension/extensioncapabilities" semconv "go.opentelemetry.io/collector/semconv/v1.27.0" "go.uber.org/zap" "golang.org/x/exp/maps" @@ -59,6 +60,8 @@ type opampAgent struct { } var _ opampcustommessages.CustomCapabilityRegistry = (*opampAgent)(nil) +var _ extensioncapabilities.Dependent = (*opampAgent)(nil) +var _ extensioncapabilities.ConfigWatcher = (*opampAgent)(nil) func (o *opampAgent) Start(ctx context.Context, host component.Host) error { o.reportFunc = func(event *componentstatus.Event) { @@ -81,8 +84,14 @@ func (o *opampAgent) Start(ctx context.Context, host component.Host) error { go monitorPPID(o.lifetimeCtx, o.cfg.PPIDPollInterval, o.cfg.PPID, o.reportFunc) } + headerFunc, err := makeHeadersFunc(o.logger, o.cfg.Server, host) + if err != nil { + return err + } + settings := types.StartSettings{ Header: header, + HeaderFunc: headerFunc, TLSConfig: tls, OpAMPServerURL: o.cfg.Server.GetEndpoint(), InstanceUid: types.InstanceUid(o.instanceID), @@ -142,6 +151,21 @@ func (o *opampAgent) Shutdown(ctx context.Context) error { return err } +// Dependencies implements extensioncapabilities.Dependent +func (o *opampAgent) Dependencies() []component.ID { + if o.cfg.Server == nil { + return nil + } + + var emptyComponentID component.ID + authID := o.cfg.Server.GetAuthExtensionID() + if authID == emptyComponentID { + return nil + } + + return []component.ID{authID} +} + func (o *opampAgent) NotifyConfig(ctx context.Context, conf *confmap.Conf) error { if o.capabilities.ReportsEffectiveConfig { o.updateEffectiveConfig(conf) diff --git a/extension/opampextension/opamp_agent_test.go b/extension/opampextension/opamp_agent_test.go index e500b6c800ec..e2013d1d45eb 100644 --- a/extension/opampextension/opamp_agent_test.go +++ b/extension/opampextension/opamp_agent_test.go @@ -240,3 +240,40 @@ func TestParseInstanceIDString(t *testing.T) { }) } } + +func TestOpAMPAgent_Dependencies(t *testing.T) { + t.Run("No server specified", func(t *testing.T) { + o := opampAgent{ + cfg: &Config{}, + } + + require.Nil(t, o.Dependencies()) + }) + + t.Run("No auth extension specified", func(t *testing.T) { + o := opampAgent{ + cfg: &Config{ + Server: &OpAMPServer{ + WS: &commonFields{}, + }, + }, + } + + require.Nil(t, o.Dependencies()) + }) + + t.Run("auth extension specified", func(t *testing.T) { + authID := component.MustNewID("basicauth") + o := opampAgent{ + cfg: &Config{ + Server: &OpAMPServer{ + WS: &commonFields{ + Auth: authID, + }, + }, + }, + } + + require.Equal(t, []component.ID{authID}, o.Dependencies()) + }) +}