Skip to content

Commit

Permalink
Add F5 Cloud Exporter (1/3) (#1965)
Browse files Browse the repository at this point in the history
* added initial F5 Cloud Exporter

* updated f5_cloud_auth config reference to 'auth'

Signed-off-by: Granville Schmidt <1246157+gramidt@users.noreply.github.com>

* updated README with suggestions

Signed-off-by: Granville Schmidt <1246157+gramidt@users.noreply.github.com>
  • Loading branch information
gramidt committed Jan 19, 2021
1 parent 1d14b0a commit a8b133b
Show file tree
Hide file tree
Showing 13 changed files with 2,203 additions and 0 deletions.
1 change: 1 addition & 0 deletions exporter/f5cloudexporter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
42 changes: 42 additions & 0 deletions exporter/f5cloudexporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# F5 Cloud Exporter

Exports data via HTTP to [F5 Cloud](https://portal.cloudservices.f5.com/).

Supported pipeline types: metrics, traces, logs

> :construction: This exporter is in beta and configuration fields are subject to change.
## Getting Started

The following settings are required:

- `endpoint` (no default): The URL to send data to. See your F5 Cloud account for details.
- `source` (no default): A unique identifier that is used to distinguish where this data is coming from (e.g. dev_cluster). This is in
addition to the pipeline attributes and resources.
- `auth.credential_file` (no default): Path to the credential file used to authenticate this client. See your F5
Cloud account for details.

The following settings can be optionally configured:

- `auth.audience` (no default): Identifies the recipient that the authentication JWT is intended for. See your F5 Cloud
account for details.

- `timeout` (default = 30s): HTTP request time limit. For details see https://golang.org/pkg/net/http/#Client
- `read_buffer_size` (default = 0): ReadBufferSize for HTTP client.
- `write_buffer_size` (default = 512 * 1024): WriteBufferSize for HTTP client.

Example:

```yaml
f5cloud:
endpoint: https://<ENDPOINT_FOUND_IN_F5_CLOUD_PORTAL>
source: prod
auth:
credential_file: "/etc/creds/key.json"
```

The full list of settings exposed for this exporter are documented [here](./config.go) with detailed sample
configurations [here](./testdata/config.yaml).

This exporter also offers proxy support as documented
[here](https://github.com/open-telemetry/opentelemetry-collector/tree/master/exporter#proxy-support).
72 changes: 72 additions & 0 deletions exporter/f5cloudexporter/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package f5cloudexporter

import (
"fmt"
"net/http"

"golang.org/x/oauth2"
)

const (
SourceHeader = "X-F5-Source"
)

type f5CloudAuthRoundTripper struct {
transport http.RoundTripper
tokenSource oauth2.TokenSource
source string
}

func (rt *f5CloudAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// Clone request to ensure thread safety
req2 := req.Clone(req.Context())

// Add authorization header
tkn, err := rt.tokenSource.Token()
if err != nil {
return nil, err
}
tkn.SetAuthHeader(req2)

// Add F5 specific headers
req2.Header.Add(SourceHeader, rt.source)

resp, err := rt.transport.RoundTrip(req2)
if err != nil {
return nil, err
}

return resp, nil
}

func newF5CloudAuthRoundTripper(ts oauth2.TokenSource, source string, next http.RoundTripper) (http.RoundTripper, error) {
if ts == nil {
return nil, fmt.Errorf("no TokenSource exists")
}

if len(source) == 0 {
return nil, fmt.Errorf("no source provided")
}

rt := f5CloudAuthRoundTripper{
transport: next,
tokenSource: ts,
source: source,
}

return &rt, nil
}
154 changes: 154 additions & 0 deletions exporter/f5cloudexporter/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package f5cloudexporter

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)

type ErrorRoundTripper struct{}

func (ert *ErrorRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("error")
}

func TestF5CloudAuthRoundTripper_RoundTrip(t *testing.T) {
validTokenSource := createMockTokenSource()
source := "tests"

defaultRoundTripper := (http.RoundTripper)(http.DefaultTransport.(*http.Transport).Clone())
errorRoundTripper := &ErrorRoundTripper{}

tests := []struct {
name string
rt http.RoundTripper
token oauth2.TokenSource
shouldError bool
}{
{
name: "Test valid token source",
rt: defaultRoundTripper,
token: validTokenSource,
shouldError: false,
},
{
name: "Test invalid token source",
rt: defaultRoundTripper,
token: &InvalidTokenSource{},
shouldError: true,
},
{
name: "Test error in next round tripper",
rt: errorRoundTripper,
token: validTokenSource,
shouldError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer test_access_token", r.Header.Get("Authorization"))
assert.Equal(t, "tests", r.Header.Get(SourceHeader))
}))
defer server.Close()
rt, err := newF5CloudAuthRoundTripper(tt.token, source, tt.rt)
assert.NoError(t, err)
req, err := http.NewRequest("POST", server.URL, strings.NewReader(""))
assert.NoError(t, err)
res, err := rt.RoundTrip(req)
if tt.shouldError {
assert.Nil(t, res)
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, res.StatusCode, 200)
})
}
}

