mirror of
https://github.com/weaveworks/scope.git
synced 2026-02-14 18:09:59 +00:00
844 lines
23 KiB
Go
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)
|
|
}
|
|
}
|