From c0520a1e4dc701eb603f544f120fe3fcda296e2d Mon Sep 17 00:00:00 2001
From: Sean Porter <portertech@gmail.com>
Date: Fri, 13 Oct 2023 15:25:08 -0700
Subject: [PATCH] OpAMP load/save effective configuration using
 remote_configuration_directory (#1274)

* opamp exclusive ulid config, semconv attrib keys

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp load and save effective config from/to remote configuration directory

Signed-off-by: Sean Porter <portertech@gmail.com>

* updated opamp readme to reflect the changes in remote config persistence

Signed-off-by: Sean Porter <portertech@gmail.com>

* configurable opamp accepts remote configuration

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp config validate remote_configuration_directory existence

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp don't use io/ioutil

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp changelog feat entry

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp platform agnostic directory stat testing

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp cross-platform glob path

Signed-off-by: Sean Porter <portertech@gmail.com>

* translate service id uuid to opamp agent ulid

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp enforce accepts remote config false

Signed-off-by: Sean Porter <portertech@gmail.com>

* removed opamp endpoint readme "as of" date

Signed-off-by: Sean Porter <portertech@gmail.com>

* opamp save config rm previous config error checking

Signed-off-by: Sean Porter <portertech@gmail.com>

* updated changelog entry for opamp changes

Signed-off-by: Sean Porter <portertech@gmail.com>

---------

Signed-off-by: Sean Porter <portertech@gmail.com>
---
 CHANGELOG.md                                  |   7 +
 pkg/extension/opampextension/README.md        |  18 +-
 pkg/extension/opampextension/config.go        |  10 +
 pkg/extension/opampextension/config_test.go   |  19 +-
 pkg/extension/opampextension/factory.go       |   3 +-
 pkg/extension/opampextension/factory_test.go  |   1 +
 pkg/extension/opampextension/go.mod           |   1 +
 pkg/extension/opampextension/go.sum           |   2 +
 pkg/extension/opampextension/opamp_agent.go   | 202 +++++++++---------
 .../opampextension/opamp_agent_test.go        |  70 ++++--
 .../opampextension/testdata/config.yaml       |   2 +-
 .../{ => opamp.d}/opamp-remote-config.yaml    |   0
 12 files changed, 196 insertions(+), 139 deletions(-)
 rename pkg/extension/opampextension/testdata/{ => opamp.d}/opamp-remote-config.yaml (100%)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8b9081129..1787a704de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Released TBD
 
+### Changed
+
+- feat(opampextension): opamp effective configuration is only derived from the
+  remote_configuration_directory contents and the contents are managed by the
+  extension [#1274]
+
+[#1274]: https://github.com/SumoLogic/sumologic-otel-collector/pull/1274
 [Unreleased]: https://github.com/SumoLogic/sumologic-otel-collector/compare/v0.87.0-sumo-0...main
 
 ## [v0.87.0-sumo-0]
diff --git a/pkg/extension/opampextension/README.md b/pkg/extension/opampextension/README.md
index bc4adb8122..2577b0c4a5 100644
--- a/pkg/extension/opampextension/README.md
+++ b/pkg/extension/opampextension/README.md
@@ -40,9 +40,8 @@ extensions:
     installation_token: <token>
     api_base_url: <api_endpoint_url>
   opamp:
-    instance_uid: <uniq_uid>
     endpoint: <wss_endpoint_url>
-    remote_configuration_directory: /etc/otelcol-sumo/conf.d
+    remote_configuration_directory: /etc/otelcol-sumo/opamp.d
 ```
 
 ## API URLs
@@ -68,7 +67,7 @@ option:
 
 Here is a list of valid values for the OpAMP `endpoint** configuration option:
 
-**Note:** As of Jan 2023, these endpoints are not yet available.
+**Note:** These endpoints are not yet available.
 
 |  Deployment   | API base URL                                |
 |:-------------:|---------------------------------------------|
@@ -84,11 +83,8 @@ Here is a list of valid values for the OpAMP `endpoint** configuration option:
 ## Storing local configuration
 
 When the OpAMP extension receives a remote configuration from the OpAMP server,
-it persists the YAML configuration to a local file. The path of this file is
-determined by the `remote_configuration_directory` configuration option and the
-file name is `opamp-remote-config.yaml`. For example, if
-`remote_configuration_directory` is set to `/etc/otelcol-sumo/conf.d`, the
-resulting local configuration file path would be
-`/etc/otelcol-sumo/conf.d/opamp-remote-config.yaml`. A configuration provider
-must be used in order to load the stored configuration, for example: `--config
-"glob:/etc/otelcol-sumo/conf.d/*"`.
+it persists each received YAML configuration to a local file in the
+`remote_configuration_directory`. The existing contents of the
+`remote_configuration_directory` are removed before doing so. A configuration
+provider must be used in order to load the stored configuration, for example:
+`--config "glob:/etc/otelcol-sumo/opamp.d/*"`.
diff --git a/pkg/extension/opampextension/config.go b/pkg/extension/opampextension/config.go
index 0057e53d48..2df8e652f2 100644
--- a/pkg/extension/opampextension/config.go
+++ b/pkg/extension/opampextension/config.go
@@ -16,6 +16,8 @@ package opampextension
 
 import (
 	"errors"
+	"fmt"
+	"os"
 
 	"github.com/oklog/ulid/v2"
 	"go.opentelemetry.io/collector/component"
@@ -34,6 +36,9 @@ type Config struct {
 	// RemoteConfigurationDirectory is where received OpAMP remote configuration
 	// is stored.
 	RemoteConfigurationDirectory string `mapstructure:"remote_configuration_directory"`
+
+	// AcceptsRemoteConfiguration indicates if the OpAMP agent will accept remote configuration.
+	AcceptsRemoteConfiguration bool `mapstructure:"accepts_remote_configuration"`
 }
 
 // CreateDefaultHTTPClientSettings returns default http client settings
@@ -58,5 +63,10 @@ func (cfg *Config) Validate() error {
 		return errors.New("opamp remote_configuration_directory must be provided")
 	}
 
+	d := cfg.RemoteConfigurationDirectory
+	if _, err := os.Stat(d); err != nil {
+		return fmt.Errorf("opamp remote_configuration_directory %s must be readable: %v", d, err)
+	}
+
 	return nil
 }
diff --git a/pkg/extension/opampextension/config_test.go b/pkg/extension/opampextension/config_test.go
index 4ef631e065..7d86aaf4b2 100644
--- a/pkg/extension/opampextension/config_test.go
+++ b/pkg/extension/opampextension/config_test.go
@@ -15,7 +15,9 @@
 package opampextension
 
 import (
+	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -50,7 +52,8 @@ func TestUnmarshalConfig(t *testing.T) {
 				},
 			},
 			InstanceUID:                  "01BX5ZZKBKACTAV9WEVGEMMVRZ",
-			RemoteConfigurationDirectory: "/tmp/",
+			RemoteConfigurationDirectory: "/tmp/opamp.d",
+			AcceptsRemoteConfiguration:   true,
 		}, cfg)
 }
 
@@ -59,9 +62,19 @@ func TestConfigValidate(t *testing.T) {
 	err := cfg.Validate()
 	require.Error(t, err)
 	assert.Equal(t, "opamp remote_configuration_directory must be provided", err.Error())
-	cfg.RemoteConfigurationDirectory = "/tmp/"
+
+	cfg.RemoteConfigurationDirectory = "/tmp/opamp.d"
 	err = cfg.Validate()
-	require.NoError(t, err)
+	assert.True(t, strings.HasPrefix(err.Error(), "opamp remote_configuration_directory /tmp/opamp.d must be readable:"))
+
+	d, err := os.MkdirTemp("", "opamp.d")
+	assert.NoError(t, err)
+	defer os.RemoveAll(d)
+
+	cfg.RemoteConfigurationDirectory = d
+	err = cfg.Validate()
+	assert.NoError(t, err)
+
 	cfg.InstanceUID = "01BX5ZZKBKACTAV9WEVGEMMVRZFAIL"
 	err = cfg.Validate()
 	require.Error(t, err)
diff --git a/pkg/extension/opampextension/factory.go b/pkg/extension/opampextension/factory.go
index 6e1c512771..25de552481 100644
--- a/pkg/extension/opampextension/factory.go
+++ b/pkg/extension/opampextension/factory.go
@@ -32,7 +32,8 @@ func NewFactory() extension.Factory {
 
 func createDefaultConfig() component.Config {
 	return &Config{
-		HTTPClientSettings: CreateDefaultHTTPClientSettings(),
+		HTTPClientSettings:         CreateDefaultHTTPClientSettings(),
+		AcceptsRemoteConfiguration: true,
 	}
 }
 
diff --git a/pkg/extension/opampextension/factory_test.go b/pkg/extension/opampextension/factory_test.go
index e01798d785..85a09b58f0 100644
--- a/pkg/extension/opampextension/factory_test.go
+++ b/pkg/extension/opampextension/factory_test.go
@@ -37,6 +37,7 @@ func TestFactory_CreateDefaultConfig(t *testing.T) {
 				AuthenticatorID: component.NewID("sumologic"),
 			},
 		},
+		AcceptsRemoteConfiguration: true,
 	})
 
 	assert.NoError(t, componenttest.CheckConfigStruct(cfg))
diff --git a/pkg/extension/opampextension/go.mod b/pkg/extension/opampextension/go.mod
index 7fdd19503c..f536984ac2 100644
--- a/pkg/extension/opampextension/go.mod
+++ b/pkg/extension/opampextension/go.mod
@@ -4,6 +4,7 @@ go 1.20
 
 require (
 	github.com/SumoLogic/sumologic-otel-collector/pkg/extension/sumologicextension v0.77.0-sumo-0
+	github.com/google/uuid v1.3.0
 	github.com/knadh/koanf/parsers/yaml v0.1.0
 	github.com/knadh/koanf/providers/rawbytes v0.1.0
 	github.com/knadh/koanf/v2 v2.0.1
diff --git a/pkg/extension/opampextension/go.sum b/pkg/extension/opampextension/go.sum
index 424dae2b3e..26e47d269c 100644
--- a/pkg/extension/opampextension/go.sum
+++ b/pkg/extension/opampextension/go.sum
@@ -27,6 +27,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
diff --git a/pkg/extension/opampextension/opamp_agent.go b/pkg/extension/opampextension/opamp_agent.go
index 7cf2e0c13f..38a1c8640e 100644
--- a/pkg/extension/opampextension/opamp_agent.go
+++ b/pkg/extension/opampextension/opamp_agent.go
@@ -20,10 +20,11 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
+	"reflect"
 	"runtime"
-	"sort"
 	"syscall"
 
+	"github.com/google/uuid"
 	"github.com/knadh/koanf/parsers/yaml"
 	"github.com/knadh/koanf/providers/rawbytes"
 	"github.com/knadh/koanf/v2"
@@ -53,7 +54,7 @@ type opampAgent struct {
 
 	instanceId ulid.ULID
 
-	effectiveConfig string
+	effectiveConfig map[string]*protobufs.AgentConfigFile
 
 	agentDescription *protobufs.AgentDescription
 
@@ -66,13 +67,8 @@ func (o *opampAgent) Start(ctx context.Context, host component.Host) error {
 	o.host = host
 	o.opampClient = client.NewWebSocket(o.logger.Sugar())
 
-	path := filepath.Join(o.cfg.RemoteConfigurationDirectory, "opamp-remote-config.yaml")
-	if _, err := os.Stat(path); err == nil {
-		err := o.loadEffectiveConfig(path)
-
-		if err != nil {
-			return err
-		}
+	if err := o.loadEffectiveConfig(o.cfg.RemoteConfigurationDirectory); err != nil {
+		return err
 	}
 
 	if err := o.createAgentDescription(); err != nil {
@@ -156,9 +152,7 @@ func (o *opampAgent) startClient(ctx context.Context) error {
 			OnMessageFunc: o.onMessage,
 		},
 		RemoteConfigStatus: o.remoteConfigStatus,
-		Capabilities: protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig |
-			protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig |
-			protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig,
+		Capabilities:       o.getAgentCapabilities(),
 	}
 
 	o.logger.Debug("Starting OpAMP client...")
@@ -246,21 +240,21 @@ func newOpampAgent(cfg *Config, logger *zap.Logger, build component.BuildInfo, r
 
 	uid := ulid.Make()
 
-	sid, ok := res.Attributes().Get(semconv.AttributeServiceInstanceID)
-	if ok {
-		puid, err := ulid.Parse(sid.AsString())
-		if err != nil {
-			return nil, err
-		}
-		uid = puid
-	}
-
 	if cfg.InstanceUID != "" {
 		puid, err := ulid.Parse(cfg.InstanceUID)
 		if err != nil {
 			return nil, err
 		}
 		uid = puid
+	} else {
+		sid, ok := res.Attributes().Get(semconv.AttributeServiceInstanceID)
+		if ok {
+			uuid, err := uuid.Parse(sid.AsString())
+			if err != nil {
+				return nil, err
+			}
+			uid = ulid.ULID(uuid)
+		}
 	}
 
 	agent := &opampAgent{
@@ -274,6 +268,18 @@ func newOpampAgent(cfg *Config, logger *zap.Logger, build component.BuildInfo, r
 	return agent, nil
 }
 
+func (o *opampAgent) getAgentCapabilities() protobufs.AgentCapabilities {
+	c := protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig
+
+	if o.cfg.AcceptsRemoteConfiguration {
+		c = c |
+			protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig |
+			protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig
+	}
+
+	return c
+}
+
 func stringKeyValue(key, value string) *protobufs.KeyValue {
 	return &protobufs.KeyValue{
 		Key: key,
@@ -290,9 +296,9 @@ func (o *opampAgent) createAgentDescription() error {
 	}
 
 	ident := []*protobufs.KeyValue{
-		stringKeyValue("service.instance.id", o.instanceId.String()),
-		stringKeyValue("service.name", o.agentType),
-		stringKeyValue("service.version", o.agentVersion),
+		stringKeyValue(semconv.AttributeServiceInstanceID, o.instanceId.String()),
+		stringKeyValue(semconv.AttributeServiceName, o.agentType),
+		stringKeyValue(semconv.AttributeServiceVersion, o.agentVersion),
 	}
 
 	nonIdent := []*protobufs.KeyValue{
@@ -309,41 +315,77 @@ func (o *opampAgent) createAgentDescription() error {
 	return nil
 }
 
-func (o *opampAgent) loadEffectiveConfig(path string) error {
-	var k = koanf.New(".")
+func (o *opampAgent) loadEffectiveConfig(dir string) error {
+	if _, err := os.Stat(dir); err != nil {
+		return err
+	}
 
-	rb, err := os.ReadFile(path)
+	ec := map[string]*protobufs.AgentConfigFile{}
 
+	paths, err := filepath.Glob(filepath.Join(dir, "*.yaml"))
 	if err != nil {
 		return err
 	}
 
-	if err := k.Load(rawbytes.Provider(rb), yaml.Parser()); err != nil {
-		return err
-	}
+	for _, p := range paths {
+		var k = koanf.New(".")
 
-	ecb, err := k.Marshal(yaml.Parser())
-	if err != nil {
-		return err
+		rb, err := os.ReadFile(p)
+
+		if err != nil {
+			return err
+		}
+
+		if err := k.Load(rawbytes.Provider(rb), yaml.Parser()); err != nil {
+			return err
+		}
+
+		fb, err := k.Marshal(yaml.Parser())
+		if err != nil {
+			return err
+		}
+
+		ec[filepath.Base(p)] = &protobufs.AgentConfigFile{Body: fb}
 	}
 
-	o.effectiveConfig = string(ecb)
+	o.effectiveConfig = ec
 
 	return nil
 }
 
-func (o *opampAgent) saveEffectiveConfig(path string) error {
-	f, err := os.Create(path)
+func (o *opampAgent) saveEffectiveConfig(dir string) error {
+	d, err := os.Open(dir)
 	if err != nil {
 		return err
 	}
-	defer f.Close()
 
-	_, err = f.Write([]byte(o.effectiveConfig))
+	files, err := d.Readdir(0)
 	if err != nil {
 		return err
 	}
 
+	for _, f := range files {
+		err := os.Remove(filepath.Join(dir, f.Name()))
+		if err != nil {
+			return err
+		}
+	}
+
+	for k, v := range o.effectiveConfig {
+		p := filepath.Join(dir, k)
+
+		f, err := os.Create(p)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		_, err = f.Write(v.Body)
+		if err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
 
@@ -367,93 +409,45 @@ func (o *opampAgent) updateAgentIdentity(instanceId ulid.ULID) {
 func (o *opampAgent) composeEffectiveConfig() *protobufs.EffectiveConfig {
 	return &protobufs.EffectiveConfig{
 		ConfigMap: &protobufs.AgentConfigMap{
-			ConfigMap: map[string]*protobufs.AgentConfigFile{
-				"": {Body: []byte(o.effectiveConfig)},
-			},
+			ConfigMap: o.effectiveConfig,
 		},
 	}
 }
 
-type agentConfigFileItem struct {
-	name string
-	file *protobufs.AgentConfigFile
-}
-
-type agentConfigFileSlice []agentConfigFileItem
-
-func (a agentConfigFileSlice) Less(i, j int) bool {
-	return a[i].name < a[j].name
-}
-
-func (a agentConfigFileSlice) Swap(i, j int) {
-	t := a[i]
-	a[i] = a[j]
-	a[j] = t
-}
-
-func (a agentConfigFileSlice) Len() int {
-	return len(a)
-}
-
 func (o *opampAgent) applyRemoteConfig(config *protobufs.AgentRemoteConfig) (configChanged bool, err error) {
 	o.logger.Debug("Received remote config from OpAMP server", zap.ByteString("hash", config.ConfigHash))
 
-	var k = koanf.New(".")
-
-	orderedConfigs := agentConfigFileSlice{}
-	for name, file := range config.Config.ConfigMap {
-		if name == "" {
-			// skip instance config
-			continue
-		}
-		orderedConfigs = append(orderedConfigs, agentConfigFileItem{
-			name: name,
-			file: file,
-		})
+	if !o.cfg.AcceptsRemoteConfiguration {
+		return false, fmt.Errorf("OpAMP agent does not accept remote configuration")
 	}
 
-	// Sort to make sure the order of merging is stable.
-	sort.Sort(orderedConfigs)
+	nec := map[string]*protobufs.AgentConfigFile{}
 
-	// Append instance config as the last item.
-	instanceConfig := config.Config.ConfigMap[""]
-	if instanceConfig != nil {
-		orderedConfigs = append(orderedConfigs, agentConfigFileItem{
-			name: "",
-			file: instanceConfig,
-		})
-	}
+	for n, f := range config.Config.ConfigMap {
+		var k = koanf.New(".")
 
-	// Merge received configs.
-	for _, item := range orderedConfigs {
-		var k2 = koanf.New(".")
-		err := k2.Load(rawbytes.Provider(item.file.Body), yaml.Parser())
+		err := k.Load(rawbytes.Provider(f.Body), yaml.Parser())
 		if err != nil {
-			return false, fmt.Errorf("cannot parse config named %s: %v", item.name, err)
+			return false, fmt.Errorf("cannot parse config named %s: %v", n, err)
 		}
-		err = k.Merge(k2)
+
+		fb, err := k.Marshal(yaml.Parser())
 		if err != nil {
-			return false, fmt.Errorf("cannot merge config named %s: %v", item.name, err)
+			return false, fmt.Errorf("cannot marshal config named %s: %v", n, err)
 		}
-	}
 
-	// The merged final result is our effective config.
-	effectiveConfigBytes, err := k.Marshal(yaml.Parser())
-	if err != nil {
-		return false, fmt.Errorf("cannot marshal the OpAMP effective config: %v", err)
+		nec[n] = &protobufs.AgentConfigFile{Body: fb}
 	}
 
-	newEffectiveConfig := string(effectiveConfigBytes)
 	configChanged = false
-	if o.effectiveConfig != newEffectiveConfig {
-		path := filepath.Join(o.cfg.RemoteConfigurationDirectory, "opamp-remote-config.yaml")
-		oldEffectiveConfig := o.effectiveConfig
-		o.effectiveConfig = newEffectiveConfig
+	if !reflect.DeepEqual(o.effectiveConfig, nec) {
+		oec := o.effectiveConfig
+		o.effectiveConfig = nec
 
-		err := o.saveEffectiveConfig(path)
+		err := o.saveEffectiveConfig(o.cfg.RemoteConfigurationDirectory)
 		if err != nil {
-			o.effectiveConfig = oldEffectiveConfig
-			return false, fmt.Errorf("cannot save the OpAMP effective config to %s: %v", path, err)
+			o.effectiveConfig = oec
+			return false, fmt.Errorf("cannot save the OpAMP effective config to %s: %v", o.cfg.RemoteConfigurationDirectory, err)
 		}
 
 		configChanged = true
diff --git a/pkg/extension/opampextension/opamp_agent_test.go b/pkg/extension/opampextension/opamp_agent_test.go
index fcdeda06b2..4875e6092d 100644
--- a/pkg/extension/opampextension/opamp_agent_test.go
+++ b/pkg/extension/opampextension/opamp_agent_test.go
@@ -36,8 +36,8 @@ func TestNewOpampAgent(t *testing.T) {
 	set.BuildInfo = component.BuildInfo{Version: "test version", Command: "otelcoltest"}
 	o, err := newOpampAgent(cfg.(*Config), set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
-	assert.Equal(t, o.agentType, "otelcoltest")
-	assert.Equal(t, o.agentVersion, "test version")
+	assert.Equal(t, "otelcoltest", o.agentType)
+	assert.Equal(t, "test version", o.agentVersion)
 	assert.NotEmpty(t, o.instanceId.String())
 	assert.Empty(t, o.effectiveConfig)
 	assert.Nil(t, o.agentDescription)
@@ -49,12 +49,24 @@ func TestNewOpampAgentAttributes(t *testing.T) {
 	set.BuildInfo = component.BuildInfo{Version: "test version", Command: "otelcoltest"}
 	set.Resource.Attributes().PutStr(semconv.AttributeServiceName, "otelcol-sumo")
 	set.Resource.Attributes().PutStr(semconv.AttributeServiceVersion, "sumo.0")
-	set.Resource.Attributes().PutStr(semconv.AttributeServiceInstanceID, "01BX5ZZKBKACTAV9WEVGEMMVRZ")
+	set.Resource.Attributes().PutStr(semconv.AttributeServiceInstanceID, "f8999bc1-4c9b-4619-9bae-7f009d2411ec")
 	o, err := newOpampAgent(cfg.(*Config), set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
-	assert.Equal(t, o.agentType, "otelcol-sumo")
-	assert.Equal(t, o.agentVersion, "sumo.0")
-	assert.Equal(t, o.instanceId.String(), "01BX5ZZKBKACTAV9WEVGEMMVRZ")
+	assert.Equal(t, "otelcol-sumo", o.agentType)
+	assert.Equal(t, "sumo.0", o.agentVersion)
+	assert.Equal(t, "7RK6DW2K4V8RCSQBKZ02EJ84FC", o.instanceId.String())
+}
+
+func TestGetAgentCapabilities(t *testing.T) {
+	cfg := createDefaultConfig().(*Config)
+	set := extensiontest.NewNopCreateSettings()
+	o, err := newOpampAgent(cfg, set.Logger, set.BuildInfo, set.Resource)
+	assert.NoError(t, err)
+
+	assert.Equal(t, o.getAgentCapabilities(), protobufs.AgentCapabilities(4102))
+
+	cfg.AcceptsRemoteConfiguration = false
+	assert.Equal(t, o.getAgentCapabilities(), protobufs.AgentCapabilities(4))
 }
 
 func TestCreateAgentDescription(t *testing.T) {
@@ -74,11 +86,10 @@ func TestLoadEffectiveConfig(t *testing.T) {
 	o, err := newOpampAgent(cfg.(*Config), set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
 
-	assert.Empty(t, o.effectiveConfig)
+	assert.Equal(t, len(o.effectiveConfig), 0)
 
-	path := filepath.Join("testdata", "opamp-remote-config.yaml")
-	assert.NoError(t, o.loadEffectiveConfig(path))
-	assert.NotEmpty(t, o.effectiveConfig)
+	assert.NoError(t, o.loadEffectiveConfig("testdata"))
+	assert.NotEqual(t, len(o.effectiveConfig), 0)
 }
 
 func TestSaveEffectiveConfig(t *testing.T) {
@@ -87,11 +98,11 @@ func TestSaveEffectiveConfig(t *testing.T) {
 	o, err := newOpampAgent(cfg.(*Config), set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
 
-	f, err := os.CreateTemp("", "opamp-remote-config.yaml")
+	d, err := os.MkdirTemp("", "opamp.d")
 	assert.NoError(t, err)
-	defer os.Remove(f.Name())
+	defer os.RemoveAll(d)
 
-	assert.NoError(t, o.saveEffectiveConfig(f.Name()))
+	assert.NoError(t, o.saveEffectiveConfig(d))
 }
 
 func TestUpdateAgentIdentity(t *testing.T) {
@@ -121,14 +132,19 @@ func TestComposeEffectiveConfig(t *testing.T) {
 }
 
 func TestApplyRemoteConfig(t *testing.T) {
-	cfg := createDefaultConfig()
+	d, err := os.MkdirTemp("", "opamp.d")
+	assert.NoError(t, err)
+	defer os.RemoveAll(d)
+
+	cfg := createDefaultConfig().(*Config)
+	cfg.RemoteConfigurationDirectory = d
 	set := extensiontest.NewNopCreateSettings()
-	o, err := newOpampAgent(cfg.(*Config), set.Logger, set.BuildInfo, set.Resource)
+	o, err := newOpampAgent(cfg, set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
 
-	assert.Empty(t, o.effectiveConfig)
+	assert.Equal(t, len(o.effectiveConfig), 0)
 
-	path := filepath.Join("testdata", "opamp-remote-config.yaml")
+	path := filepath.Join("testdata", "opamp.d", "opamp-remote-config.yaml")
 	rb, err := os.ReadFile(path)
 	assert.NoError(t, err)
 
@@ -144,9 +160,15 @@ func TestApplyRemoteConfig(t *testing.T) {
 	}
 
 	changed, err := o.applyRemoteConfig(rc)
-	assert.True(t, changed)
 	assert.NoError(t, err)
-	assert.NotEmpty(t, o.effectiveConfig)
+	assert.True(t, changed)
+	assert.NotEqual(t, len(o.effectiveConfig), 0)
+
+	cfg.AcceptsRemoteConfiguration = false
+	changed, err = o.applyRemoteConfig(rc)
+	assert.False(t, changed)
+	assert.Error(t, err)
+	assert.Equal(t, "OpAMP agent does not accept remote configuration", err.Error())
 }
 
 func TestShutdown(t *testing.T) {
@@ -161,8 +183,13 @@ func TestShutdown(t *testing.T) {
 }
 
 func TestStart(t *testing.T) {
+	d, err := os.MkdirTemp("", "opamp.d")
+	assert.NoError(t, err)
+	defer os.RemoveAll(d)
+
 	cfg := createDefaultConfig().(*Config)
 	cfg.HTTPClientSettings.Auth = nil
+	cfg.RemoteConfigurationDirectory = d
 	set := extensiontest.NewNopCreateSettings()
 	o, err := newOpampAgent(cfg, set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
@@ -171,8 +198,13 @@ func TestStart(t *testing.T) {
 }
 
 func TestReload(t *testing.T) {
+	d, err := os.MkdirTemp("", "opamp.d")
+	assert.NoError(t, err)
+	defer os.RemoveAll(d)
+
 	cfg := createDefaultConfig().(*Config)
 	cfg.HTTPClientSettings.Auth = nil
+	cfg.RemoteConfigurationDirectory = d
 	set := extensiontest.NewNopCreateSettings()
 	o, err := newOpampAgent(cfg, set.Logger, set.BuildInfo, set.Resource)
 	assert.NoError(t, err)
diff --git a/pkg/extension/opampextension/testdata/config.yaml b/pkg/extension/opampextension/testdata/config.yaml
index 338acbd1c6..13980f6fae 100644
--- a/pkg/extension/opampextension/testdata/config.yaml
+++ b/pkg/extension/opampextension/testdata/config.yaml
@@ -1,3 +1,3 @@
 endpoint: wss://127.0.0.1:4320/v1/opamp
 instance_uid: 01BX5ZZKBKACTAV9WEVGEMMVRZ
-remote_configuration_directory: /tmp/
+remote_configuration_directory: /tmp/opamp.d
diff --git a/pkg/extension/opampextension/testdata/opamp-remote-config.yaml b/pkg/extension/opampextension/testdata/opamp.d/opamp-remote-config.yaml
similarity index 100%
rename from pkg/extension/opampextension/testdata/opamp-remote-config.yaml
rename to pkg/extension/opampextension/testdata/opamp.d/opamp-remote-config.yaml