diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go new file mode 100644 index 000000000..2b012075b --- /dev/null +++ b/pkg/manager/manager_test.go @@ -0,0 +1,309 @@ +package manager + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/docker/infrakit/pkg/discovery" + "github.com/docker/infrakit/pkg/leader" + group_mock "github.com/docker/infrakit/pkg/mock/spi/group" + store_mock "github.com/docker/infrakit/pkg/mock/store" + group_rpc "github.com/docker/infrakit/pkg/rpc/group" + "github.com/docker/infrakit/pkg/rpc/server" + "github.com/docker/infrakit/pkg/spi/group" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +type testLeaderDetector struct { + t *testing.T + me string + input <-chan string + stop chan struct{} + ch chan leader.Leadership +} + +func (l *testLeaderDetector) Start() (<-chan leader.Leadership, error) { + l.stop = make(chan struct{}) + l.ch = make(chan leader.Leadership) + go func() { + for { + select { + case <-l.stop: + return + case found := <-l.input: + if found == l.me { + l.ch <- leader.Leadership{Status: leader.Leader} + } else { + l.ch <- leader.Leadership{Status: leader.NotLeader} + } + } + } + }() + return l.ch, nil +} + +func (l *testLeaderDetector) Stop() { + close(l.stop) +} + +func testEnsemble(t *testing.T, + dir, id string, + leader chan string, + ctrl *gomock.Controller, + configStore func(*store_mock.MockSnapshot), + configureGroup func(*group_mock.MockPlugin)) (Backend, server.Stoppable) { + + disc, err := discovery.NewPluginDiscoveryWithDirectory(dir) + require.NoError(t, err) + + detector := &testLeaderDetector{t: t, me: id, input: leader} + + snap := store_mock.NewMockSnapshot(ctrl) + configStore(snap) + + // start group + gm := group_mock.NewMockPlugin(ctrl) + configureGroup(gm) + + gs := group_rpc.PluginServer(gm) + st, err := server.StartPluginAtPath(filepath.Join(dir, "group-stateless"), gs) + require.NoError(t, err) + + m, err := NewManager(disc, detector, snap, "group-stateless") + require.NoError(t, err) + + return m, st +} + +func testSetLeader(t *testing.T, c []chan string, l string) { + for _, cc := range c { + cc <- l + } +} + +func testDiscoveryDir(t *testing.T) string { + dir := filepath.Join(os.TempDir(), fmt.Sprintf("%v", time.Now().UnixNano())) + err := os.MkdirAll(dir, 0744) + require.NoError(t, err) + return dir +} + +func testBuildGroupSpec(groupID, properties string) group.Spec { + raw := json.RawMessage([]byte(properties)) + return group.Spec{ + ID: group.ID(groupID), + Properties: &raw, + } +} + +func testBuildGlobalSpec(t *testing.T, gs group.Spec) GlobalSpec { + buff, err := json.Marshal(gs) + require.NoError(t, err) + raw := json.RawMessage(buff) + return GlobalSpec{ + Groups: map[group.ID]PluginSpec{ + gs.ID: { + Plugin: "group-stateless", + Properties: &raw, + }, + }, + } +} + +func testToStruct(m *json.RawMessage) interface{} { + o := map[string]interface{}{} + json.Unmarshal([]byte(*m), &o) + return &o +} + +func TestNoCallsToGroupWhenNoLeader(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + leaderChans := []chan string{make(chan string), make(chan string)} + + manager1, stoppable1 := testEnsemble(t, testDiscoveryDir(t), "m1", leaderChans[0], ctrl, + func(s *store_mock.MockSnapshot) { + // no calls + }, + func(g *group_mock.MockPlugin) { + // no calls + }) + manager2, stoppable2 := testEnsemble(t, testDiscoveryDir(t), "m2", leaderChans[1], ctrl, + func(s *store_mock.MockSnapshot) { + // no calls + }, + func(g *group_mock.MockPlugin) { + // no calls + }) + + manager1.Start() + manager2.Start() + + manager1.Stop() + manager2.Stop() + + stoppable1.Stop() + stoppable2.Stop() +} + +func TestStartOneLeader(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + gs := testBuildGroupSpec("managers", ` +{ + "field1": "value1" +} +`) + global := testBuildGlobalSpec(t, gs) + + leaderChans := []chan string{make(chan string), make(chan string)} + checkpoint := make(chan struct{}) + + manager1, stoppable1 := testEnsemble(t, testDiscoveryDir(t), "m1", leaderChans[0], ctrl, + func(s *store_mock.MockSnapshot) { + empty := &GlobalSpec{} + s.EXPECT().Load(gomock.Eq(empty)).Do( + func(o interface{}) error { + p, is := o.(*GlobalSpec) + require.True(t, is) + *p = global + return nil + }).Return(nil) + }, + func(g *group_mock.MockPlugin) { + g.EXPECT().CommitGroup(gomock.Any(), false).Do( + func(spec group.Spec, pretend bool) (string, error) { + + defer close(checkpoint) + + require.Equal(t, gs.ID, spec.ID) + require.Equal(t, testToStruct(gs.Properties), testToStruct(spec.Properties)) + return "ok", nil + }).Return("ok", nil) + }) + manager2, stoppable2 := testEnsemble(t, testDiscoveryDir(t), "m2", leaderChans[1], ctrl, + func(s *store_mock.MockSnapshot) { + // no calls expected + }, + func(g *group_mock.MockPlugin) { + // no calls expected + }) + + manager1.Start() + manager2.Start() + + testSetLeader(t, leaderChans, "m1") + + <-checkpoint + + manager1.Stop() + manager2.Stop() + + stoppable1.Stop() + stoppable2.Stop() + +} + +func TestChangeLeadership(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + gs := testBuildGroupSpec("managers", ` +{ + "field1": "value1" +} +`) + global := testBuildGlobalSpec(t, gs) + + leaderChans := []chan string{make(chan string), make(chan string)} + checkpoint1 := make(chan struct{}) + checkpoint2 := make(chan struct{}) + checkpoint3 := make(chan struct{}) + + manager1, stoppable1 := testEnsemble(t, testDiscoveryDir(t), "m1", leaderChans[0], ctrl, + func(s *store_mock.MockSnapshot) { + empty := &GlobalSpec{} + s.EXPECT().Load(gomock.Eq(empty)).Do( + func(o interface{}) error { + p, is := o.(*GlobalSpec) + require.True(t, is) + *p = global + return nil + }, + ).Return(nil) + }, + func(g *group_mock.MockPlugin) { + g.EXPECT().CommitGroup(gomock.Any(), false).Do( + func(spec group.Spec, pretend bool) (string, error) { + + defer close(checkpoint1) + + require.Equal(t, gs.ID, spec.ID) + require.Equal(t, testToStruct(gs.Properties), testToStruct(spec.Properties)) + return "ok", nil + }, + ).Return("ok", nil) + + // We will get a call to inspect what's being watched + g.EXPECT().InspectGroups().Return([]group.Spec{gs}, nil) + + // Now we lost leadership... need to unwatch + g.EXPECT().FreeGroup(gomock.Eq(group.ID("managers"))).Do( + func(id group.ID) error { + + defer close(checkpoint3) + + return nil + }, + ).Return(nil) + }) + manager2, stoppable2 := testEnsemble(t, testDiscoveryDir(t), "m2", leaderChans[1], ctrl, + func(s *store_mock.MockSnapshot) { + empty := &GlobalSpec{} + s.EXPECT().Load(gomock.Eq(empty)).Do( + func(o interface{}) error { + p, is := o.(*GlobalSpec) + require.True(t, is) + *p = global + return nil + }, + ).Return(nil) + }, + func(g *group_mock.MockPlugin) { + g.EXPECT().CommitGroup(gomock.Any(), false).Do( + func(spec group.Spec, pretend bool) (string, error) { + + defer close(checkpoint2) + + require.Equal(t, gs.ID, spec.ID) + require.Equal(t, testToStruct(gs.Properties), testToStruct(spec.Properties)) + return "ok", nil + }, + ).Return("ok", nil) + }) + + manager1.Start() + manager2.Start() + + testSetLeader(t, leaderChans, "m1") + + <-checkpoint1 + + testSetLeader(t, leaderChans, "m2") + + <-checkpoint2 + <-checkpoint3 + + manager1.Stop() + manager2.Stop() + + stoppable1.Stop() + stoppable2.Stop() +} diff --git a/pkg/mock/generate.go b/pkg/mock/generate.go index f132d42d2..b1c1bd756 100644 --- a/pkg/mock/generate.go +++ b/pkg/mock/generate.go @@ -1,6 +1,8 @@ package mock -//go:generate mockgen -package instance -destination spi/instance/instance.go github.com/docker/infrakit/spi/instance Plugin -//go:generate mockgen -package instance -destination spi/flavor/flavor.go github.com/docker/infrakit/spi/flavor Plugin +//go:generate mockgen -package instance -destination spi/instance/instance.go github.com/docker/infrakit/pkg/spi/instance Plugin +//go:generate mockgen -package flavor -destination spi/flavor/flavor.go github.com/docker/infrakit/pkg/spi/flavor Plugin +//go:generate mockgen -package group -destination spi/group/group.go github.com/docker/infrakit/pkg/spi/group Plugin //go:generate mockgen -package client -destination docker/docker/client/api.go github.com/docker/docker/client APIClient -//go:generate mockgen -package group -destination plugin/group/group.go github.com/docker/infrakit/plugin/group Scaled +//go:generate mockgen -package group -destination plugin/group/group.go github.com/docker/infrakit/pkg/plugin/group Scaled +//go:generate mockgen -package store -destination store/store.go github.com/docker/infrakit/pkg/store Snapshot diff --git a/pkg/mock/plugin/group/group.go b/pkg/mock/plugin/group/group.go index b36385be5..b059ba1b5 100644 --- a/pkg/mock/plugin/group/group.go +++ b/pkg/mock/plugin/group/group.go @@ -1,5 +1,5 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/docker/infrakit/plugin/group (interfaces: Scaled) +// Source: github.com/docker/infrakit/pkg/plugin/group (interfaces: Scaled) package group diff --git a/pkg/mock/spi/flavor/flavor.go b/pkg/mock/spi/flavor/flavor.go index d21315055..f566b94b5 100644 --- a/pkg/mock/spi/flavor/flavor.go +++ b/pkg/mock/spi/flavor/flavor.go @@ -1,7 +1,7 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/docker/infrakit/spi/flavor (interfaces: Plugin) +// Source: github.com/docker/infrakit/pkg/spi/flavor (interfaces: Plugin) -package instance +package flavor import ( json "encoding/json" diff --git a/pkg/mock/spi/group/group.go b/pkg/mock/spi/group/group.go new file mode 100644 index 000000000..3b4398f4c --- /dev/null +++ b/pkg/mock/spi/group/group.go @@ -0,0 +1,83 @@ +// Automatically generated by MockGen. DO NOT EDIT! +// Source: github.com/docker/infrakit/pkg/spi/group (interfaces: Plugin) + +package group + +import ( + group "github.com/docker/infrakit/pkg/spi/group" + gomock "github.com/golang/mock/gomock" +) + +// Mock of Plugin interface +type MockPlugin struct { + ctrl *gomock.Controller + recorder *_MockPluginRecorder +} + +// Recorder for MockPlugin (not exported) +type _MockPluginRecorder struct { + mock *MockPlugin +} + +func NewMockPlugin(ctrl *gomock.Controller) *MockPlugin { + mock := &MockPlugin{ctrl: ctrl} + mock.recorder = &_MockPluginRecorder{mock} + return mock +} + +func (_m *MockPlugin) EXPECT() *_MockPluginRecorder { + return _m.recorder +} + +func (_m *MockPlugin) CommitGroup(_param0 group.Spec, _param1 bool) (string, error) { + ret := _m.ctrl.Call(_m, "CommitGroup", _param0, _param1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockPluginRecorder) CommitGroup(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "CommitGroup", arg0, arg1) +} + +func (_m *MockPlugin) DescribeGroup(_param0 group.ID) (group.Description, error) { + ret := _m.ctrl.Call(_m, "DescribeGroup", _param0) + ret0, _ := ret[0].(group.Description) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockPluginRecorder) DescribeGroup(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "DescribeGroup", arg0) +} + +func (_m *MockPlugin) DestroyGroup(_param0 group.ID) error { + ret := _m.ctrl.Call(_m, "DestroyGroup", _param0) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockPluginRecorder) DestroyGroup(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "DestroyGroup", arg0) +} + +func (_m *MockPlugin) FreeGroup(_param0 group.ID) error { + ret := _m.ctrl.Call(_m, "FreeGroup", _param0) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockPluginRecorder) FreeGroup(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "FreeGroup", arg0) +} + +func (_m *MockPlugin) InspectGroups() ([]group.Spec, error) { + ret := _m.ctrl.Call(_m, "InspectGroups") + ret0, _ := ret[0].([]group.Spec) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockPluginRecorder) InspectGroups() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "InspectGroups") +} diff --git a/pkg/mock/spi/instance/instance.go b/pkg/mock/spi/instance/instance.go index 244765d84..ff2e8f249 100644 --- a/pkg/mock/spi/instance/instance.go +++ b/pkg/mock/spi/instance/instance.go @@ -1,5 +1,5 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/docker/infrakit/spi/instance (interfaces: Plugin) +// Source: github.com/docker/infrakit/pkg/spi/instance (interfaces: Plugin) package instance diff --git a/pkg/mock/store/store.go b/pkg/mock/store/store.go new file mode 100644 index 000000000..61fb51ed1 --- /dev/null +++ b/pkg/mock/store/store.go @@ -0,0 +1,49 @@ +// Automatically generated by MockGen. DO NOT EDIT! +// Source: github.com/docker/infrakit/pkg/store (interfaces: Snapshot) + +package store + +import ( + gomock "github.com/golang/mock/gomock" +) + +// Mock of Snapshot interface +type MockSnapshot struct { + ctrl *gomock.Controller + recorder *_MockSnapshotRecorder +} + +// Recorder for MockSnapshot (not exported) +type _MockSnapshotRecorder struct { + mock *MockSnapshot +} + +func NewMockSnapshot(ctrl *gomock.Controller) *MockSnapshot { + mock := &MockSnapshot{ctrl: ctrl} + mock.recorder = &_MockSnapshotRecorder{mock} + return mock +} + +func (_m *MockSnapshot) EXPECT() *_MockSnapshotRecorder { + return _m.recorder +} + +func (_m *MockSnapshot) Load(_param0 interface{}) error { + ret := _m.ctrl.Call(_m, "Load", _param0) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockSnapshotRecorder) Load(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "Load", arg0) +} + +func (_m *MockSnapshot) Save(_param0 interface{}) error { + ret := _m.ctrl.Call(_m, "Save", _param0) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockSnapshotRecorder) Save(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "Save", arg0) +}