func TestCreateF5CloudAuthRoundTripperWithToken(t *testing.T) {
defaultRoundTripper := (http.RoundTripper)(http.DefaultTransport.(*http.Transport).Clone())

token := createMockTokenSource()
source := "test"

tests := []struct {
name string
token oauth2.TokenSource
source string
rt http.RoundTripper
shouldError bool
}{
{
name: "success_case",
token: token,
source: source,
rt: defaultRoundTripper,
shouldError: false,
},
{
name: "no_token_provided_error",
token: nil,
source: source,
rt: defaultRoundTripper,
shouldError: true,
},
{
name: "no_source_provided_error",
token: token,
source: "",
rt: defaultRoundTripper,
shouldError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := newF5CloudAuthRoundTripper(tt.token, tt.source, tt.rt)
if tt.shouldError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

func createMockTokenSource() oauth2.TokenSource {
tkn := &oauth2.Token{
AccessToken: "test_access_token",
TokenType: "",
RefreshToken: "",
Expiry: time.Time{},
}

return oauth2.StaticTokenSource(tkn)
}

type InvalidTokenSource struct{}

func (ts *InvalidTokenSource) Token() (*oauth2.Token, error) {
return nil, fmt.Errorf("bad TokenSource for testing")
}
73 changes: 73 additions & 0 deletions exporter/f5cloudexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package f5cloudexporter

import (
"fmt"
"net/url"
"os"

otlphttp "go.opentelemetry.io/collector/exporter/otlphttpexporter"
)

// Config defines configuration for F5 Cloud exporter.
type Config struct {
// Config represents the OTLP HTTP Exporter configuration.
otlphttp.Config `mapstructure:",squash"`

// Source represents a unique identifier that is used to distinguish where this data is coming from.
Source string `mapstructure:"source"`

// AuthConfig represents the F5 Cloud authentication configuration options.
AuthConfig AuthConfig `mapstructure:"auth"`
}

func (c *Config) sanitize() error {
if len(c.Endpoint) == 0 {
return fmt.Errorf("missing required \"endpoint\" setting")
}

endpointURL, err := url.Parse(c.Endpoint)
if err != nil {
return err
}

if len(c.Source) == 0 {
return fmt.Errorf("missing required \"source\" setting")
}

if len(c.AuthConfig.CredentialFile) == 0 {
return fmt.Errorf("missing required \"auth.credential_file\" setting")
}

if _, err := os.Stat(c.AuthConfig.CredentialFile); os.IsNotExist(err) {
return fmt.Errorf("the provided \"auth.credential_file\" does not exist")
}

if len(c.AuthConfig.Audience) == 0 {
c.AuthConfig.Audience = fmt.Sprintf("%s://%s", endpointURL.Scheme, endpointURL.Hostname())
}

return nil
}

// AuthConfig defines F5 Cloud authentication configurations for F5CloudAuthRoundTripper
type AuthConfig struct {
// CredentialFile is the F5 Cloud credentials for your designated account.
CredentialFile string `mapstructure:"credential_file"`

// Audience is the F5 Cloud audience for your designated account.
Audience string `mapstructure:"audience"`
}
Loading

0 comments on commit a8b133b

Please sign in to comment.