Skip to content

Commit

Permalink
[extension/opamp]: Support auth extensions (open-telemetry#35508)
Browse files Browse the repository at this point in the history
  • Loading branch information
BinaryFissionGames authored Oct 16, 2024
1 parent b089282 commit cf25636
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 7 deletions.
13 changes: 13 additions & 0 deletions .chloggen/feat_opampextension-support-auth.yaml
Original file line number Diff line number Diff line change
@@ -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]
2 changes: 2 additions & 0 deletions extension/opampextension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:

Expand Down
80 changes: 80 additions & 0 deletions extension/opampextension/auth.go
Original file line number Diff line number Diff line change
@@ -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
}
135 changes: 135 additions & 0 deletions extension/opampextension/auth_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 13 additions & 0 deletions extension/opampextension/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions extension/opampextension/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)

Expand All @@ -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
Expand All @@ -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
)

Expand Down
12 changes: 8 additions & 4 deletions extension/opampextension/go.sum

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

Loading

0 comments on commit cf25636

Please sign in to comment.