Files
weave-scope/probe/plugins/registry_internal_test.go
2019-11-26 11:29:42 +00:00

844 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/common/fs"
"github.com/weaveworks/common/test"
"github.com/weaveworks/common/test/fs"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/probe/controls"
"github.com/weaveworks/scope/report"
"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 := node.ActiveControls()
if !reflect.DeepEqual(expectedNodeControls, actualNodeControls) {
t.Fatalf("node controls in node %s in topology %s are not equal:\n%s", nodeID, topology.Label, test.Diff(expectedNodeControls, actualNodeControls))
}
}
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)
topology.AddNode(report.MakeNode(nodeID).WithLatestActiveControls(nodeControls(nodeControlIndices)...))
return topology
}
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)
}
}