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 })