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

httpprovider for Collector: load configuration from config files in HTTP servers #5876

Merged
merged 12 commits into from
Aug 15, 2022
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- Remove unnecessary limitation on `pcommon.Value.Equal` that slices have only primitive values. (#5865)
- Add support to handle 404, 405 http error code as permanent errors in OTLP exporter (#5827)
- Enforce scheme name restrictions to all `confmap.Provider` implementations. (#5861)
- Add httpprovider to allow loading config files stored in HTTP (#5810)

### 🧰 Bug fixes 🧰

Expand Down
13 changes: 13 additions & 0 deletions confmap/provider/httpprovider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
What is this new component httpprovider?
- An implementation of ConfigMapProvider for HTTP (httpprovider) allows OTEL Collector the ability to load configuration for itself by fetching and reading config files stored in HTTP servers.
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved

How this new component httpprovider works?
- It will be called by ConfigMapResolver to load configurations for OTEL Collector.
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
- By giving a config URI starting with prefix 'http://', this httpprovider will be used to download config files from given HTTP URIs, and then used the downloaded config files to deploy the OTEL Collector.
- In our code, we check the validity scheme and string pattern of HTTP URIs. And also check if there are any problems on config downloading and config deserialization.

Expected URI format:
- http://...

Prerequistes:
- Need to setup a HTTP server ahead, which returns with a config files according to the given URI
84 changes: 84 additions & 0 deletions confmap/provider/httpprovider/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 httpprovider // import "go.opentelemetry.io/collector/confmap/provider/httpprovider"

import (
"context"
"fmt"
"io/ioutil"
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
"net/http"
"strings"

"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/provider/internal"
)

const (
schemeName = "http"
)

type httpClient interface {
Get(url string) (resp *http.Response, err error)
}
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved

type provider struct {
client httpClient
}

// New returns a new confmap.Provider that reads the configuration from a file.
//
// This Provider supports "http" scheme, and can be called with a "uri" that follows:
//
// One example for http-uri be like: http://localhost:3333/getConfig
//
// Examples:
// `http://localhost:3333/getConfig` - (unix, windows)
func New() confmap.Provider {
return &provider{client: &http.Client{}}
}

func (fmp *provider) Retrieve(_ context.Context, uri string, _ confmap.WatcherFunc) (*confmap.Retrieved, error) {
if !strings.HasPrefix(uri, schemeName+":") {
return nil, fmt.Errorf("%q uri is not supported by %q provider", uri, schemeName)
}

// send a HTTP GET request
resp, err := fmp.client.Get(uri)
if err != nil {
return nil, fmt.Errorf("unable to download the file via HTTP GET for uri %q, with err: %w ", uri, err)
}
defer resp.Body.Close()

// check the HTTP status code
if resp.StatusCode != 200 {
return nil, fmt.Errorf("404: resource didn't exist, fail to read the response body from uri %q", uri)
}

// read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("fail to read the response body from uri %q, with err: %w ", uri, err)
}

return internal.NewRetrievedFromYAML(body)
}

func (*provider) Scheme() string {
return schemeName
}

func (*provider) Shutdown(context.Context) error {
return nil
}
140 changes: 140 additions & 0 deletions confmap/provider/httpprovider/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright The OpenTelemetry Authors
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
//
// 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 httpprovider

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

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

"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/confmaptest"
)

// Create a provider mocking httpmapprovider works in normal cases
func NewTestProvider() confmap.Provider {
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
return &provider{client: &http.Client{}}
}

// Create a provider mocking httpmapprovider works when the returned config file is invalid
func NewTestInvalidProvider() confmap.Provider {
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
return &provider{client: &http.Client{}}
}

// Create a provider mocking httpmapprovider works when there is no corresponding config file according to the given http-uri
func NewTestNonExistProvider() confmap.Provider {
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
return &provider{client: &http.Client{}}
}

func TestFunctionalityDownloadFileHTTP(t *testing.T) {
fp := NewTestProvider()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f, err := ioutil.ReadFile("./testdata/otel-config.yaml")
if err != nil {
w.WriteHeader(404)
_, err := w.Write([]byte("Cannot find the config file"))
if err != nil {
fmt.Println("Write failed: ", err)
}
return
}
w.WriteHeader(200)
_, err = w.Write(f)
if err != nil {
fmt.Println("Write failed: ", err)
}
}))
defer ts.Close()
_, err := fp.Retrieve(context.Background(), ts.URL, nil)
assert.NoError(t, err)
assert.NoError(t, fp.Shutdown(context.Background()))
}

