fleet/server/service/orbit_client_test.go
Dante Catalfamo 643fc8314b
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.

Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.

The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.

Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.

This is the old `GetConfig()` interface:

```go
type OrbitConfigFetcher interface {
	GetConfig() (*fleet.OrbitConfig, error)
}
```

This is the new `OrbitConfigReceiver` interface:

```go
type OrbitConfigReceiver interface {
	Run(*OrbitConfig) error
}
```

To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.

```go
orbitClient.RegisterConfigReceiver(extRunner)
```

Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series,  one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain

Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain

This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)

Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet

Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
  - The application is about to restart
  - We can set a timeout for how long receivers are allowed to take

Closes #12662

---------

Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 15:22:56 -04:00

183 lines
4.4 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"reflect"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)
func TestGetConfig(t *testing.T) {
t.Run(
"config cache", func(t *testing.T) {
oc := OrbitClient{}
oc.configCache.config = &fleet.OrbitConfig{}
oc.configCache.lastUpdated = time.Now().Add(1 * time.Second)
config, err := oc.GetConfig()
require.NoError(t, err)
require.Equal(t, oc.configCache.config, config)
},
)
t.Run(
"config cache error", func(t *testing.T) {
oc := OrbitClient{}
oc.configCache.config = nil
oc.configCache.err = errors.New("test error")
oc.configCache.lastUpdated = time.Now().Add(1 * time.Second)
config, err := oc.GetConfig()
require.Error(t, err)
require.Equal(t, oc.configCache.config, config)
},
)
}
func clientWithConfig(cfg *fleet.OrbitConfig) *OrbitClient {
ctx, cancel := context.WithCancel(context.Background())
oc := &OrbitClient{
ReceiverUpdateContext: ctx,
ReceiverUpdateCancelFunc: cancel,
}
oc.configCache.config = cfg
oc.configCache.lastUpdated = time.Now().Add(1 * time.Hour)
return oc
}
func TestConfigReceiverCalls(t *testing.T) {
var called1, called2 bool
testmsg := json.RawMessage("testing")
rfunc1 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
if !reflect.DeepEqual(cfg.Flags, testmsg) {
return errors.New("not equal testmsg")
}
called1 = true
return nil
})
rfunc2 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
if !reflect.DeepEqual(cfg.Flags, testmsg) {
return errors.New("not equal testmsg")
}
called2 = true
return nil
})
client := clientWithConfig(&fleet.OrbitConfig{Flags: testmsg})
client.RegisterConfigReceiver(rfunc1)
client.RegisterConfigReceiver(rfunc2)
err := client.RunConfigReceivers()
require.NoError(t, err)
require.True(t, called1)
require.True(t, called2)
}
func TestConfigReceiverErrors(t *testing.T) {
var called1, called2 bool
rfunc1 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
called1 = true
return nil
})
rfunc2 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
called2 = true
return nil
})
err1 := errors.New("error1")
err2 := errors.New("error2")
efunc1 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
return err1
})
efunc2 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
return err2
})
// Make sure we don't get stuck or crash on receiver panic
pfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
panic("woah")
})
client := clientWithConfig(&fleet.OrbitConfig{})
client.RegisterConfigReceiver(efunc1)
client.RegisterConfigReceiver(rfunc1)
client.RegisterConfigReceiver(efunc2)
client.RegisterConfigReceiver(rfunc2)
client.RegisterConfigReceiver(pfunc)
err := client.RunConfigReceivers()
require.ErrorIs(t, err, err1)
require.ErrorIs(t, err, err2)
require.True(t, called1)
require.True(t, called2)
}
func TestExecuteConfigReceiversCancel(t *testing.T) {
client := clientWithConfig(&fleet.OrbitConfig{})
client.ReceiverUpdateInterval = 100 * time.Millisecond
var calls1, calls2 int
requiredCalls := 4
cfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
calls1++
if calls1 == requiredCalls {
client.ReceiverUpdateCancelFunc()
}
return nil
})
rfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
calls2++
return nil
})
client.RegisterConfigReceiver(cfunc)
client.RegisterConfigReceiver(rfunc)
err := client.ExecuteConfigReceivers()
require.Nil(t, err)
require.Equal(t, requiredCalls, calls1)
require.Equal(t, requiredCalls, calls2)
}
func TestExecuteConfigReceiversInterrupt(t *testing.T) {
client := clientWithConfig(&fleet.OrbitConfig{})
client.ReceiverUpdateInterval = 200 * time.Millisecond
var called bool
rfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
called = true
return nil
})
client.RegisterConfigReceiver(rfunc)
finChan := make(chan error, 1)
go func() {
finChan <- client.ExecuteConfigReceivers()
}()
go func() {
time.Sleep(200 * time.Millisecond)
client.ReceiverUpdateCancelFunc()
}()
select {
case err := <-finChan:
require.Nil(t, err)
require.True(t, called)
case <-time.NewTimer(2 * time.Second).C:
require.Fail(t, "receiver interrupt cancel didn't work")
}
client.ReceiverUpdateCancelFunc()
}