Files
weave-scope/probe/plugins/registry_internal_test.go
Krzesimir Nowak 9e092f1a4a Switch to LatestMap-style node controls
This allows plugins to add controls to nodes that already have some
controls set by other plugin. Previously only the last plugin that
sets the controls in the node would have its controls visible. That
was because of NodeControls' Merge function that actually weren't
merging data from two inputs, but rather returning data that was newer
and discarding the older one.
2016-08-12 17:15:43 +02:00

848 lines
23 KiB
Go

package plugins
import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"path/filepath"
"sort"
"sync"
"syscall"
"testing"
"time"
"github.com/paypal/ionet"
"github.com/ugorji/go/codec"
fs_hook "github.com/weaveworks/scope/common/fs"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/probe/controls"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
"github.com/weaveworks/scope/test/fs"
"github.com/weaveworks/scope/test/reflect"
)
func testRegistry(t *testing.T, apiVersion string) *Registry {
handlerRegistry := controls.NewDefaultHandlerRegistry()
root := "/plugins"
r, err := NewRegistry(root, apiVersion, nil, handlerRegistry, nil)
if err != nil {
t.Fatal(err)
}
return r
}
func stubTransport(fn func(socket string, timeout time.Duration) (http.RoundTripper, error)) {
transport = fn
}
func restoreTransport() { transport = makeUnixRoundTripper }
type readWriteCloseRoundTripper struct {
io.ReadWriteCloser
}
func (rwc readWriteCloseRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
conn := &closeableConn{
Conn: &ionet.Conn{R: rwc, W: rwc},
Closer: rwc,
}
client := httputil.NewClientConn(conn, nil)
defer client.Close()
return client.Do(req)
}
// closeableConn gives us an overrideable Close, where ionet.Conn does not.
type closeableConn struct {
net.Conn
io.Closer
}
func (c *closeableConn) Close() error {
c.Conn.Close()
return c.Closer.Close()
}
type mockPlugin struct {
t *testing.T
Name string
Handler http.Handler
}
func (p mockPlugin) dir() string {
return "/plugins"
}
func (p mockPlugin) path() string {
return filepath.Join(p.dir(), p.base())
}
func (p mockPlugin) base() string {
return p.Name + ".sock"
}
func (p mockPlugin) file() fs.File {
incomingR, incomingW := io.Pipe()
outgoingR, outgoingW := io.Pipe()
// TODO: This is a terrible hack of a little http server. Really, we should
// implement some sort of fs.File -> net.Listener bridge and run an net/http
// server on that.
go func() {
for {
conn := httputil.NewServerConn(&ionet.Conn{R: incomingR, W: outgoingW}, nil)
req, err := conn.Read()
if err == io.EOF {
outgoingW.Close()
return
} else if err != nil {
p.t.Fatal(err)
}
resp := httptest.NewRecorder()
p.Handler.ServeHTTP(resp, req)
fmt.Fprintf(outgoingW, "HTTP/1.1 %d %s\nContent-Length: %d\n\n%s", resp.Code, http.StatusText(resp.Code), resp.Body.Len(), resp.Body.String())
}
}()
return fs.File{
FName: p.base(),
FWriter: incomingW,
FReader: outgoingR,
FStat: syscall.Stat_t{Mode: syscall.S_IFSOCK},
}
}
type chanWriter chan []byte
func (w chanWriter) Write(p []byte) (int, error) {
w <- p
return len(p), nil
}
func (w chanWriter) Close() error {
close(w)
return nil
}
func setup(t *testing.T, sockets ...fs.Entry) fs.Entry {
mockFS := fs.Dir("", fs.Dir("plugins", sockets...))
fs_hook.Mock(
mockFS)
stubTransport(func(socket string, timeout time.Duration) (http.RoundTripper, error) {
f, err := mockFS.Open(socket)
return readWriteCloseRoundTripper{f}, err
})
return mockFS
}
func restore(t *testing.T) {
fs_hook.Restore()
restoreTransport()
}
type iterator func(func(*Plugin))
func checkLoadedPlugins(t *testing.T, forEach iterator, expected []xfer.PluginSpec) {
var plugins []xfer.PluginSpec
forEach(func(p *Plugin) {
plugins = append(plugins, p.PluginSpec)
})
sort.Sort(xfer.PluginSpecsByID(plugins))
if !reflect.DeepEqual(plugins, expected) {
t.Fatalf(test.Diff(expected, plugins))
}
}
func checkLoadedPluginIDs(t *testing.T, forEach iterator, expectedIDs []string) {
var pluginIDs []string
forEach(func(p *Plugin) {
pluginIDs = append(pluginIDs, p.ID)
})
sort.Strings(pluginIDs)
if len(pluginIDs) != len(expectedIDs) {
t.Fatalf("Expected plugins %q, got: %q", expectedIDs, pluginIDs)
}
for i, id := range pluginIDs {
if id != expectedIDs[i] {
t.Fatalf("Expected plugins %q, got: %q", expectedIDs, pluginIDs)
}
}
}
type testResponse struct {
Status int
Body string
}
type testResponseMap map[string]testResponse
// mapStringHandler returns an http.Handler which just prints the given string for each path
func mapStringHandler(responses testResponseMap) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if response, found := responses[r.URL.Path]; found {
w.WriteHeader(response.Status)
fmt.Fprint(w, response.Body)
} else {
http.NotFound(w, r)
}
})
}
// stringHandler returns an http.Handler which just prints the given string
func stringHandler(status int, j string) http.Handler {
return mapStringHandler(testResponseMap{"/report": {status, j}})
}
type testHandlerRegistryBackend struct {
handlers map[string]xfer.ControlHandlerFunc
t *testing.T
mtx sync.Mutex
}
func newTestHandlerRegistryBackend(t *testing.T) *testHandlerRegistryBackend {
return &testHandlerRegistryBackend{
handlers: map[string]xfer.ControlHandlerFunc{},
t: t,
}
}
// Lock locks the backend, so the batch insertions or removals can be
// performed.
func (b *testHandlerRegistryBackend) Lock() {
b.mtx.Lock()
}
// Unlock unlocks the backend.
func (b *testHandlerRegistryBackend) Unlock() {
b.mtx.Unlock()
}
// Register a new control handler under a given id.
func (b *testHandlerRegistryBackend) Register(control string, f xfer.ControlHandlerFunc) {
b.handlers[control] = f
}
// Rm deletes the handler for a given name.
func (b *testHandlerRegistryBackend) Rm(control string) {
delete(b.handlers, control)
}
// Handler gets the handler for the given id.
func (b *testHandlerRegistryBackend) Handler(control string) (xfer.ControlHandlerFunc, bool) {
handler, ok := b.handlers[control]
return handler, ok
}
func TestRegistryLoadsExistingPlugins(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
)
defer restore(t)
r := testRegistry(t, "1")
defer r.Close()
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{"testPlugin"})
}
func TestRegistryLoadsExistingPluginsEvenWhenOneFails(t *testing.T) {
setup(
t,
// TODO: This first one needs to fail
fs.Dir("fail",
mockPlugin{
t: t,
Name: "aFailure",
Handler: stringHandler(http.StatusInternalServerError, `Internal Server Error`),
}.file(),
),
mockPlugin{
t: t,
Name: "testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
)
defer restore(t)
r := testRegistry(t, "1")
defer r.Close()
r.Report()
checkLoadedPlugins(t, r.ForEach, []xfer.PluginSpec{
{
ID: "aFailure",
Label: "aFailure",
Status: "error: plugin returned non-200 status code: 500 Internal Server Error",
},
{
ID: "testPlugin",
Label: "testPlugin",
Interfaces: []string{"reporter"},
APIVersion: "1",
Status: "ok",
},
})
}
func TestRegistryDiscoversNewPlugins(t *testing.T) {
mockFS := setup(t)
defer restore(t)
r := testRegistry(t, "")
defer r.Close()
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{})
// Add the new plugin
plugin := mockPlugin{
t: t,
Name: "testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin","label":"testPlugin","interfaces":["reporter"]}]}`),
}
mockFS.Add(plugin.dir(), plugin.file())
if err := r.scan(); err != nil {
t.Fatal(err)
}
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{"testPlugin"})
}
func TestRegistryRemovesPlugins(t *testing.T) {
plugin := mockPlugin{
t: t,
Name: "testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin","label":"testPlugin","interfaces":["reporter"]}]}`),
}
mockFS := setup(t, plugin.file())
defer restore(t)
r := testRegistry(t, "")
defer r.Close()
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{"testPlugin"})
// Remove the plugin
mockFS.Remove(plugin.path())
if err := r.scan(); err != nil {
t.Fatal(err)
}
checkLoadedPluginIDs(t, r.ForEach, []string{})
}
func TestRegistryUpdatesPluginsWhenTheyChange(t *testing.T) {
resp := `{"Plugins":[{"id":"testPlugin","label":"testPlugin","interfaces":["reporter"]}]}`
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, resp)
})
plugin := mockPlugin{
t: t,
Name: "testPlugin",
Handler: handler,
}
setup(t, plugin.file())
defer restore(t)
r := testRegistry(t, "")
defer r.Close()
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{"testPlugin"})
// Update the plugin. Just change what the handler will respond with.
resp = `{"Plugins":[{"id":"testPlugin","label":"updatedPlugin","interfaces":["reporter"]}]}`
r.Report()
checkLoadedPlugins(t, r.ForEach, []xfer.PluginSpec{
{
ID: "testPlugin",
Label: "updatedPlugin",
Interfaces: []string{"reporter"},
Status: "ok",
},
})
}
func TestRegistryReturnsPluginsByInterface(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "plugin1",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"plugin1","label":"plugin1","interfaces":["reporter"]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "plugin2",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"plugin2","label":"plugin2","interfaces":["other"]}]}`),
}.file(),
)
defer restore(t)
r := testRegistry(t, "")
defer r.Close()
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{"plugin1", "plugin2"})
checkLoadedPluginIDs(t, func(fn func(*Plugin)) { r.Implementers("reporter", fn) }, []string{"plugin1"})
checkLoadedPluginIDs(t, func(fn func(*Plugin)) { r.Implementers("other", fn) }, []string{"plugin2"})
}
func TestRegistryHandlesConflictingPlugins(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "plugin1",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"plugin1","label":"plugin1","interfaces":["reporter"]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "plugin1",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"plugin1","label":"plugin2","interfaces":["reporter","other"]}]}`),
}.file(),
)
defer restore(t)
r := testRegistry(t, "")
defer r.Close()
r.Report()
// Should just have the second one (we just log conflicts)
checkLoadedPluginIDs(t, r.ForEach, []string{"plugin1"})
checkLoadedPluginIDs(t, func(fn func(*Plugin)) { r.Implementers("other", fn) }, []string{"plugin1"})
}
func TestRegistryRejectsErroneousPluginResponses(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "okPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"okPlugin","label":"okPlugin","interfaces":["reporter"]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "non200ResponseCode",
Handler: stringHandler(http.StatusInternalServerError, `{"Plugins":[{"id":"non200ResponseCode","label":"non200ResponseCode","interfaces":["reporter"]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "noLabel",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"noLabel","interfaces":["reporter"]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "noInterface",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"noInterface","label":"noInterface","interfaces":[]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "wrongApiVersion",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"wrongApiVersion","label":"wrongApiVersion","interfaces":["reporter"],"api_version":"foo"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "nonJSONResponseBody",
Handler: stringHandler(http.StatusOK, `notJSON`),
}.file(),
mockPlugin{
t: t,
Name: "changedID",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"differentID","label":"changedID","interfaces":["reporter"]}]}`),
}.file(),
mockPlugin{
t: t,
Name: "moreThanOnePlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"moreThanOnePlugin","label":"moreThanOnePlugin","interfaces":["reporter"]}, {"id":"haha","label":"haha","interfaces":["reporter"]}]}`),
}.file(),
)
defer restore(t)
r := testRegistry(t, "")
defer r.Close()
r.Report()
checkLoadedPlugins(t, r.ForEach, []xfer.PluginSpec{
{
ID: "changedID",
Label: "changedID",
Status: `error: plugin must not change its id (is "differentID", should be "changedID")`,
},
{
ID: "moreThanOnePlugin",
Label: "moreThanOnePlugin",
Status: `error: report must contain exactly one plugin (found 2)`,
},
{
ID: "noInterface",
Label: "noInterface",
Interfaces: []string{},
Status: `error: spec must implement the "reporter" interface`,
},
{
ID: "noLabel",
Interfaces: []string{"reporter"},
Status: `error: spec must contain a label`,
},
{
ID: "non200ResponseCode",
Label: "non200ResponseCode",
Status: "error: plugin returned non-200 status code: 500 Internal Server Error",
},
{
ID: "nonJSONResponseBody",
Label: "nonJSONResponseBody",
Status: "error: decoding error: [pos 4]: json: expecting ull: got otJ",
},
{
ID: "okPlugin",
Label: "okPlugin",
Interfaces: []string{"reporter"},
Status: `ok`,
},
{
ID: "wrongApiVersion",
Label: "wrongApiVersion",
Interfaces: []string{"reporter"},
APIVersion: "foo",
Status: `error: incorrect API version: expected "", got "foo"`,
},
})
}
func TestRegistryRejectsPluginResponsesWhichAreTooLarge(t *testing.T) {
description := ""
for i := 0; i < 129; i++ {
description += "a"
}
response := fmt.Sprintf(
`{
"Plugins": [
{
"id": "foo",
"label": "foo",
"description": %q,
"interfaces": ["reporter"]
}
]
}`,
description,
)
setup(t, mockPlugin{t: t, Name: "foo", Handler: stringHandler(http.StatusOK, response)}.file())
oldMaxResponseBytes := maxResponseBytes
maxResponseBytes = 128
defer func() {
maxResponseBytes = oldMaxResponseBytes
restore(t)
}()
r := testRegistry(t, "")
defer r.Close()
r.Report()
checkLoadedPlugins(t, r.ForEach, []xfer.PluginSpec{
{ID: "foo", Label: "foo", Status: `error: response must be shorter than 50MB`},
})
}
func TestRegistryChecksForValidPluginIDs(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "P-L-U-G-I-N",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"P-L-U-G-I-N","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "another-testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"another-testPlugin","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "testPlugin!",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin!","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "test~plugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"test~plugin","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "testPlugin-",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"testPlugin-","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
mockPlugin{
t: t,
Name: "-testPlugin",
Handler: stringHandler(http.StatusOK, `{"Plugins":[{"id":"-testPlugin","label":"testPlugin","interfaces":["reporter"],"api_version":"1"}]}`),
}.file(),
)
defer restore(t)
r := testRegistry(t, "1")
defer r.Close()
r.Report()
checkLoadedPluginIDs(t, r.ForEach, []string{"P-L-U-G-I-N", "another-testPlugin", "testPlugin"})
}
func checkControls(t *testing.T, topology report.Topology, expectedControls, expectedNodeControls []string, nodeID string) {
controlsSet := report.MakeStringSet(expectedControls...)
for _, id := range controlsSet {
control, found := topology.Controls[id]
if !found {
t.Fatalf("Could not find an expected control %s in topology %s", id, topology.Label)
}
if control.ID != id {
t.Fatalf("Control ID mismatch, expected %s, got %s", id, control.ID)
}
}
if len(controlsSet) != len(topology.Controls) {
t.Fatalf("Expected exactly %d controls in topology, got %d", len(controlsSet), len(topology.Controls))
}
node, found := topology.Nodes[nodeID]
if !found {
t.Fatalf("expected a node %s in a topology", nodeID)
}
actualNodeControls := []string{}
node.LatestControls.ForEach(func(controlID string, _ time.Time, _ report.NodeControlData) {
actualNodeControls = append(actualNodeControls, controlID)
})
nodeControlsSet := report.MakeStringSet(expectedNodeControls...)
actualNodeControlsSet := report.MakeStringSet(actualNodeControls...)
if !reflect.DeepEqual(nodeControlsSet, actualNodeControlsSet) {
t.Fatalf("node controls in node %s in topology %s are not equal:\n%s", nodeID, topology.Label, test.Diff(nodeControlsSet, actualNodeControlsSet))
}
}
func control(index int) (string, string) {
return fmt.Sprintf("ctrl%d", index), fmt.Sprintf("Ctrl %d", index)
}
func controlID(index int) string {
ID, _ := control(index)
return ID
}
func mustMarshal(value interface{}) string {
buf := &bytes.Buffer{}
codec.NewEncoder(buf, &codec.JsonHandle{}).MustEncode(value)
return buf.String()
}
func mustUnmarshal(r io.Reader, value interface{}) {
codec.NewDecoder(r, &codec.JsonHandle{}).MustDecode(value)
}
func topologyControls(indices []int) report.Controls {
var controls []report.Control
for _, index := range indices {
ID, name := control(index)
controls = append(controls, report.Control{
ID: ID,
Human: name,
Icon: "fa-at",
Rank: index,
})
}
rptControls := report.Controls{}
rptControls.AddControls(controls)
return rptControls
}
func nodeControls(indices []int) []string {
var IDs []string
for _, index := range indices {
ID, _ := control(index)
IDs = append(IDs, ID)
}
return IDs
}
func topologyWithControls(label, nodeID string, controlIndices, nodeControlIndices []int) report.Topology {
topology := report.MakeTopology().WithLabel(label, "")
topology.Controls = topologyControls(controlIndices)
return topology.AddNode(report.MakeNode(nodeID).WithLatestActiveControls(nodeControls(nodeControlIndices)...))
}
func pluginSpec(ID string, interfaces ...string) xfer.PluginSpec {
return xfer.PluginSpec{
ID: ID,
Label: ID,
Interfaces: interfaces,
APIVersion: "1",
}
}
func testReport(topology report.Topology, spec xfer.PluginSpec) report.Report {
rpt := report.MakeReport()
set := false
f := func(t *report.Topology) {
if t.Label != topology.Label {
return
}
if set {
panic("Two topologies with the same label")
}
set = true
*t = t.Merge(topology)
}
rpt.WalkTopologies(f)
if !set {
panic(fmt.Sprintf("%s name is not a valid topology label", topology.Label))
}
rpt.Plugins = xfer.MakePluginSpecs(spec)
return rpt
}
func TestRegistryRewritesControlReports(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "testPlugin",
Handler: mapStringHandler(testResponseMap{
"/report": {http.StatusOK, mustMarshal(testReport(topologyWithControls("pod", "node1", []int{1}, []int{1, 2}), pluginSpec("testPlugin", "reporter", "controller")))},
"/control": {http.StatusOK, mustMarshal(PluginResponse{})},
}),
}.file(),
mockPlugin{
t: t,
Name: "testPluginReporterOnly",
Handler: mapStringHandler(testResponseMap{
"/report": {http.StatusOK, mustMarshal(testReport(topologyWithControls("host", "node1", []int{1}, []int{1, 2}), pluginSpec("testPluginReporterOnly", "reporter")))},
}),
}.file(),
)
defer restore(t)
r := testRegistry(t, "1")
defer r.Close()
rpt, err := r.Report()
if err != nil {
t.Fatal(err)
}
// in a Pod topology, ctrl1 should be faked, ctrl2 should be left intact
expectedPodControls := []string{fakeControlID("testPlugin", controlID(1))}
expectedPodNodeControls := []string{fakeControlID("testPlugin", controlID(1)), controlID(2)}
checkControls(t, rpt.Pod, expectedPodControls, expectedPodNodeControls, "node1")
// in a Host topology, controls should be kept untouched
expectedHostControls := []string{controlID(1)}
expectedHostNodeControls := []string{controlID(1), controlID(2)}
checkControls(t, rpt.Host, expectedHostControls, expectedHostNodeControls, "node1")
}
func TestRegistryRegistersHandlers(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "testPlugin",
Handler: mapStringHandler(testResponseMap{
"/report": {http.StatusOK, mustMarshal(testReport(topologyWithControls("pod", "node1", []int{1}, []int{1, 2}), pluginSpec("testPlugin", "reporter", "controller")))},
"/control": {http.StatusOK, mustMarshal(PluginResponse{})},
}),
}.file(),
mockPlugin{
t: t,
Name: "testPlugin2",
Handler: mapStringHandler(testResponseMap{
"/report": {http.StatusOK, mustMarshal(testReport(topologyWithControls("pod", "node2", []int{1, 2}, []int{1}), pluginSpec("testPlugin2", "reporter", "controller")))},
"/control": {http.StatusOK, mustMarshal(PluginResponse{})},
}),
}.file(),
)
defer restore(t)
testBackend := newTestHandlerRegistryBackend(t)
handlerRegistry := controls.NewHandlerRegistry(testBackend)
root := "/plugins"
r, err := NewRegistry(root, "1", nil, handlerRegistry, nil)
if err != nil {
t.Fatal(err)
}
defer r.Close()
r.Report()
expectedLen := 3
if len(testBackend.handlers) != expectedLen {
t.Fatalf("Expected %d registered handler, got %d", expectedLen, len(testBackend.handlers))
}
fakeIDs := []string{
fakeControlID("testPlugin", controlID(1)),
fakeControlID("testPlugin2", controlID(1)),
fakeControlID("testPlugin2", controlID(2)),
}
for _, fakeID := range fakeIDs {
if _, found := testBackend.Handler(fakeID); !found {
t.Fatalf("Expected to have a handler for %s", fakeID)
}
}
}
func TestRegistryHandlersCallPlugins(t *testing.T) {
setup(
t,
mockPlugin{
t: t,
Name: "testPlugin",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/report":
w.WriteHeader(http.StatusOK)
rpt := mustMarshal(testReport(topologyWithControls("pod", "node1", []int{1}, []int{1}), pluginSpec("testPlugin", "reporter", "controller")))
fmt.Fprint(w, rpt)
case "/control":
xreq := xfer.Request{}
mustUnmarshal(r.Body, &xreq)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, mustMarshal(PluginResponse{Response: xfer.Response{Value: fmt.Sprintf("%s,%s", xreq.NodeID, xreq.Control)}}))
default:
http.NotFound(w, r)
}
}),
}.file(),
)
defer restore(t)
handlerRegistry := controls.NewDefaultHandlerRegistry()
root := "/plugins"
r, err := NewRegistry(root, "1", nil, handlerRegistry, nil)
if err != nil {
t.Fatal(err)
}
defer r.Close()
r.Report()
fakeID := fakeControlID("testPlugin", controlID(1))
req := xfer.Request{NodeID: "node1", Control: fakeID}
res := handlerRegistry.HandleControlRequest(req)
if res.Value != fmt.Sprintf("node1,%s", controlID(1)) {
t.Fatalf("Got unexpected response: %#v", res)
}
}