func TestUnsupportedScheme(t *testing.T) {
fp := NewTestProvider()
_, err := fp.Retrieve(context.Background(), "https://...", nil)
assert.Error(t, err)
assert.NoError(t, fp.Shutdown(context.Background()))
}

func TestEmptyURI(t *testing.T) {
fp := NewTestProvider()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return 200 if this is "empty"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Bogdan, I am still a little bit confused. I think it should return 400 instead? The case is the HTTP URI is empty, not the content of config file is empty?

_, err := w.Write([]byte(""))
if err != nil {
fmt.Println("Write failed: ", err)
}
}))
defer ts.Close()
_, err := fp.Retrieve(context.Background(), ts.URL, nil)
require.Error(t, err)
require.NoError(t, fp.Shutdown(context.Background()))
}

func TestNonExistent(t *testing.T) {
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved
fp := NewTestNonExistProvider()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f, err := ioutil.ReadFile("./testdata/nonexist-otel-config.yaml")
if err != nil {
w.WriteHeader(404)
_, err := w.Write([]byte("Cannot find the config file"))
if err != nil {
fmt.Println("Write failed: ", err)
}
return
}
w.WriteHeader(200)
_, err = w.Write(f)
if err != nil {
fmt.Println("Write failed: ", err)
}
}))
defer ts.Close()
_, err := fp.Retrieve(context.Background(), ts.URL, nil)
assert.Error(t, err)
require.NoError(t, fp.Shutdown(context.Background()))
}

func TestInvalidYAML(t *testing.T) {
fp := NewTestInvalidProvider()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, err := w.Write([]byte("wrong : ["))
if err != nil {
fmt.Println("Write failed: ", err)
}
}))
defer ts.Close()
_, err := fp.Retrieve(context.Background(), ts.URL, nil)
assert.Error(t, err)
require.NoError(t, fp.Shutdown(context.Background()))
}

func TestScheme(t *testing.T) {
fp := NewTestProvider()
assert.Equal(t, "http", fp.Scheme())
require.NoError(t, fp.Shutdown(context.Background()))
}

func TestValidateProviderScheme(t *testing.T) {
assert.NoError(t, confmaptest.ValidateProviderScheme(New()))
}
37 changes: 37 additions & 0 deletions confmap/provider/httpprovider/testdata/otel-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
extensions:
memory_ballast:
size_mib: 512
zpages:
endpoint: 0.0.0.0:55679

receivers:
otlp:
protocols:
grpc:
http:

processors:
batch:
memory_limiter:
# 75% of maximum memory up to 4G
limit_mib: 1536
# 25% of limit up to 2G
spike_limit_mib: 512
check_interval: 5s

exporters:
logging:
loglevel: debug

service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [logging]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [logging]

extensions: [memory_ballast, zpages]
3 changes: 2 additions & 1 deletion service/config_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"go.opentelemetry.io/collector/confmap/converter/expandconverter"
"go.opentelemetry.io/collector/confmap/provider/envprovider"
"go.opentelemetry.io/collector/confmap/provider/fileprovider"
"go.opentelemetry.io/collector/confmap/provider/httpprovider"
"go.opentelemetry.io/collector/confmap/provider/yamlprovider"
"go.opentelemetry.io/collector/service/internal/configunmarshaler"
)
Expand Down Expand Up @@ -84,7 +85,7 @@ func newDefaultConfigProviderSettings(uris []string) ConfigProviderSettings {
return ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: uris,
Providers: makeMapProvidersMap(fileprovider.New(), envprovider.New(), yamlprovider.New()),
Providers: makeMapProvidersMap(fileprovider.New(), envprovider.New(), yamlprovider.New(), httpprovider.New()),
Converters: []confmap.Converter{expandconverter.New()},
},
}
Expand Down