github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/external/agent_test.go (about) 1 // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package external 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "os" 12 "path/filepath" 13 "runtime" 14 "testing" 15 16 "github.com/choria-io/go-choria/build" 17 "github.com/choria-io/go-choria/choria" 18 "github.com/choria-io/go-choria/config" 19 "github.com/choria-io/go-choria/providers/agent/mcorpc" 20 addl "github.com/choria-io/go-choria/providers/agent/mcorpc/ddl/agent" 21 "github.com/choria-io/go-choria/providers/agent/mcorpc/ddl/common" 22 "github.com/choria-io/go-choria/server/agents" 23 "github.com/golang/mock/gomock" 24 . "github.com/onsi/ginkgo/v2" 25 . "github.com/onsi/gomega" 26 ) 27 28 func Test(t *testing.T) { 29 RegisterFailHandler(Fail) 30 RunSpecs(t, "Providers/Agent/McoRPC/External") 31 } 32 33 var _ = Describe("McoRPC/External", func() { 34 var ( 35 mockctl *gomock.Controller 36 agentMgr *MockAgentManager 37 cfg *config.Config 38 prov *Provider 39 si *MockServerInfoSource 40 err error 41 wd string 42 ) 43 44 BeforeEach(func() { 45 build.TLS = "false" 46 47 mockctl = gomock.NewController(GinkgoT()) 48 agentMgr = NewMockAgentManager(mockctl) 49 si = NewMockServerInfoSource(mockctl) 50 51 cfg = config.NewConfigForTests() 52 cfg.DisableSecurityProviderVerify = true 53 54 wd, err = os.Getwd() 55 Expect(err).ToNot(HaveOccurred()) 56 57 cfg.Choria.RubyLibdir = []string{filepath.Join(wd, "testdata")} 58 59 fw, err := choria.NewWithConfig(cfg) 60 Expect(err).ToNot(HaveOccurred()) 61 fw.SetLogWriter(GinkgoWriter) 62 63 agentMgr.EXPECT().Choria().Return(fw).AnyTimes() 64 agentMgr.EXPECT().Logger().Return(fw.Logger("mgr")).AnyTimes() 65 si.EXPECT().Facts().Return(json.RawMessage(`{"ginkgo":true}`)).AnyTimes() 66 67 prov = &Provider{ 68 cfg: cfg, 69 log: fw.Logger("ginkgo"), 70 agents: []*addl.DDL{}, 71 paths: make(map[string]string), 72 } 73 }) 74 75 AfterEach(func() { 76 mockctl.Finish() 77 }) 78 79 Describe("newExternalAgent", func() { 80 var ( 81 ddl *addl.DDL 82 ) 83 84 BeforeEach(func() { 85 ddl = &addl.DDL{ 86 SourceLocation: filepath.Join(wd, "testdata/mcollective/agent/ginkgo.json"), 87 Metadata: &agents.Metadata{ 88 Name: "ginkgo", 89 Timeout: 1, 90 }, 91 Actions: []*addl.Action{ 92 {Name: "act1"}, 93 {Name: "act2"}, 94 }, 95 } 96 }) 97 98 It("Should load all the actions", func() { 99 agent, err := prov.newExternalAgent(ddl, agentMgr) 100 Expect(err).ToNot(HaveOccurred()) 101 Expect(agent.ActionNames()).To(Equal([]string{"act1", "act2"})) 102 }) 103 }) 104 105 Describe("agentPath", func() { 106 It("Should support the basic agent path to a single file", func() { 107 dir := filepath.Join(wd, "testdata/mcollective/agent/ginkgo.json") 108 Expect(prov.agentPath("ginkgo", filepath.Join(wd, "testdata/mcollective/agent/ginkgo.json"))).To(Equal(filepath.Join(filepath.Dir(dir), "ginkgo"))) 109 }) 110 111 It("Should support the basic os and arch aware agent paths", func() { 112 td, err := os.MkdirTemp("", "") 113 Expect(err).ToNot(HaveOccurred()) 114 defer os.RemoveAll(td) 115 116 dir := filepath.Join(td, "na") 117 Expect(os.MkdirAll(dir, 0744)).To(Succeed()) 118 119 path := prov.agentPath("na", dir) 120 expected := filepath.Join(dir, fmt.Sprintf("na-%s_%s", runtime.GOOS, runtime.GOARCH)) 121 Expect(path).To(Equal(expected)) 122 }) 123 }) 124 125 Describe("externalActivationCheck", func() { 126 It("should handle non 0 exit code checks", func() { 127 d := &addl.DDL{ 128 SourceLocation: filepath.Join(wd, "testdata/mcollective/agent/activation_checker_enabled.json"), 129 Metadata: &agents.Metadata{Name: "activation_checker_fails"}, 130 } 131 c, err := prov.externalActivationCheck(d) 132 Expect(err).ToNot(HaveOccurred()) 133 Expect(c()).To(BeFalse()) 134 }) 135 136 It("should handle specifically disabled agents", func() { 137 d := &addl.DDL{ 138 SourceLocation: filepath.Join(wd, "testdata/mcollective/agent/activation_checker_enabled.json"), 139 Metadata: &agents.Metadata{Name: "activation_checker_disabled"}, 140 } 141 c, err := prov.externalActivationCheck(d) 142 Expect(err).ToNot(HaveOccurred()) 143 Expect(c()).To(BeFalse()) 144 }) 145 146 It("should handle specifically enabled agents", func() { 147 if runtime.GOOS == "windows" { 148 Skip("Windows TODO") 149 } 150 151 d := &addl.DDL{ 152 SourceLocation: filepath.Join(wd, "testdata/mcollective/agent/activation_checker_enabled.json"), 153 Metadata: &agents.Metadata{Name: "activation_checker_enabled"}, 154 } 155 c, err := prov.externalActivationCheck(d) 156 Expect(err).ToNot(HaveOccurred()) 157 Expect(c()).To(BeTrue()) 158 }) 159 }) 160 161 Describe("externalAction", func() { 162 var ( 163 ddl *addl.DDL 164 agent *mcorpc.Agent 165 ) 166 167 BeforeEach(func() { 168 ddl = &addl.DDL{ 169 SourceLocation: filepath.Join(wd, "testdata/mcollective/agent/activation_checker_enabled.json"), 170 Metadata: &agents.Metadata{ 171 Name: "ginkgo", 172 Timeout: 1, 173 }, 174 Actions: []*addl.Action{ 175 { 176 Name: "ping", 177 Input: map[string]*common.InputItem{ 178 "hello": { 179 Type: "string", 180 Optional: false, 181 Validation: "shellsafe", 182 MaxLength: 0, 183 }, 184 }, 185 Output: map[string]*common.OutputItem{ 186 "hello": { 187 Type: "string", 188 Default: "default", 189 }, 190 "optional": { 191 Type: "string", 192 Default: "optional default", 193 }, 194 }, 195 }, 196 }, 197 } 198 prov.agents = append(prov.agents, ddl) 199 prov.paths["ginkgo"] = ddl.SourceLocation 200 201 agent, err = prov.newExternalAgent(ddl, agentMgr) 202 agent.SetServerInfo(si) 203 204 Expect(err).ToNot(HaveOccurred()) 205 }) 206 207 It("Should handle a missing executable", func() { 208 ctx, cancel := context.WithCancel(context.Background()) 209 defer cancel() 210 211 prov.paths["ginkgo_missing"] = ddl.SourceLocation 212 ddl.Metadata.Name = "ginkgo_missing" 213 rep := &mcorpc.Reply{} 214 req := &mcorpc.Request{ 215 Agent: "ginkgo_missing", 216 Action: "ping", 217 Data: json.RawMessage(`{"hello":"world"}`), 218 } 219 220 prov.externalAction(ctx, req, rep, agent, nil) 221 Expect(rep.Statusmsg).To(MatchRegexp("Cannot call.+ginkgo_missing#ping.+agent executable was not found")) 222 Expect(rep.Statuscode).To(Equal(mcorpc.Aborted)) 223 }) 224 225 It("Should handle execution failures", func() { 226 if runtime.GOOS == "windows" { 227 Skip("Windows TODO") 228 } 229 230 ctx, cancel := context.WithCancel(context.Background()) 231 defer cancel() 232 233 prov.paths["ginkgo_abort"] = ddl.SourceLocation 234 ddl.Metadata.Name = "ginkgo_abort" 235 rep := &mcorpc.Reply{} 236 req := &mcorpc.Request{ 237 Agent: "ginkgo_abort", 238 Action: "ping", 239 Data: json.RawMessage(`{"hello":"world"}`), 240 } 241 242 prov.externalAction(ctx, req, rep, agent, nil) 243 Expect(rep.Statusmsg).To(MatchRegexp("Could not call.+ginkgo_abort#ping.+exit status 1")) 244 Expect(rep.Statuscode).To(Equal(mcorpc.Aborted)) 245 }) 246 247 It("Should validate the input before executing the agent", func() { 248 ctx, cancel := context.WithCancel(context.Background()) 249 defer cancel() 250 251 prov.paths["ginkgo_abort"] = ddl.SourceLocation 252 ddl.Metadata.Name = "ginkgo_abort" 253 rep := &mcorpc.Reply{} 254 req := &mcorpc.Request{ 255 Agent: "ginkgo_abort", 256 Action: "ping", 257 Data: json.RawMessage(`{"hello":1}`), 258 } 259 260 prov.externalAction(ctx, req, rep, agent, nil) 261 Expect(rep.Statusmsg).To(MatchRegexp("Validation failed: validation failed for input 'hello': is not a string")) 262 Expect(rep.Statuscode).To(Equal(mcorpc.Aborted)) 263 }) 264 265 It("Should execute the correct request binary with the correct input and set defaults on the reply", func() { 266 if runtime.GOOS == "windows" { 267 Skip("Windows TODO") 268 } 269 270 ctx, cancel := context.WithCancel(context.Background()) 271 defer cancel() 272 273 rep := &mcorpc.Reply{} 274 req := &mcorpc.Request{ 275 Agent: "ginkgo", 276 Action: "ping", 277 Data: json.RawMessage(`{"hello":"world"}`), 278 } 279 280 prov.externalAction(ctx, req, rep, agent, nil) 281 Expect(rep.Statusmsg).To(Equal("OK")) 282 Expect(rep.Statuscode).To(Equal(mcorpc.OK)) 283 Expect(rep.Data.(map[string]any)["hello"].(string)).To(Equal("world")) 284 Expect(rep.Data.(map[string]any)["optional"].(string)).To(Equal("optional default")) 285 }) 286 }) 287 })