diff --git a/internal/agent/agent.go b/internal/agent/agent.go index ab3551f46..c197f5ef4 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -18,6 +18,7 @@ import ( "github.com/newrelic/infrastructure-agent/pkg/sysinfo/hostid" + "github.com/newrelic/infrastructure-agent/internal/agent/cmdchannel/fflag" "github.com/newrelic/infrastructure-agent/internal/agent/instrumentation" "github.com/newrelic/infrastructure-agent/internal/agent/inventory" "github.com/newrelic/infrastructure-agent/internal/agent/types" @@ -95,6 +96,7 @@ type Agent struct { agentID *entity.ID // pointer as it's referred from several points mtx sync.Mutex // Protect plugins notificationHandler *ctl.NotificationHandlerWithCancellation // Handle ipc messaging. + ffRetriever feature_flags.Retriever } type inventoryState struct { @@ -393,6 +395,7 @@ func NewAgent( cloudHarvester, fpHarvester, notificationHandler, + ffRetriever, ) } @@ -410,6 +413,7 @@ func New( cloudHarvester cloud.Harvester, fpHarvester fingerprint.Harvester, notificationHandler *ctl.NotificationHandlerWithCancellation, + ffRetriever feature_flags.Retriever, ) (*Agent, error) { a := &Agent{ Context: ctx, @@ -422,6 +426,7 @@ func New( connectSrv: connectSrv, provideIDs: provideIDs, notificationHandler: notificationHandler, + ffRetriever: ffRetriever, } a.plugins = make([]Plugin, 0) @@ -820,6 +825,20 @@ func (a *Agent) Run() (err error) { close(exit) }() + // We check FF to delete the whole inventory and trigger the whoel inventory send + // This will bypass the deltas cache and force the inventory to be sent after restarting the Agent + if a.ffRetriever != nil { + alog.Debug("readding FlagFullInventoryDeletion feature flag") + + ffFullInventoryDeletionEnabled, ffExists := a.ffRetriever.GetFeatureFlag(fflag.FlagFullInventoryDeletion) + if ffExists && ffFullInventoryDeletionEnabled { + alog.Info("Cleaning inventory cache and forcing full inventory report") + a.store.ResetAllDeltas(a.Context.EntityKey()) + } + } else { + alog.Warn("Feature flags retriever is not available") + } + if a.inventoryHandler != nil { if a.shouldSendInventory() { a.inventoryHandler.Start() diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 4d36938cd..7f4855f5d 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -20,6 +20,8 @@ import ( "testing" "time" + "github.com/newrelic/infrastructure-agent/internal/agent/cmdchannel/fflag" + "github.com/newrelic/infrastructure-agent/internal/agent/delta" agentTypes "github.com/newrelic/infrastructure-agent/internal/agent/types" "github.com/newrelic/infrastructure-agent/internal/feature_flags" @@ -52,7 +54,7 @@ var NilIDLookup host.IDLookup var matcher = func(interface{}) bool { return true } -func newTesting(cfg *config.Config) *Agent { +func newTesting(cfg *config.Config, ffRetriever feature_flags.Retriever) *Agent { dataDir, err := ioutil.TempDir("", "prefix") if err != nil { panic(err) @@ -93,6 +95,7 @@ func newTesting(cfg *config.Config) *Agent { cloudDetector, fpHarvester, ctl.NewNotificationHandlerWithCancellation(nil), + ffRetriever, ) if err != nil { panic(err) @@ -113,17 +116,19 @@ func (self *TestAgentData) SortKey() string { } func TestIgnoreInventory(t *testing.T) { - a := newTesting(&config.Config{ + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(&config.Config{ IgnoredInventoryPathsMap: map[string]struct{}{ "test/plugin/yum": {}, }, MaxInventorySize: 1024, - }) + }, ffRetriever) + defer func() { - _ = os.RemoveAll(a.store.DataDir) + _ = os.RemoveAll(agent.store.DataDir) }() - assert.NoError(t, a.storePluginOutput(agentTypes.PluginOutput{ + require.NoError(t, agent.storePluginOutput(agentTypes.PluginOutput{ Id: ids.PluginID{"test", "plugin"}, Entity: entity.NewFromNameWithoutID("someEntity"), Data: agentTypes.PluginInventoryDataset{ @@ -132,7 +137,7 @@ func TestIgnoreInventory(t *testing.T) { }, })) - restoredDataBytes, err := ioutil.ReadFile(filepath.Join(a.store.DataDir, "test", "someEntity", "plugin.json")) + restoredDataBytes, err := ioutil.ReadFile(filepath.Join(agent.store.DataDir, "test", "someEntity", "plugin.json")) require.NoError(t, err) var restoredData map[string]interface{} @@ -161,8 +166,10 @@ func TestServicePidMap(t *testing.T) { } func TestSetAgentKeysDisplayInstance(t *testing.T) { - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) idMap := host.IDLookup{ sysinfo.HOST_SOURCE_DISPLAY_NAME: "displayName", @@ -170,14 +177,17 @@ func TestSetAgentKeysDisplayInstance(t *testing.T) { sysinfo.HOST_SOURCE_INSTANCE_ID: "instanceId", } - a.setAgentKey(idMap) - assert.Equal(t, idMap[sysinfo.HOST_SOURCE_INSTANCE_ID], a.Context.EntityKey()) + err := agent.setAgentKey(idMap) + require.NoError(t, err) + assert.Equal(t, idMap[sysinfo.HOST_SOURCE_INSTANCE_ID], agent.Context.EntityKey()) } // Test that empty strings in the identity map are properly ignored in favor of non-empty ones func TestSetAgentKeysInstanceEmptyString(t *testing.T) { - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) keys := host.IDLookup{ sysinfo.HOST_SOURCE_DISPLAY_NAME: "displayName", @@ -185,33 +195,41 @@ func TestSetAgentKeysInstanceEmptyString(t *testing.T) { sysinfo.HOST_SOURCE_INSTANCE_ID: "", } - a.setAgentKey(keys) - assert.Equal(t, keys[sysinfo.HOST_SOURCE_DISPLAY_NAME], a.Context.EntityKey()) + err := agent.setAgentKey(keys) + require.NoError(t, err) + assert.Equal(t, keys[sysinfo.HOST_SOURCE_DISPLAY_NAME], agent.Context.EntityKey()) } func TestSetAgentKeysDisplayNameMatchesHostName(t *testing.T) { - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) keyMap := host.IDLookup{ sysinfo.HOST_SOURCE_DISPLAY_NAME: "hostName", sysinfo.HOST_SOURCE_HOSTNAME: "hostName", } - a.setAgentKey(keyMap) - assert.Equal(t, "hostName", a.Context.EntityKey()) + err := agent.setAgentKey(keyMap) + require.NoError(t, err) + assert.Equal(t, "hostName", agent.Context.EntityKey()) } func TestSetAgentKeysNoValues(t *testing.T) { - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) - assert.Error(t, a.setAgentKey(host.IDLookup{})) + require.Error(t, agent.setAgentKey(host.IDLookup{})) } func TestUpdateIDLookupTable(t *testing.T) { - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) dataset := agentTypes.PluginInventoryDataset{} dataset = append(dataset, sysinfo.HostAliases{ @@ -227,8 +245,8 @@ func TestUpdateIDLookupTable(t *testing.T) { Source: sysinfo.HOST_SOURCE_HOSTNAME_SHORT, }) - assert.NoError(t, a.updateIDLookupTable(dataset)) - assert.Equal(t, "instanceId", a.Context.EntityKey()) + require.NoError(t, agent.updateIDLookupTable(dataset)) + assert.Equal(t, "instanceId", agent.Context.EntityKey()) } func TestIDLookup_EntityNameCloudInstance(t *testing.T) { @@ -243,7 +261,7 @@ func TestIDLookup_EntityNameCloudInstance(t *testing.T) { name, err := l.AgentShortEntityName() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "instance-id", name) } @@ -258,7 +276,7 @@ func TestIDLookup_EntityNameAzure(t *testing.T) { } name, err := l.AgentShortEntityName() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "azure-id", name) } @@ -273,7 +291,7 @@ func TestIDLookup_EntityNameGCP(t *testing.T) { } name, err := l.AgentShortEntityName() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "gcp-id", name) } @@ -288,7 +306,7 @@ func TestIDLookup_EntityNameAlibaba(t *testing.T) { } name, err := l.AgentShortEntityName() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "alibaba-id", name) } @@ -299,7 +317,7 @@ func TestIDLookup_EntityNameDisplayName(t *testing.T) { } name, err := l.AgentShortEntityName() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "display-name", name) } @@ -310,7 +328,7 @@ func TestIDLookup_EntityNameShortName(t *testing.T) { } name, err := l.AgentShortEntityName() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "short", name) } @@ -319,7 +337,8 @@ func TestRemoveOutdatedEntities(t *testing.T) { const anotherPlugin = "anotherPlugin" // Given an agent - agent := newTesting(nil) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) defer os.RemoveAll(agent.store.DataDir) agent.inventories = map[string]*inventoryEntity{} @@ -366,8 +385,8 @@ func TestRemoveOutdatedEntities(t *testing.T) { _, err1 := os.Stat(filepath.Join(dataDir, aPlugin, entity.Folder)) _, err2 := os.Stat(filepath.Join(dataDir, anotherPlugin, entity.Folder)) if entity.ShouldBeRegistered { - assert.NoError(t, err1) - assert.NoError(t, err2) + require.NoError(t, err1) + require.NoError(t, err2) } else { assert.True(t, os.IsNotExist(err1)) assert.True(t, os.IsNotExist(err2)) @@ -377,30 +396,32 @@ func TestRemoveOutdatedEntities(t *testing.T) { func TestReconnectablePlugins(t *testing.T) { // Given an agent - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) wg := sync.WaitGroup{} wg.Add(2) // With a set of registered plugins nrp := nonReconnectingPlugin{invocations: 0, wg: &wg} - a.RegisterPlugin(&nrp) - rp := reconnectingPlugin{invocations: 0, context: a.Context, wg: &wg} - a.RegisterPlugin(&rp) + agent.RegisterPlugin(&nrp) + reconnPlugin := reconnectingPlugin{invocations: 0, context: agent.Context, wg: &wg} + agent.RegisterPlugin(&reconnPlugin) // That successfully started - a.startPlugins() - assert.NoError(t, wait(time.Second, &wg)) + agent.startPlugins() + require.NoError(t, wait(time.Second, &wg)) // When the agent reconnects wg.Add(1) - a.Context.Reconnect() - assert.NoError(t, wait(time.Second, &wg)) + agent.Context.Reconnect() + require.NoError(t, wait(time.Second, &wg)) // The non-reconnecting plugins are not invoked again assert.Equal(t, 1, nrp.invocations) // And the reconnecting plugins are invoked again - assert.Equal(t, 2, rp.invocations) + assert.Equal(t, 2, reconnPlugin.invocations) } func TestCheckConnectionRetry(t *testing.T) { @@ -420,7 +441,7 @@ func TestCheckConnectionRetry(t *testing.T) { // The agent should eventually connect a, err := NewAgent(cnf, "testing-timeouts", "userAgent", ffFetcher) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, a) } @@ -441,7 +462,7 @@ func TestCheckConnectionTimeout(t *testing.T) { // The agent stops reconnecting after retrying as configured _, err := NewAgent(cnf, "testing-timeouts", "userAgent", ffFetcher) - assert.Error(t, err) + require.Error(t, err) } func Test_checkCollectorConnectivity_NoTimeoutOnInfiniteRetries(t *testing.T) { @@ -466,7 +487,7 @@ func Test_checkCollectorConnectivity_NoTimeoutOnInfiniteRetries(t *testing.T) { // Then no timeout error is returned select { case err := <-connErr: - assert.Error(t, err) + require.Error(t, err) // this should never be triggered t.Fail() case <-time.After(100 * time.Millisecond): @@ -487,17 +508,19 @@ func (killingPlugin) IsExternal() bool { return false } func (killingPlugin) GetExternalPluginName() string { return "" } func TestTerminate(t *testing.T) { - a := newTesting(nil) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) defer func() { - _ = os.RemoveAll(a.store.DataDir) + _ = os.RemoveAll(agent.store.DataDir) }() - a.plugins = []Plugin{ + agent.plugins = []Plugin{ &killingPlugin{killed: false}, &killingPlugin{killed: false}, &killingPlugin{killed: false}, } - a.Terminate() - assert.Len(t, a.plugins, 3) - for _, plugin := range a.plugins { + agent.Terminate() + assert.Len(t, agent.plugins, 3) + + for _, plugin := range agent.plugins { assert.True(t, plugin.(*killingPlugin).killed) } } @@ -506,21 +529,23 @@ func TestStopByCancelFn_UsedBySignalHandler(t *testing.T) { wg := sync.WaitGroup{} wg.Add(1) - a := newTesting(nil) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + ffRetriever.ShouldGetFeatureFlag(fflag.FlagFullInventoryDeletion, false, false) + agent := newTesting(nil, ffRetriever) defer func() { - _ = os.RemoveAll(a.store.DataDir) + _ = os.RemoveAll(agent.store.DataDir) }() - a.plugins = []Plugin{ + agent.plugins = []Plugin{ &killingPlugin{killed: false}, &killingPlugin{killed: false}, &killingPlugin{killed: false}, } go func() { - assert.NoError(t, a.Run()) + require.NoError(t, agent.Run()) wg.Done() }() - a.Context.CancelFn() + agent.Context.CancelFn() wg.Wait() } @@ -587,17 +612,21 @@ func TestAgent_Run_DontSendInventoryIfFwdOnly(t *testing.T) { FirstReapInterval: tt.firstReapInterval, SendInterval: tt.sendInterval, } - a := newTesting(cfg) + + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + ffRetriever.ShouldGetFeatureFlag(fflag.FlagFullInventoryDeletion, false, false) + agent := newTesting(cfg, ffRetriever) // Give time to at least send one request - ctxTimeout, _ := context2.WithTimeout(a.Context.Ctx, time.Millisecond*10) - a.Context.Ctx = ctxTimeout + ctxTimeout, cancel := context2.WithTimeout(agent.Context.Ctx, time.Millisecond*10) + defer cancel() + agent.Context.Ctx = ctxTimeout // Inventory recording calls snd := &patchSenderCallRecorder{} - a.inventories = map[string]*inventoryEntity{"test": {sender: snd}} + agent.inventories = map[string]*inventoryEntity{"test": {sender: snd}} go func() { - assert.NoError(t, a.Run()) + require.NoError(t, agent.Run()) wg.Done() }() wg.Wait() @@ -719,12 +748,15 @@ func (self *testAgentNullableData) SortKey() string { } func TestStorePluginOutput(t *testing.T) { - a := newTesting(nil) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) + aV := "aValue" bV := "bValue" cV := "cValue" - err := a.storePluginOutput(agentTypes.PluginOutput{ + err := agent.storePluginOutput(agentTypes.PluginOutput{ Id: ids.PluginID{"test", "plugin"}, Entity: entity.NewFromNameWithoutID("someEntity"), Data: agentTypes.PluginInventoryDataset{ @@ -735,9 +767,9 @@ func TestStorePluginOutput(t *testing.T) { }, }) - assert.NoError(t, err) + require.NoError(t, err) - sourceFile := filepath.Join(a.store.DataDir, "test", "someEntity", "plugin.json") + sourceFile := filepath.Join(agent.store.DataDir, "test", "someEntity", "plugin.json") sourceB, err := ioutil.ReadFile(sourceFile) require.NoError(t, err) @@ -769,8 +801,10 @@ func (self mockHostinfoData) SortKey() string { } func BenchmarkStorePluginOutput(b *testing.B) { - a := newTesting(&config.Config{MaxInventorySize: 1000 * 1000}) - defer os.RemoveAll(a.store.DataDir) + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(&config.Config{MaxInventorySize: 1000 * 1000}, ffRetriever) + + defer os.RemoveAll(agent.store.DataDir) distroName := "Fedora 29 (Cloud Edition)" benchmarks := []struct { @@ -812,7 +846,7 @@ func BenchmarkStorePluginOutput(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = a.storePluginOutput(output) + _ = agent.storePluginOutput(output) } b.StopTimer() }) @@ -889,10 +923,10 @@ func Test_ProcessSampling(t *testing.T) { } for _, tc := range testCases { - a, _ := NewAgent(tc.c, "test", "userAgent", tc.ff) + agent, _ := NewAgent(tc.c, "test", "userAgent", tc.ff) t.Run(tc.name, func(t *testing.T) { - actual := a.Context.shouldIncludeEvent(someSample) + actual := agent.Context.shouldIncludeEvent(someSample) assert.Equal(t, tc.want, actual) }) } @@ -1086,12 +1120,12 @@ func Test_ProcessSamplingExcludesAllCases(t *testing.T) { } ff := test.NewFFRetrieverReturning(false, false) - a, _ := NewAgent(cnf, "test", "userAgent", ff) + agent, _ := NewAgent(cnf, "test", "userAgent", ff) t.Run(testCase.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, testCase.expectInclude, a.Context.IncludeEvent(someSample)) + assert.Equal(t, testCase.expectInclude, agent.Context.IncludeEvent(someSample)) }) } } @@ -1166,11 +1200,12 @@ func TestRunsWithCloudProvider(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} //nolint:exhaustruct agt := newTesting(&config.Config{ CloudProvider: testCase.cloudProvider, CloudMaxRetryCount: testCase.retries, - }) + }, ffRetriever) err := agt.Run() @@ -1224,10 +1259,11 @@ func TestAgent_checkInstanceIDRetry(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - a := newTesting(nil) - a.cloudHarvester = testCase.cloudHarvester + ffRetriever := &feature_flags.FeatureFlagRetrieverMock{} + agent := newTesting(nil, ffRetriever) + agent.cloudHarvester = testCase.cloudHarvester - if err := a.checkInstanceIDRetry(testCase.args.maxRetries, testCase.args.backoffTime); (err != nil) != testCase.wantErr { + if err := agent.checkInstanceIDRetry(testCase.args.maxRetries, testCase.args.backoffTime); (err != nil) != testCase.wantErr { t.Errorf("Agent.checkInstanceIDRetry() error = %v, wantErr %v", err, testCase.wantErr) } }) diff --git a/internal/agent/cmdchannel/fflag/ffhandler.go b/internal/agent/cmdchannel/fflag/ffhandler.go index 0e37a85a8..6dd610e7d 100644 --- a/internal/agent/cmdchannel/fflag/ffhandler.go +++ b/internal/agent/cmdchannel/fflag/ffhandler.go @@ -23,10 +23,11 @@ const ( FlagParallelizeInventory = "parallelize_inventory_enabled" FlagAsyncInventoryHandler = "async_inventory_handler_enabled" - FlagProtocolV4 = "protocol_v4_enabled" - FlagFullProcess = "full_process_sampling" - FlagDmRegisterDeprecated = "dm_register_deprecated" - FlagFluentBit19 = "fluent_bit_19_win" + FlagProtocolV4 = "protocol_v4_enabled" + FlagFullProcess = "full_process_sampling" + FlagDmRegisterDeprecated = "dm_register_deprecated" + FlagFluentBit19 = "fluent_bit_19_win" + FlagFullInventoryDeletion = "full_inventory_deletion" // Config CfgYmlRegisterEnabled = "register_enabled" CfgYmlParallelizeInventory = "inventory_queue_len" @@ -183,7 +184,8 @@ func (h *handler) Handle(ctx context.Context, c commandapi.Command, isInitialFet func isBasicFeatureFlag(flag string) bool { return flag == FlagProtocolV4 || flag == FlagFullProcess || - flag == FlagDmRegisterDeprecated + flag == FlagDmRegisterDeprecated || + flag == FlagFullInventoryDeletion } func (h *handler) setFFConfig(ff string, enabled bool) { diff --git a/test/infra/agent.go b/test/infra/agent.go index 3d0d7a1cc..9f0dbc970 100644 --- a/test/infra/agent.go +++ b/test/infra/agent.go @@ -4,6 +4,8 @@ package infra import ( "compress/gzip" + "context" + "io/ioutil" "net/http" "path/filepath" @@ -17,6 +19,7 @@ import ( "github.com/newrelic/infrastructure-agent/internal/agent" "github.com/newrelic/infrastructure-agent/internal/agent/delta" + "github.com/newrelic/infrastructure-agent/internal/feature_flags" "github.com/newrelic/infrastructure-agent/internal/testhelpers" backendhttp "github.com/newrelic/infrastructure-agent/pkg/backend/http" "github.com/newrelic/infrastructure-agent/pkg/backend/identityapi" @@ -112,11 +115,13 @@ func NewAgentWithConnectClientAndConfig(connectClient *http.Client, dataClient b transport := backendhttp.BuildTransport(cfg, backendhttp.ClientTimeout) transport = backendhttp.NewRequestDecoratorTransport(cfg, transport) dataClient = backendhttp.NewRequestDecoratorTransport(cfg, infra.ToRoundTripper(dataClient)).RoundTrip - a, err := agent.New(cfg, ctx, "user-agent", lookups, st, connectSrv, provideIDs, dataClient, transport, cloudDetector, fingerprintHarvester, ctl.NewNotificationHandlerWithCancellation(nil)) + ffRetriever := feature_flags.NewManager(map[string]bool{}) + agent, err := agent.New(cfg, ctx, "user-agent", lookups, st, connectSrv, provideIDs, dataClient, transport, cloudDetector, fingerprintHarvester, ctl.NewNotificationHandlerWithCancellation(context.TODO()), ffRetriever) if err != nil { panic(err) } - a.Init() - return a + agent.Init() + + return agent }