github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/server/agents/agents_test.go (about)

     1  // Copyright (c) 2017-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package agents
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"os"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/choria-io/go-choria/config"
    19  	"github.com/choria-io/go-choria/inter"
    20  	imock "github.com/choria-io/go-choria/inter/imocks"
    21  	"github.com/choria-io/go-choria/message"
    22  	"github.com/choria-io/go-choria/protocol"
    23  	v1 "github.com/choria-io/go-choria/protocol/v1"
    24  	"github.com/golang/mock/gomock"
    25  
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  )
    29  
    30  func Test(t *testing.T) {
    31  	os.Setenv("MCOLLECTIVE_CERTNAME", "rip.mcollective")
    32  	RegisterFailHandler(Fail)
    33  	RunSpecs(t, "Server/Agents")
    34  }
    35  
    36  var _ = Describe("Server/Agents", func() {
    37  	var (
    38  		mockctl  *gomock.Controller
    39  		mgr      *Manager
    40  		conn     *imock.MockConnector
    41  		agent    *MockAgent
    42  		requests chan inter.ConnectorMessage
    43  		ctx      context.Context
    44  		cancel   func()
    45  		fw       *imock.MockFramework
    46  		cfg      *config.Config
    47  		handler  func(ctx context.Context, msg *message.Message, request protocol.Request, ci inter.ConnectorInfo, result chan *AgentReply)
    48  	)
    49  
    50  	BeforeEach(func() {
    51  		mockctl = gomock.NewController(GinkgoT())
    52  		fw, cfg = imock.NewFrameworkForTests(mockctl, GinkgoWriter, imock.WithCallerID(), imock.LogDiscard())
    53  		cfg.Collectives = []string{"cone", "ctwo"}
    54  
    55  		requests = make(chan inter.ConnectorMessage)
    56  		ctx, cancel = context.WithCancel(context.Background())
    57  
    58  		metadata := Metadata{
    59  			Author:      "stub@example.net",
    60  			Description: "Stub Agent",
    61  			License:     "Apache-2.0",
    62  			Name:        "stub_agent",
    63  			Timeout:     10,
    64  			URL:         "https://choria.io/",
    65  			Version:     "1.0.0",
    66  		}
    67  
    68  		handler = func(ctx context.Context, msg *message.Message, request protocol.Request, ci inter.ConnectorInfo, result chan *AgentReply) {
    69  			if bytes.Equal(msg.Payload(), []byte("sleep")) {
    70  				time.Sleep(10 * time.Second)
    71  			}
    72  
    73  			reply := &AgentReply{
    74  				Body:    []byte(fmt.Sprintf("pong %s", msg.Payload())),
    75  				Message: msg,
    76  				Request: request,
    77  			}
    78  
    79  			result <- reply
    80  		}
    81  
    82  		is := NewMockServerInfoSource(mockctl)
    83  		is.EXPECT().KnownAgents().Return([]string{"stub_agent"}).AnyTimes()
    84  		is.EXPECT().Classes().Return([]string{"one", "two"}).AnyTimes()
    85  		is.EXPECT().Facts().Return(json.RawMessage(`{"stub":true}`)).AnyTimes()
    86  		is.EXPECT().AgentMetadata("stub_agent").Return(metadata, true).AnyTimes()
    87  
    88  		mgr = New(requests, fw, conn, is, fw.Logger("x"))
    89  		conn = imock.NewMockConnector(mockctl)
    90  
    91  		agent = NewMockAgent(mockctl)
    92  		agent.EXPECT().Name().Return(metadata.Name).AnyTimes()
    93  		agent.EXPECT().Metadata().Return(&metadata).AnyTimes()
    94  		agent.EXPECT().SetServerInfo(is).Return().AnyTimes()
    95  		agent.EXPECT().HandleMessage(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(handler).AnyTimes()
    96  	})
    97  
    98  	AfterEach(func() {
    99  		cancel()
   100  		mockctl.Finish()
   101  	})
   102  
   103  	Describe("DenyAgent", func() {
   104  		It("Should add the agent to the deny list", func() {
   105  			Expect(mgr.denylist).To(BeEmpty())
   106  			Expect(mgr.agentDenied("testing")).To(BeFalse())
   107  			mgr.DenyAgent("testing")
   108  			Expect(mgr.denylist).To(Equal([]string{"testing"}))
   109  			Expect(mgr.agentDenied("testing")).To(BeTrue())
   110  		})
   111  	})
   112  
   113  	Describe("RegisterAgent", func() {
   114  		It("Should reject agents with small timeouts", func() {
   115  			agent.Metadata().Timeout = 0
   116  			err := mgr.RegisterAgent(ctx, "testing", agent, conn)
   117  			Expect(err).To(MatchError("invalid agent: timeout < 1"))
   118  		})
   119  
   120  		It("Should reject agents without a name", func() {
   121  			agent.Metadata().Name = ""
   122  			err := mgr.RegisterAgent(ctx, "testing", agent, conn)
   123  			Expect(err).To(MatchError("invalid agent: invalid metadata"))
   124  		})
   125  
   126  		It("Should honor the ShouldActivate wish of the agent", func() {
   127  			agent.EXPECT().ShouldActivate().Return(false).Times(1)
   128  			err := mgr.RegisterAgent(ctx, "testing", agent, conn)
   129  			Expect(err).ToNot(HaveOccurred())
   130  			Expect(mgr.KnownAgents()).To(BeEmpty())
   131  		})
   132  
   133  		It("Should honor the deny list", func() {
   134  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   135  			mgr.DenyAgent("testing")
   136  			err := mgr.RegisterAgent(ctx, "testing", agent, conn)
   137  			Expect(err).ToNot(HaveOccurred())
   138  			Expect(mgr.KnownAgents()).To(BeEmpty())
   139  		})
   140  
   141  		It("should not subscribe the agent twice", func() {
   142  			conn.EXPECT().AgentBroadcastTarget("cone", "stub").Return("cone.stub")
   143  			conn.EXPECT().AgentBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   144  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "", gomock.Any()).Return(nil).Times(1)
   145  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "", gomock.Any()).Return(nil).Times(1)
   146  
   147  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   148  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   149  			Expect(err).ToNot(HaveOccurred())
   150  
   151  			err = mgr.RegisterAgent(ctx, "stub", agent, conn)
   152  			Expect(err).To(MatchError("agent stub is already registered"))
   153  
   154  		})
   155  
   156  		It("should subscribe the agent to all collectives", func() {
   157  			conn.EXPECT().AgentBroadcastTarget("cone", "stub").Return("cone.stub")
   158  			conn.EXPECT().AgentBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   159  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "", gomock.Any()).Return(nil).Times(1)
   160  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "", gomock.Any()).Return(nil).Times(1)
   161  
   162  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   163  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   164  			Expect(err).ToNot(HaveOccurred())
   165  		})
   166  
   167  		It("should support service agents", func() {
   168  			agent.Metadata().Service = true
   169  			conn.EXPECT().ServiceBroadcastTarget("cone", "stub").Return("cone.stub")
   170  			conn.EXPECT().ServiceBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   171  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "stub", gomock.Any()).Return(nil).Times(1)
   172  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "stub", gomock.Any()).Return(nil).Times(1)
   173  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   174  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   175  			Expect(err).ToNot(HaveOccurred())
   176  		})
   177  
   178  		It("should only register service agents in service host mode", func() {
   179  			mgr.servicesOnly = true
   180  
   181  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   182  			Expect(err).ToNot(HaveOccurred())
   183  			Expect(mgr.agents).To(BeEmpty())
   184  
   185  			agent.Metadata().Service = true
   186  			conn.EXPECT().ServiceBroadcastTarget("cone", "stub").Return("cone.stub")
   187  			conn.EXPECT().ServiceBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   188  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "stub", gomock.Any()).Return(nil).Times(1)
   189  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "stub", gomock.Any()).Return(nil).Times(1)
   190  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   191  			err = mgr.RegisterAgent(ctx, "stub", agent, conn)
   192  			Expect(err).ToNot(HaveOccurred())
   193  			Expect(mgr.agents).To(HaveLen(1))
   194  		})
   195  
   196  		It("should handle subscribe failures", func() {
   197  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   198  			conn.EXPECT().AgentBroadcastTarget("cone", "stub").Return("cone.stub")
   199  			conn.EXPECT().AgentBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   200  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "", gomock.Any()).Return(nil).AnyTimes()
   201  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "", gomock.Any()).Return(errors.New("2nd sub failed")).AnyTimes()
   202  			conn.EXPECT().Unsubscribe("cone.stub").Return(nil)
   203  
   204  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   205  			Expect(err).To(MatchError("could not register agent stub: subscription failed: 2nd sub failed"))
   206  		})
   207  
   208  		It("Should retrieve the right agent", func() {
   209  			conn.EXPECT().AgentBroadcastTarget("cone", "stub").Return("cone.stub")
   210  			conn.EXPECT().AgentBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   211  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "", gomock.Any()).Return(nil).AnyTimes()
   212  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "", gomock.Any()).Return(nil).AnyTimes()
   213  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   214  
   215  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   216  			Expect(err).ToNot(HaveOccurred())
   217  
   218  			a, ok := mgr.Get("stub")
   219  			Expect(ok).To(BeTrue())
   220  			Expect(a).To(Equal(agent))
   221  		})
   222  	})
   223  
   224  	Describe("UnregisterAgent", func() {
   225  		It("Should unsubscribe and unregister the agent", func() {
   226  			conn.EXPECT().AgentBroadcastTarget("cone", "stub").Return("cone.stub")
   227  			conn.EXPECT().AgentBroadcastTarget("ctwo", "stub").Return("ctwo.stub")
   228  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "", gomock.Any()).Return(nil).AnyTimes()
   229  			conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo.stub", "ctwo.stub", "", gomock.Any()).Return(nil).AnyTimes()
   230  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   231  
   232  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   233  			Expect(err).ToNot(HaveOccurred())
   234  
   235  			a, ok := mgr.Get("stub")
   236  			Expect(ok).To(BeTrue())
   237  			Expect(a).To(Equal(agent))
   238  
   239  			Expect(mgr.agents).To(HaveKey("stub"))
   240  			Expect(mgr.subs).To(HaveKey("stub"))
   241  			Expect(mgr.subs["stub"]).To(HaveLen(2))
   242  
   243  			conn.EXPECT().Unsubscribe("cone.stub").Return(nil)
   244  			conn.EXPECT().Unsubscribe("ctwo.stub").Return(fmt.Errorf("fail"))
   245  			Expect(mgr.UnregisterAgent("stub", conn)).To(Succeed())
   246  			Expect(mgr.agents).ToNot(HaveKey("stub"))
   247  			Expect(mgr.subs).ToNot(HaveKey("stub"))
   248  		})
   249  	})
   250  
   251  	Describe("ReplaceAgent", func() {
   252  		var oa *MockAgent
   253  		BeforeEach(func() {
   254  			oa = NewMockAgent(mockctl)
   255  			oa.EXPECT().Metadata().Return(&Metadata{
   256  				Author:      "stub@example.net",
   257  				Description: "Stub Agent",
   258  				License:     "Apache-2.0",
   259  				Name:        "stub_agent",
   260  				Timeout:     10,
   261  				URL:         "https://choria.io/",
   262  				Version:     "0.99.0",
   263  			}).AnyTimes()
   264  		})
   265  
   266  		It("Should validate agents", func() {
   267  			agent.Metadata().Timeout = 0
   268  			Expect(mgr.ReplaceAgent("testing", agent)).To(MatchError("invalid agent: timeout < 1"))
   269  		})
   270  
   271  		It("Should only replace agents with active agents", func() {
   272  			agent.EXPECT().ShouldActivate().Return(false).Times(1)
   273  			Expect(mgr.ReplaceAgent("testing", agent)).To(MatchError("replacement agent is not activating due to activation checks"))
   274  		})
   275  
   276  		It("Should reject replacements of unknown agents", func() {
   277  			agent.EXPECT().ShouldActivate().Return(true).Times(1)
   278  			Expect(mgr.ReplaceAgent("testing", agent)).To(MatchError("agent \"testing\" is not currently known"))
   279  		})
   280  
   281  		It("Should not allow the service property to be changed", func() {
   282  			oa.Metadata().Service = true
   283  			mgr.agents["testing"] = oa
   284  			agent.EXPECT().ShouldActivate().Return(true).Times(1)
   285  			Expect(mgr.ReplaceAgent("testing", agent)).To(MatchError("replacement agent cannot change service property"))
   286  		})
   287  
   288  		It("Should replace the agent", func() {
   289  			mgr.agents["testing"] = oa
   290  			agent.EXPECT().ShouldActivate().Return(true).Times(1)
   291  			Expect(mgr.agents["testing"]).To(Equal(oa))
   292  			Expect(mgr.ReplaceAgent("testing", agent)).To(Succeed())
   293  			Expect(mgr.agents["testing"]).To(Equal(agent))
   294  		})
   295  	})
   296  
   297  	Describe("KnownAgents", func() {
   298  		It("Should report on all the known agents", func() {
   299  			for _, a := range []string{"stub1", "stub2", "stub3"} {
   300  				conn.EXPECT().AgentBroadcastTarget("cone", a).Return("cone." + a)
   301  				conn.EXPECT().AgentBroadcastTarget("ctwo", a).Return("ctwo." + a)
   302  				conn.EXPECT().QueueSubscribe(gomock.Any(), "cone."+a, "cone."+a, "", gomock.Any()).Return(nil).AnyTimes()
   303  				conn.EXPECT().QueueSubscribe(gomock.Any(), "ctwo."+a, "ctwo."+a, "", gomock.Any()).Return(nil).AnyTimes()
   304  			}
   305  
   306  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   307  			err := mgr.RegisterAgent(ctx, "stub1", agent, conn)
   308  			Expect(err).ToNot(HaveOccurred())
   309  			err = mgr.RegisterAgent(ctx, "stub2", agent, conn)
   310  			Expect(err).ToNot(HaveOccurred())
   311  			err = mgr.RegisterAgent(ctx, "stub3", agent, conn)
   312  			Expect(err).ToNot(HaveOccurred())
   313  
   314  			Expect(mgr.KnownAgents()).To(Equal([]string{"stub1", "stub2", "stub3"}))
   315  		})
   316  	})
   317  
   318  	Describe("Dispatch", func() {
   319  		var request protocol.Request
   320  		var msg inter.Message
   321  		var err error
   322  		wg := &sync.WaitGroup{}
   323  
   324  		BeforeEach(func() {
   325  			cfg.Collectives = []string{"cone"}
   326  			request, err = v1.NewRequest("stub", "example.net", "choria=rip.mcollective", 60, "123", "cone")
   327  			Expect(err).ToNot(HaveOccurred())
   328  			request.SetMessage([]byte("hello world"))
   329  
   330  			msg, err = message.NewMessageFromRequest(request, "choria.reply.to", mgr.fw)
   331  			Expect(err).ToNot(HaveOccurred())
   332  			agent.EXPECT().ShouldActivate().Return(true).AnyTimes()
   333  			conn.EXPECT().AgentBroadcastTarget("cone", "stub").Return("cone.stub").AnyTimes()
   334  			conn.EXPECT().QueueSubscribe(gomock.Any(), "cone.stub", "cone.stub", "", gomock.Any()).Return(nil).AnyTimes()
   335  		})
   336  
   337  		It("Should handle unknown agents", func() {
   338  			replyc := make(chan *AgentReply, 1)
   339  
   340  			wg.Add(1)
   341  			mgr.Dispatch(ctx, wg, replyc, msg, request)
   342  
   343  			var reply *AgentReply
   344  
   345  			select {
   346  			case reply = <-replyc:
   347  			default:
   348  				reply = nil
   349  			}
   350  
   351  			Expect(reply).To(BeNil())
   352  		})
   353  
   354  		It("Should handle replies correctly", func() {
   355  			wg.Add(1)
   356  
   357  			agent.Metadata().Timeout = 1
   358  
   359  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   360  			Expect(err).ToNot(HaveOccurred())
   361  
   362  			replyc := make(chan *AgentReply, 1)
   363  			mgr.Dispatch(ctx, wg, replyc, msg, request)
   364  
   365  			reply := <-replyc
   366  
   367  			Expect(reply.Body).To(Equal([]byte("pong hello world")))
   368  		})
   369  
   370  		It("Should finish when the context is canceled", func() {
   371  			wg.Add(1)
   372  
   373  			agent.Metadata().Timeout = 10
   374  
   375  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   376  			Expect(err).ToNot(HaveOccurred())
   377  
   378  			msg.SetPayload([]byte("sleep"))
   379  			replyc := make(chan *AgentReply, 1)
   380  			go func() {
   381  				defer GinkgoRecover()
   382  				mgr.Dispatch(ctx, wg, replyc, msg, request)
   383  			}()
   384  
   385  			cancel()
   386  
   387  			reply := <-replyc
   388  
   389  			Expect(reply.Error.Error()).To(MatchRegexp("exiting on interrupt"))
   390  		})
   391  
   392  		It("Should finish on timeout", func() {
   393  			wg.Add(1)
   394  
   395  			agent.Metadata().Timeout = 1
   396  
   397  			err := mgr.RegisterAgent(ctx, "stub", agent, conn)
   398  			Expect(err).ToNot(HaveOccurred())
   399  
   400  			msg.SetPayload([]byte("sleep"))
   401  			replyc := make(chan *AgentReply, 1)
   402  			go func() {
   403  				defer GinkgoRecover()
   404  				mgr.Dispatch(ctx, wg, replyc, msg, request)
   405  			}()
   406  
   407  			reply := <-replyc
   408  
   409  			Expect(reply.Error.Error()).To(MatchRegexp("exiting on 1s timeout"))
   410  		})
   411  	})
   412  })