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