diff --git a/CHANGELOG.md b/CHANGELOG.md index c57588c059a4..2fd119a09ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ ### πŸ’‘ Enhancements πŸ’‘ +- Move `service.mapResolver` to `confmap.Resolver` (#5444) + ### 🧰 Bug fixes 🧰 ## v0.52.0 Beta diff --git a/confmap/README.md b/confmap/README.md index 6a40ed10a873..f8a9a6cef0b0 100644 --- a/confmap/README.md +++ b/confmap/README.md @@ -1,11 +1,11 @@ # High Level Design This document is work in progress, some concepts are not yet available -(e.g. MapResolver is a private concept in the service for the moment). +(e.g. Resolver is a private concept in the service for the moment). -## ConfMap +## Conf -The [ConfMap](confmap.go) represents the raw configuration for a service (e.g. OpenTelemetry Collector). +The [Conf](confmap.go) represents the raw configuration for a service (e.g. OpenTelemetry Collector). ## Provider @@ -21,21 +21,21 @@ characters long to avoid conflicting with a driver-letter identifier as specifie The [Converter](converter.go) allows implementing conversion logic for the provided configuration. One of the most common use-case is to migrate/transform the configuration after a backwards incompatible change. -## MapResolver +## Resolver -The `MapResolver` handles the use of multiple [Providers](#provider) and [Converters](#converter) +The `Resolver` handles the use of multiple [Providers](#provider) and [Converters](#converter) simplifying configuration parsing, monitoring for updates, and the overall life-cycle of the used config providers. -The `MapResolver` provides two main functionalities: [Configuration Resolving](#configuration-resolving) and +The `Resolver` provides two main functionalities: [Configuration Resolving](#configuration-resolving) and [Watching for Updates](#watching-for-updates). ### Configuration Resolving -The `MapResolver` receives as input a set of `Providers`, a list of `Converters`, and a list of configuration identifier -`configURI` that will be used to generate the resulting, or effective, configuration in the form of a `config.Map`, +The `Resolver` receives as input a set of `Providers`, a list of `Converters`, and a list of configuration identifier +`configURI` that will be used to generate the resulting, or effective, configuration in the form of a `Conf`, that can be used by code that is oblivious to the usage of `Providers` and `Converters`. ```terminal - MapResolver Provider + Resolver Provider β”‚ β”‚ Resolve β”‚ β”‚ ────────────────►│ β”‚ @@ -63,17 +63,17 @@ that can be used by code that is oblivious to the usage of `Providers` and `Conv The `Resolve` method proceeds in the following steps: -1. Start with an empty "result" of `config.Map` type. +1. Start with an empty "result" of `Conf` type. 2. For each config URI retrieves individual configurations, and merges it into the "result". 2. For each "Converter", call "Convert" for the "result". 4. Return the "result", aka effective, configuration. ### Watching for Updates -After the configuration was processed, the `MapResolver` can be used as a single point to watch for updates in the +After the configuration was processed, the `Resolver` can be used as a single point to watch for updates in the configuration retrieved via the `Provider` used to retrieve the β€œinitial” configuration and to generate the β€œeffective” one. ```terminal - MapResolver Provider + Resolver Provider β”‚ β”‚ Watch β”‚ β”‚ ───────────►│ β”‚ @@ -86,4 +86,4 @@ configuration retrieved via the `Provider` used to retrieve the β€œinitial” co ◄──────────── β”‚ ``` -The `MapResolver` does that by passing an `onChange` func to each `Provider.Retrieve` call and capturing all watch events. +The `Resolver` does that by passing an `onChange` func to each `Provider.Retrieve` call and capturing all watch events. diff --git a/confmap/confmap_test.go b/confmap/confmap_test.go index 74ce5d107042..21c9506aee36 100644 --- a/confmap/confmap_test.go +++ b/confmap/confmap_test.go @@ -16,7 +16,6 @@ package confmap import ( "errors" - "fmt" "io/ioutil" "path/filepath" "strings" @@ -104,28 +103,12 @@ func TestToStringMap(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - parser, err := newMapFromFile(test.fileName) - require.NoError(t, err) - assert.Equal(t, test.stringMap, parser.ToStringMap()) + conf := newConfFromFile(t, test.fileName) + assert.Equal(t, test.stringMap, conf.ToStringMap()) }) } } -// newMapFromFile creates a new confmap.Conf by reading the given file. -func newMapFromFile(fileName string) (*Conf, error) { - content, err := ioutil.ReadFile(filepath.Clean(fileName)) - if err != nil { - return nil, fmt.Errorf("unable to read the file %v: %w", fileName, err) - } - - var data map[string]interface{} - if err = yaml.Unmarshal(content, &data); err != nil { - return nil, fmt.Errorf("unable to parse yaml: %w", err) - } - - return NewFromStringMap(data), nil -} - func TestExpandNilStructPointersHookFunc(t *testing.T) { stringMap := map[string]interface{}{ "boolean": nil, @@ -236,3 +219,14 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T) cfg := &TestIDConfig{} assert.Error(t, conf.UnmarshalExact(cfg)) } + +// newConfFromFile creates a new Conf by reading the given file. +func newConfFromFile(t *testing.T, fileName string) *Conf { + content, err := ioutil.ReadFile(filepath.Clean(fileName)) + require.NoErrorf(t, err, "unable to read the file %v", fileName) + + var data map[string]interface{} + require.NoError(t, yaml.Unmarshal(content, &data), "unable to parse yaml") + + return NewFromStringMap(data) +} diff --git a/service/mapresolver.go b/confmap/resolver.go similarity index 66% rename from service/mapresolver.go rename to confmap/resolver.go index 473691d33c27..119096d5b4f5 100644 --- a/service/mapresolver.go +++ b/confmap/resolver.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package service // import "go.opentelemetry.io/collector/service" +package confmap // import "go.opentelemetry.io/collector/service" import ( "context" @@ -23,65 +23,68 @@ import ( "sync" "go.uber.org/multierr" - - "go.opentelemetry.io/collector/config/experimental/configsource" - "go.opentelemetry.io/collector/confmap" ) // follows drive-letter specification: // https://tools.ietf.org/id/draft-kerwin-file-scheme-07.html#syntax var driverLetterRegexp = regexp.MustCompile("^[A-z]:") -// mapResolver resolves a configuration as a confmap.Conf. -type mapResolver struct { +// Resolver resolves a configuration as a Conf. +type Resolver struct { uris []string - providers map[string]confmap.Provider - converters []confmap.Converter + providers map[string]Provider + converters []Converter sync.Mutex - closers []confmap.CloseFunc + closers []CloseFunc watcher chan error } -// newMapResolver returns a new mapResolver that resolves configuration from multiple URIs. +type ResolverSettings struct { + URIs []string + Providers map[string]Provider + Converters []Converter +} + +// NewResolver returns a new Resolver that resolves configuration from multiple URIs. // // To resolve a configuration the following steps will happen: // 1. Retrieves individual configurations from all given "URIs", and merge them in the retrieve order. -// 2. Once the confmap.Conf is merged, apply the converters in the given order. +// 2. Once the Conf is merged, apply the converters in the given order. // -// After the configuration was resolved the `mapResolver` can be used as a single point to watch for updates in +// After the configuration was resolved the `Resolver` can be used as a single point to watch for updates in // the configuration data retrieved via the config providers used to process the "initial" configuration and to generate // the "effective" one. The typical usage is the following: // -// mapResolver.Resolve(ctx) -// mapResolver.Watch() // wait for an event. -// mapResolver.Resolve(ctx) -// mapResolver.Watch() // wait for an event. +// Resolver.Resolve(ctx) +// Resolver.Watch() // wait for an event. +// Resolver.Resolve(ctx) +// Resolver.Watch() // wait for an event. // // repeat Resolve/Watch cycle until it is time to shut down the Collector process. -// mapResolver.Shutdown(ctx) +// Resolver.Shutdown(ctx) // // `uri` must follow the ":" format. This format is compatible with the URI definition // (see https://datatracker.ietf.org/doc/html/rfc3986). An empty "" defaults to "file" schema. -func newMapResolver(uris []string, providers map[string]confmap.Provider, converters []confmap.Converter) (*mapResolver, error) { - if len(uris) == 0 { +func NewResolver(set ResolverSettings) (*Resolver, error) { + if len(set.URIs) == 0 { return nil, errors.New("invalid map resolver config: no URIs") } - if len(providers) == 0 { - return nil, errors.New("invalid map resolver config: no map providers") + if len(set.Providers) == 0 { + return nil, errors.New("invalid map resolver config: no providers") } // Safe copy, ensures the slices and maps cannot be changed from the caller. - urisCopy := make([]string, len(uris)) - copy(urisCopy, uris) - providersCopy := make(map[string]confmap.Provider, len(providers)) - for k, v := range providers { + urisCopy := make([]string, len(set.URIs)) + copy(urisCopy, set.URIs) + providersCopy := make(map[string]Provider, len(set.Providers)) + for k, v := range set.Providers { providersCopy[k] = v } - convertersCopy := make([]confmap.Converter, len(converters)) - copy(convertersCopy, converters) + convertersCopy := make([]Converter, len(set.Converters)) + copy(convertersCopy, set.Converters) - return &mapResolver{ + return &Resolver{ uris: urisCopy, providers: providersCopy, converters: convertersCopy, @@ -89,17 +92,17 @@ func newMapResolver(uris []string, providers map[string]confmap.Provider, conver }, nil } -// Resolve returns the configuration as a confmap.Conf, or error otherwise. +// Resolve returns the configuration as a Conf, or error otherwise. // // Should never be called concurrently with itself, Watch or Shutdown. -func (mr *mapResolver) Resolve(ctx context.Context) (*confmap.Conf, error) { +func (mr *Resolver) Resolve(ctx context.Context) (*Conf, error) { // First check if already an active watching, close that if any. if err := mr.closeIfNeeded(ctx); err != nil { return nil, fmt.Errorf("cannot close previous watch: %w", err) } // Retrieves individual configurations from all URIs in the given order, and merge them in retMap. - retMap := confmap.New() + retMap := New() for _, uri := range mr.uris { // For backwards compatibility: // - empty url scheme means "file". @@ -145,7 +148,7 @@ func (mr *mapResolver) Resolve(ctx context.Context) (*confmap.Conf, error) { // error indicates that there was a problem with watching the configuration changes. // // Should never be called concurrently with itself or Get. -func (mr *mapResolver) Watch() <-chan error { +func (mr *Resolver) Watch() <-chan error { return mr.watcher } @@ -153,7 +156,7 @@ func (mr *mapResolver) Watch() <-chan error { // and release any resources that it may have created. It terminates the Watch channel. // // Should never be called concurrently with itself or Get. -func (mr *mapResolver) Shutdown(ctx context.Context) error { +func (mr *Resolver) Shutdown(ctx context.Context) error { close(mr.watcher) var errs error @@ -165,25 +168,14 @@ func (mr *mapResolver) Shutdown(ctx context.Context) error { return errs } -func (mr *mapResolver) onChange(event *confmap.ChangeEvent) { - // TODO: Remove check for configsource.ErrSessionClosed when providers updated to not call onChange when closed. - if !errors.Is(event.Error, configsource.ErrSessionClosed) { - mr.watcher <- event.Error - } +func (mr *Resolver) onChange(event *ChangeEvent) { + mr.watcher <- event.Error } -func (mr *mapResolver) closeIfNeeded(ctx context.Context) error { +func (mr *Resolver) closeIfNeeded(ctx context.Context) error { var err error for _, ret := range mr.closers { err = multierr.Append(err, ret(ctx)) } return err } - -func makeMapProvidersMap(providers ...confmap.Provider) map[string]confmap.Provider { - ret := make(map[string]confmap.Provider, len(providers)) - for _, provider := range providers { - ret[provider.Scheme()] = provider - } - return ret -} diff --git a/service/mapresolver_test.go b/confmap/resolver_test.go similarity index 52% rename from service/mapresolver_test.go rename to confmap/resolver_test.go index 9b7f7aef8d56..714f0c85ad9c 100644 --- a/service/mapresolver_test.go +++ b/confmap/resolver_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package service +package confmap import ( "context" @@ -23,35 +23,30 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "go.opentelemetry.io/collector/config/experimental/configsource" - "go.opentelemetry.io/collector/confmap" - "go.opentelemetry.io/collector/confmap/confmaptest" - "go.opentelemetry.io/collector/confmap/provider/fileprovider" ) type mockProvider struct { scheme string - retM *confmap.Conf + retC *Conf errR error errS error errW error errC error } -func (m *mockProvider) Retrieve(_ context.Context, _ string, watcher confmap.WatcherFunc) (confmap.Retrieved, error) { +func (m *mockProvider) Retrieve(_ context.Context, _ string, watcher WatcherFunc) (Retrieved, error) { if m.errR != nil { - return confmap.Retrieved{}, m.errR + return Retrieved{}, m.errR } - if m.retM == nil { - return confmap.NewRetrievedFromMap(confmap.New()), nil + if m.retC == nil { + return NewRetrievedFromMap(New()), nil } if watcher != nil { - watcher(&confmap.ChangeEvent{Error: m.errW}) + watcher(&ChangeEvent{Error: m.errW}) } - return confmap.NewRetrievedFromMap( - m.retM, - confmap.WithRetrievedClose(func(ctx context.Context) error { return m.errC })), nil + return NewRetrievedFromMap( + m.retC, + WithRetrievedClose(func(ctx context.Context) error { return m.errC })), nil } func (m *mockProvider) Scheme() string { @@ -65,20 +60,34 @@ func (m *mockProvider) Shutdown(context.Context) error { return m.errS } +type errFileProvider struct{} + +func (m *errFileProvider) Retrieve(_ context.Context, uri string, watcher WatcherFunc) (Retrieved, error) { + return Retrieved{}, errors.New(uri) +} + +func (m *errFileProvider) Scheme() string { + return "file" +} + +func (m *errFileProvider) Shutdown(context.Context) error { + return nil +} + type mockConverter struct { err error } -func (m *mockConverter) Convert(context.Context, *confmap.Conf) error { +func (m *mockConverter) Convert(context.Context, *Conf) error { return errors.New("converter_err") } -func TestMapResolver_Errors(t *testing.T) { +func TestResolverErrors(t *testing.T) { tests := []struct { name string locations []string - providers []confmap.Provider - converters []confmap.Converter + providers []Provider + converters []Converter expectResolveErr bool expectWatchErr bool expectCloseErr bool @@ -87,13 +96,13 @@ func TestMapResolver_Errors(t *testing.T) { { name: "unsupported location scheme", locations: []string{"mock:", "not_supported:"}, - providers: []confmap.Provider{&mockProvider{}}, + providers: []Provider{&mockProvider{}}, expectResolveErr: true, }, { name: "retrieve location config error", locations: []string{"mock:", "err:"}, - providers: []confmap.Provider{ + providers: []Provider{ &mockProvider{}, &mockProvider{scheme: "err", errR: errors.New("retrieve_err")}, }, @@ -101,30 +110,28 @@ func TestMapResolver_Errors(t *testing.T) { }, { name: "converter error", - locations: []string{"mock:", filepath.Join("testdata", "otelcol-nop.yaml")}, - providers: []confmap.Provider{&mockProvider{}, fileprovider.New()}, - converters: []confmap.Converter{&mockConverter{err: errors.New("converter_err")}}, + locations: []string{"mock:"}, + providers: []Provider{&mockProvider{}}, + converters: []Converter{&mockConverter{err: errors.New("converter_err")}}, expectResolveErr: true, }, { name: "watch error", locations: []string{"mock:", "err:"}, - providers: func() []confmap.Provider { - conf, err := confmaptest.LoadConf(filepath.Join("testdata", "otelcol-nop.yaml")) - require.NoError(t, err) - return []confmap.Provider{&mockProvider{}, &mockProvider{scheme: "err", retM: conf, errW: errors.New("watch_err")}} + providers: func() []Provider { + conf := newConfFromFile(t, filepath.Join("testdata", "config.yaml")) + return []Provider{&mockProvider{}, &mockProvider{scheme: "err", retC: conf, errW: errors.New("watch_err")}} }(), expectWatchErr: true, }, { name: "close error", locations: []string{"mock:", "err:"}, - providers: func() []confmap.Provider { - conf, err := confmaptest.LoadConf(filepath.Join("testdata", "otelcol-nop.yaml")) - require.NoError(t, err) - return []confmap.Provider{ + providers: func() []Provider { + conf := newConfFromFile(t, filepath.Join("testdata", "config.yaml")) + return []Provider{ &mockProvider{}, - &mockProvider{scheme: "err", retM: conf, errC: errors.New("close_err")}, + &mockProvider{scheme: "err", retC: conf, errC: errors.New("close_err")}, } }(), expectCloseErr: true, @@ -132,12 +139,11 @@ func TestMapResolver_Errors(t *testing.T) { { name: "shutdown error", locations: []string{"mock:", "err:"}, - providers: func() []confmap.Provider { - conf, err := confmaptest.LoadConf(filepath.Join("testdata", "otelcol-nop.yaml")) - require.NoError(t, err) - return []confmap.Provider{ + providers: func() []Provider { + conf := newConfFromFile(t, filepath.Join("testdata", "config.yaml")) + return []Provider{ &mockProvider{}, - &mockProvider{scheme: "err", retM: conf, errS: errors.New("close_err")}, + &mockProvider{scheme: "err", retC: conf, errS: errors.New("close_err")}, } }(), expectShutdownErr: true, @@ -145,7 +151,7 @@ func TestMapResolver_Errors(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - resolver, err := newMapResolver(tt.locations, makeMapProvidersMap(tt.providers...), tt.converters) + resolver, err := NewResolver(ResolverSettings{URIs: tt.locations, Providers: makeMapProvidersMap(tt.providers...), Converters: tt.converters}) assert.NoError(t, err) _, errN := resolver.Resolve(context.Background()) @@ -188,27 +194,27 @@ func TestBackwardsCompatibilityForFilePath(t *testing.T) { { name: "unix", location: `/test`, - errMessage: `unable to read the file file:/test`, + errMessage: `file:/test`, }, { name: "file_unix", location: `file:/test`, - errMessage: `unable to read the file file:/test`, + errMessage: `file:/test`, }, { name: "windows_C", location: `C:\test`, - errMessage: `unable to read the file file:C:\test`, + errMessage: `file:C:\test`, }, { name: "windows_z", location: `z:\test`, - errMessage: `unable to read the file file:z:\test`, + errMessage: `file:z:\test`, }, { name: "file_windows", location: `file:C:\test`, - errMessage: `unable to read the file file:C:\test`, + errMessage: `file:C:\test`, }, { name: "invalid_scheme", @@ -217,21 +223,19 @@ func TestBackwardsCompatibilityForFilePath(t *testing.T) { }, } for _, tt := range tests { - resolver, err := newMapResolver([]string{tt.location}, makeMapProvidersMap(fileprovider.New()), nil) + resolver, err := NewResolver(ResolverSettings{URIs: []string{tt.location}, Providers: makeMapProvidersMap(&errFileProvider{}), Converters: nil}) assert.NoError(t, err) _, err = resolver.Resolve(context.Background()) assert.Contains(t, err.Error(), tt.errMessage, tt.name) } } -func TestMapResolver(t *testing.T) { - provider := func() confmap.Provider { - conf, err := confmaptest.LoadConf(filepath.Join("testdata", "otelcol-nop.yaml")) - require.NoError(t, err) - return &mockProvider{retM: conf} +func TestResolver(t *testing.T) { + provider := func() Provider { + return &mockProvider{retC: newConfFromFile(t, filepath.Join("testdata", "config.yaml"))} }() - resolver, err := newMapResolver([]string{"mock:"}, makeMapProvidersMap(provider), nil) + resolver, err := NewResolver(ResolverSettings{URIs: []string{"mock:"}, Providers: makeMapProvidersMap(provider), Converters: nil}) require.NoError(t, err) _, errN := resolver.Resolve(context.Background()) assert.NoError(t, errN) @@ -251,62 +255,49 @@ func TestMapResolver(t *testing.T) { assert.NoError(t, errC) } -func TestMapResolverNoLocations(t *testing.T) { - _, err := newMapResolver([]string{}, makeMapProvidersMap(fileprovider.New()), nil) +func TestResolverNoLocations(t *testing.T) { + _, err := NewResolver(ResolverSettings{URIs: []string{}, Providers: makeMapProvidersMap(&mockProvider{}), Converters: nil}) assert.Error(t, err) } -func TestMapResolverMapProviders(t *testing.T) { - _, err := newMapResolver([]string{filepath.Join("testdata", "otelcol-nop.yaml")}, nil, nil) +func TestResolverNoProviders(t *testing.T) { + _, err := NewResolver(ResolverSettings{URIs: []string{filepath.Join("testdata", "config.yaml")}, Providers: nil, Converters: nil}) assert.Error(t, err) } -func TestMapResolverNoWatcher(t *testing.T) { - watcherWG := sync.WaitGroup{} - resolver, err := newMapResolver( - []string{filepath.Join("testdata", "otelcol-nop.yaml")}, - makeMapProvidersMap(fileprovider.New()), nil) - require.NoError(t, err) - _, errN := resolver.Resolve(context.Background()) - assert.NoError(t, errN) - - watcherWG.Add(1) - go func() { - errW, ok := <-resolver.Watch() - // Channel is closed, no exception - assert.False(t, ok) - assert.NoError(t, errW) - watcherWG.Done() +func TestResolverShutdownClosesWatch(t *testing.T) { + provider := func() Provider { + return &mockProvider{retC: newConfFromFile(t, filepath.Join("testdata", "config.yaml"))} }() - assert.NoError(t, resolver.Shutdown(context.Background())) - watcherWG.Wait() -} - -func TestMapResolverShutdownClosesWatch(t *testing.T) { - provider := func() confmap.Provider { - // Use fakeRetrieved with nil errors to have Watchable interface implemented. - conf, err := confmaptest.LoadConf(filepath.Join("testdata", "otelcol-nop.yaml")) - require.NoError(t, err) - return &mockProvider{retM: conf, errW: configsource.ErrSessionClosed} - }() - - resolver, err := newMapResolver([]string{"mock:"}, makeMapProvidersMap(provider), nil) + resolver, err := NewResolver(ResolverSettings{URIs: []string{"mock:"}, Providers: makeMapProvidersMap(provider), Converters: nil}) _, errN := resolver.Resolve(context.Background()) require.NoError(t, err) assert.NoError(t, errN) - watcherWG := sync.WaitGroup{} + var watcherWG sync.WaitGroup watcherWG.Add(1) go func() { + // The mock implementation sends a first watch event. errW, ok := <-resolver.Watch() + assert.Nil(t, errW) + assert.True(t, ok) + + errW, ok = <-resolver.Watch() // Channel is closed, no exception + assert.Nil(t, errW) assert.False(t, ok) - assert.NoError(t, errW) watcherWG.Done() }() assert.NoError(t, resolver.Shutdown(context.Background())) watcherWG.Wait() } +func makeMapProvidersMap(providers ...Provider) map[string]Provider { + ret := make(map[string]Provider, len(providers)) + for _, provider := range providers { + ret[provider.Scheme()] = provider + } + return ret +} diff --git a/service/config_provider.go b/service/config_provider.go index aca1fb585e45..d2f485b6b9c4 100644 --- a/service/config_provider.go +++ b/service/config_provider.go @@ -62,10 +62,11 @@ type ConfigProvider interface { } type configProvider struct { - mapResolver *mapResolver + mapResolver *confmap.Resolver } // ConfigProviderSettings are the settings to configure the behavior of the ConfigProvider. +// TODO: embed confmap.ResolverSettings into this to avoid duplicates. type ConfigProviderSettings struct { // Locations from where the confmap.Conf is retrieved, and merged in the given order. // It is required to have at least one location. @@ -93,7 +94,7 @@ func newDefaultConfigProviderSettings(locations []string) ConfigProviderSettings // * Then applies all the confmap.Converter in the given order. // * Then unmarshalls the confmap.Conf into the service Config. func NewConfigProvider(set ConfigProviderSettings) (ConfigProvider, error) { - mr, err := newMapResolver(set.Locations, set.MapProviders, set.MapConverters) + mr, err := confmap.NewResolver(confmap.ResolverSettings{URIs: set.Locations, Providers: set.MapProviders, Converters: set.MapConverters}) if err != nil { return nil, err } @@ -128,3 +129,11 @@ func (cm *configProvider) Watch() <-chan error { func (cm *configProvider) Shutdown(ctx context.Context) error { return cm.mapResolver.Shutdown(ctx) } + +func makeMapProvidersMap(providers ...confmap.Provider) map[string]confmap.Provider { + ret := make(map[string]confmap.Provider, len(providers)) + for _, provider := range providers { + ret[provider.Scheme()] = provider + } + return ret +}