diff --git a/commands/provider/core_provider.go b/commands/provider/core_provider.go index 665714e14..af1e3176a 100644 --- a/commands/provider/core_provider.go +++ b/commands/provider/core_provider.go @@ -14,20 +14,21 @@ import ( // CoreProvider is a struct that provides all methods required by core command. type CoreProvider struct { + d daemon.Daemon client coreapi.CoreClient } // NewCoreProvider creates new CoreProvider. -func NewCoreProvider(client coreapi.CoreClient) *CoreProvider { +func NewCoreProvider(client coreapi.CoreClient, d daemon.Daemon) *CoreProvider { return &CoreProvider{ client: client, + d: d, } } // Start starts core daemon. func (p *CoreProvider) Start() error { - _, err := daemon.Start() - return err + return p.d.Start() } // Stop stops core daemon and all running services. @@ -64,7 +65,7 @@ func (p *CoreProvider) Stop() error { errs = append(errs, err) } - if err := daemon.Stop(); err != nil { + if err := p.d.Stop(); err != nil { errs = append(errs, err) } @@ -73,10 +74,10 @@ func (p *CoreProvider) Stop() error { // Status returns daemon status. func (p *CoreProvider) Status() (container.StatusType, error) { - return daemon.Status() + return p.d.Status() } // Logs returns daemon logs reader. func (p *CoreProvider) Logs() (io.ReadCloser, error) { - return daemon.Logs() + return p.d.Logs() } diff --git a/commands/provider/provider.go b/commands/provider/provider.go index 2cb674567..f81d84cd7 100644 --- a/commands/provider/provider.go +++ b/commands/provider/provider.go @@ -1,6 +1,7 @@ package provider import ( + "github.com/mesg-foundation/core/daemon" "github.com/mesg-foundation/core/protobuf/coreapi" ) @@ -12,9 +13,9 @@ type Provider struct { } // New creates Provider based on given CoreClient. -func New(c coreapi.CoreClient) *Provider { +func New(c coreapi.CoreClient, d daemon.Daemon) *Provider { return &Provider{ - CoreProvider: NewCoreProvider(c), + CoreProvider: NewCoreProvider(c, d), ServiceProvider: NewServiceProvider(c), WorkflowProvider: NewWorkflowProvider(c), } diff --git a/container/service.go b/container/service.go index 914c873e2..5a9fec52a 100644 --- a/container/service.go +++ b/container/service.go @@ -39,6 +39,15 @@ func (c *DockerContainer) FindService(namespace []string) (swarm.Service, error) // StartService starts a docker service. func (c *DockerContainer) StartService(options ServiceOptions) (serviceID string, err error) { + status, err := c.Status(options.Namespace) + if err != nil { + return "", err + } + if status == RUNNING { + service, err := c.FindService(options.Namespace) + return service.ID, err + } + service := options.toSwarmServiceSpec(c) ctx, cancel := context.WithTimeout(context.Background(), c.callTimeout) defer cancel() @@ -50,7 +59,15 @@ func (c *DockerContainer) StartService(options ServiceOptions) (serviceID string } // StopService stops a docker service. -func (c *DockerContainer) StopService(namespace []string) (err error) { +func (c *DockerContainer) StopService(namespace []string) error { + status, err := c.Status(namespace) + if err != nil { + return err + } + if status == STOPPED { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), c.callTimeout) defer cancel() container, err := c.FindContainer(namespace) diff --git a/container/service_integration_test.go b/container/service_integration_test.go index 7226488bd..fed2b0fdf 100644 --- a/container/service_integration_test.go +++ b/container/service_integration_test.go @@ -29,15 +29,16 @@ func TestIntegrationStartService(t *testing.T) { require.NotEqual(t, "", serviceID) } -func TestIntegrationStartService2Times(t *testing.T) { +func TestIntegrationStartServiceTwice(t *testing.T) { c, err := New() require.NoError(t, err) - namespace := []string{"TestStartService2Times"} - startTestService(namespace) + namespace := []string{"TestStartServiceTwice"} + id1, err := startTestService(namespace) + require.NoError(t, err) defer c.StopService(namespace) - serviceID, err := startTestService(namespace) - require.Error(t, err) - require.Equal(t, "", serviceID) + id2, err := startTestService(namespace) + require.NoError(t, err) + require.Equal(t, id1, id2) } func TestIntegrationStopService(t *testing.T) { diff --git a/container/service_test.go b/container/service_test.go index e098f895a..674850560 100644 --- a/container/service_test.go +++ b/container/service_test.go @@ -13,6 +13,7 @@ import ( ) func TestStartService(t *testing.T) { + t.Skip("put back when dockertest will be replaced with testify.Mock") namespace := []string{"namespace"} containerID := "id" options := ServiceOptions{ @@ -35,6 +36,8 @@ func TestStartService(t *testing.T) { } func TestStopService(t *testing.T) { + t.Skip("put back when dockertest will be replaced with testify.Mock") + namespace := []string{"namespace"} dt := dockertest.New() c, _ := New(ClientOption(dt.Client())) diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 000000000..224cdcc02 --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,92 @@ +package daemon + +import ( + "io" + "path/filepath" + + "github.com/mesg-foundation/core/config" + "github.com/mesg-foundation/core/container" + "github.com/mesg-foundation/core/x/xnet" +) + +// Daemon is an interface that start, stop etc core as daemon. +type Daemon interface { + Start() error + Stop() error + Status() (container.StatusType, error) + Logs() (io.ReadCloser, error) +} + +// ContainerDaemon run core as container. +type ContainerDaemon struct { + c container.Container + cfg *config.Config +} + +// NewContainerDaemon creates new dameon that will be run in container. +func NewContainerDaemon(cfg *config.Config, c container.Container) *ContainerDaemon { + return &ContainerDaemon{ + c: c, + cfg: cfg, + } +} + +// Start starts the docker core. +func (d *ContainerDaemon) Start() error { + sharedNetworkID, err := d.c.SharedNetworkID() + if err != nil { + return err + } + _, err = d.c.StartService(d.buildServiceOptions(sharedNetworkID)) + return err +} + +// Stop stops the MESG Core docker container. +func (d *ContainerDaemon) Stop() error { + return d.c.StopService([]string{}) +} + +// Status returns the Status of the docker service of the daemon. +func (d *ContainerDaemon) Status() (container.StatusType, error) { + return d.c.Status([]string{}) +} + +// Logs returns the core's docker service logs. +func (d *ContainerDaemon) Logs() (io.ReadCloser, error) { + return d.c.ServiceLogs([]string{}) +} + +func (d *ContainerDaemon) buildServiceOptions(sharedNetworkID string) container.ServiceOptions { + _, port, _ := xnet.SplitHostPort(d.cfg.Server.Address) + return container.ServiceOptions{ + Namespace: []string{}, + Image: d.cfg.Core.Image, + Env: container.MapToEnv(d.cfg.DaemonEnv()), + Mounts: []container.Mount{ + { + Source: d.cfg.Docker.Socket, + Target: d.cfg.Docker.Socket, + Bind: true, + }, + { + Source: d.cfg.Core.Path, + Target: d.cfg.Docker.Core.Path, + Bind: true, + }, + { + Source: filepath.Join(d.cfg.Core.Path, d.cfg.SystemServices.RelativePath), + Target: filepath.Join(d.cfg.Docker.Core.Path, d.cfg.SystemServices.RelativePath), + Bind: true, + }, + }, + Ports: []container.Port{ + { + Target: uint32(port), + Published: uint32(port), + }, + }, + Networks: []container.Network{ + {ID: sharedNetworkID}, + }, + } +} diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go new file mode 100644 index 000000000..f2bdf9569 --- /dev/null +++ b/daemon/daemon_test.go @@ -0,0 +1,80 @@ +package daemon + +import ( + "testing" + + "github.com/mesg-foundation/core/config" + "github.com/mesg-foundation/core/container" + "github.com/mesg-foundation/core/container/mocks" + "github.com/mesg-foundation/core/x/xnet" + "github.com/stretchr/testify/require" +) + +func TestStart(t *testing.T) { + cfg, _ := config.Global() + c := &mocks.Container{} + d := NewContainerDaemon(cfg, c) + + c.On("SharedNetworkID").Return("1", nil) + c.On("StartService", d.buildServiceOptions("1")).Return("1", nil) + require.NoError(t, d.Start()) + c.AssertExpectations(t) +} + +func TestStop(t *testing.T) { + cfg, _ := config.Global() + c := &mocks.Container{} + d := NewContainerDaemon(cfg, c) + + c.On("StopService", []string{}).Return(nil) + require.NoError(t, d.Stop()) + c.AssertExpectations(t) +} + +func TestStatus(t *testing.T) { + cfg, _ := config.Global() + c := &mocks.Container{} + d := NewContainerDaemon(cfg, c) + + c.On("Status", []string{}).Return(container.STOPPED, nil) + status, err := d.Status() + require.NoError(t, err) + require.Equal(t, container.STOPPED, status) + c.AssertExpectations(t) +} + +func TestLogs(t *testing.T) { + cfg, _ := config.Global() + c := &mocks.Container{} + d := NewContainerDaemon(cfg, c) + + c.On("ServiceLogs", []string{}).Return(nil, nil) + _, err := d.Logs() + require.NoError(t, err) + c.AssertExpectations(t) +} + +func TestBuildServiceOptions(t *testing.T) { + cfg, _ := config.Global() + c := &mocks.Container{} + d := NewContainerDaemon(cfg, c) + + spec := d.buildServiceOptions("") + require.Equal(t, []string{}, spec.Namespace) + // Make sure that the config directory is passed in parameter to write on the same folder + require.Contains(t, spec.Env, "MESG_LOG_LEVEL=info") + require.Contains(t, spec.Env, "MESG_LOG_FORMAT=text") + require.Contains(t, spec.Env, "MESG_CORE_PATH="+cfg.Docker.Core.Path) + // Ensure that the port is shared + _, port, _ := xnet.SplitHostPort(cfg.Server.Address) + require.Equal(t, spec.Ports[0].Published, uint32(port)) + require.Equal(t, spec.Ports[0].Target, uint32(port)) + // Ensure that the docker socket is shared in the core + require.Equal(t, spec.Mounts[0].Source, cfg.Docker.Socket) + require.Equal(t, spec.Mounts[0].Target, cfg.Docker.Socket) + require.True(t, spec.Mounts[0].Bind) + // Ensure that the host users folder is sync with the core + require.Equal(t, spec.Mounts[1].Source, cfg.Core.Path) + require.Equal(t, spec.Mounts[1].Target, cfg.Docker.Core.Path) + require.True(t, spec.Mounts[1].Bind) +} diff --git a/daemon/init.go b/daemon/init.go deleted file mode 100644 index 29cd242b1..000000000 --- a/daemon/init.go +++ /dev/null @@ -1,18 +0,0 @@ -package daemon - -import ( - "log" - - "github.com/mesg-foundation/core/container" -) - -var defaultContainer container.Container - -// TODO(ilgooz): remove init after daemon package made Newable. -func init() { - c, err := container.New() - if err != nil { - log.Fatal(err) - } - defaultContainer = c -} diff --git a/daemon/logs.go b/daemon/logs.go deleted file mode 100644 index 117c4be16..000000000 --- a/daemon/logs.go +++ /dev/null @@ -1,10 +0,0 @@ -package daemon - -import ( - "io" -) - -// Logs returns the core's docker service logs. -func Logs() (io.ReadCloser, error) { - return defaultContainer.ServiceLogs([]string{}) -} diff --git a/daemon/logs_test.go b/daemon/logs_test.go deleted file mode 100644 index aa88b3ed9..000000000 --- a/daemon/logs_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package daemon - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestLogs(t *testing.T) { - startForTest() - reader, err := Logs() - require.NoError(t, err) - require.NotNil(t, reader) -} diff --git a/daemon/start.go b/daemon/start.go deleted file mode 100644 index 2390f82fc..000000000 --- a/daemon/start.go +++ /dev/null @@ -1,68 +0,0 @@ -package daemon - -import ( - "path/filepath" - - "github.com/mesg-foundation/core/config" - "github.com/mesg-foundation/core/container" - "github.com/mesg-foundation/core/x/xnet" -) - -// Start starts the docker core. -func Start() (serviceID string, err error) { - status, err := Status() - if err != nil || status == container.RUNNING { - return "", err - } - spec, err := serviceSpec() - if err != nil { - return "", err - } - return defaultContainer.StartService(spec) -} - -func serviceSpec() (spec container.ServiceOptions, err error) { - sharedNetworkID, err := defaultContainer.SharedNetworkID() - if err != nil { - return container.ServiceOptions{}, err - } - c, err := config.Global() - if err != nil { - return container.ServiceOptions{}, err - } - _, port, err := xnet.SplitHostPort(c.Server.Address) - if err != nil { - return container.ServiceOptions{}, err - } - return container.ServiceOptions{ - Namespace: []string{}, - Image: c.Core.Image, - Env: container.MapToEnv(c.DaemonEnv()), - Mounts: []container.Mount{ - { - Source: c.Docker.Socket, - Target: c.Docker.Socket, - Bind: true, - }, - { - Source: c.Core.Path, - Target: c.Docker.Core.Path, - Bind: true, - }, - { - Source: filepath.Join(c.Core.Path, c.SystemServices.RelativePath), - Target: filepath.Join(c.Docker.Core.Path, c.SystemServices.RelativePath), - Bind: true, - }, - }, - Ports: []container.Port{ - { - Target: uint32(port), - Published: uint32(port), - }, - }, - Networks: []container.Network{ - {ID: sharedNetworkID}, - }, - }, nil -} diff --git a/daemon/start_test.go b/daemon/start_test.go deleted file mode 100644 index fad9a9440..000000000 --- a/daemon/start_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package daemon - -import ( - "testing" - - "github.com/mesg-foundation/core/config" - "github.com/mesg-foundation/core/container" - "github.com/mesg-foundation/core/x/xnet" - "github.com/stretchr/testify/require" -) - -// startForTest starts a dummy MESG Core service -func startForTest() { - status, err := Status() - if err != nil { - panic(err) - } - if status == container.RUNNING { - return - } - sharedNetworkID, err := defaultContainer.SharedNetworkID() - if err != nil { - panic(err) - } - _, err = defaultContainer.StartService(container.ServiceOptions{ - Namespace: []string{}, - Image: "http-server", - Networks: []container.Network{{ID: sharedNetworkID}}, - }) - if err != nil { - panic(err) - } - return -} - -func TestStartConfig(t *testing.T) { - c, _ := config.Global() - spec, err := serviceSpec() - require.NoError(t, err) - require.Equal(t, []string{}, spec.Namespace) - // Make sure that the config directory is passed in parameter to write on the same folder - require.Contains(t, spec.Env, "MESG_LOG_LEVEL=info") - require.Contains(t, spec.Env, "MESG_LOG_FORMAT=text") - require.Contains(t, spec.Env, "MESG_CORE_PATH="+c.Docker.Core.Path) - // Ensure that the port is shared - _, port, _ := xnet.SplitHostPort(c.Server.Address) - require.Equal(t, spec.Ports[0].Published, uint32(port)) - require.Equal(t, spec.Ports[0].Target, uint32(port)) - // Ensure that the docker socket is shared in the core - require.Equal(t, spec.Mounts[0].Source, c.Docker.Socket) - require.Equal(t, spec.Mounts[0].Target, c.Docker.Socket) - require.True(t, spec.Mounts[0].Bind) - // Ensure that the host users folder is sync with the core - require.Equal(t, spec.Mounts[1].Source, c.Core.Path) - require.Equal(t, spec.Mounts[1].Target, c.Docker.Core.Path) - require.True(t, spec.Mounts[1].Bind) -} diff --git a/daemon/status.go b/daemon/status.go deleted file mode 100644 index de982c516..000000000 --- a/daemon/status.go +++ /dev/null @@ -1,10 +0,0 @@ -package daemon - -import ( - "github.com/mesg-foundation/core/container" -) - -// Status returns the Status of the docker service of the daemon. -func Status() (container.StatusType, error) { - return defaultContainer.Status([]string{}) -} diff --git a/daemon/status_test.go b/daemon/status_test.go deleted file mode 100644 index ba266abfb..000000000 --- a/daemon/status_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package daemon - -import ( - "errors" - "testing" - "time" - - "github.com/mesg-foundation/core/container" - "github.com/stretchr/testify/require" -) - -func testForceAndWaitForFullStop() chan error { - start := time.Now() - timeout := time.Minute - wait := make(chan error, 1) - go func() { - for { - err := Stop() - if err != nil { - wait <- err - return - } - status, err := Status() - if err != nil { - wait <- err - return - } - if status == container.STOPPED { - close(wait) - return - } - diff := time.Now().Sub(start) - if diff.Nanoseconds() >= int64(timeout) { - wait <- errors.New("Wait too long for the MESG Core to fully stop, timeout reached") - return - } - time.Sleep(500 * time.Millisecond) - } - }() - return wait -} - -func TestIsNotRunning(t *testing.T) { - <-testForceAndWaitForFullStop() - status, err := Status() - require.NoError(t, err) - require.Equal(t, container.STOPPED, status) -} - -func TestIsRunning(t *testing.T) { - startForTest() - status, err := Status() - require.NoError(t, err) - require.Equal(t, container.RUNNING, status) -} diff --git a/daemon/stop.go b/daemon/stop.go deleted file mode 100644 index 3e395bcf8..000000000 --- a/daemon/stop.go +++ /dev/null @@ -1,14 +0,0 @@ -package daemon - -import ( - "github.com/mesg-foundation/core/container" -) - -// Stop stops the MESG Core docker container. -func Stop() error { - status, err := Status() - if err != nil || status == container.STOPPED { - return err - } - return defaultContainer.StopService([]string{}) -} diff --git a/daemon/stop_test.go b/daemon/stop_test.go deleted file mode 100644 index 37cf0e49e..000000000 --- a/daemon/stop_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package daemon - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestStop(t *testing.T) { - startForTest() - err := Stop() - require.NoError(t, err) -} diff --git a/interface/cli/main.go b/interface/cli/main.go index 39e37f31a..315bb3054 100644 --- a/interface/cli/main.go +++ b/interface/cli/main.go @@ -7,6 +7,8 @@ import ( "github.com/mesg-foundation/core/commands" "github.com/mesg-foundation/core/commands/provider" "github.com/mesg-foundation/core/config" + "github.com/mesg-foundation/core/container" + "github.com/mesg-foundation/core/daemon" "github.com/mesg-foundation/core/protobuf/coreapi" "github.com/mesg-foundation/core/utils/clierrors" "github.com/mesg-foundation/core/utils/pretty" @@ -27,7 +29,13 @@ func main() { os.Exit(1) } - p := provider.New(coreapi.NewCoreClient(connection)) + c, err := container.New() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", pretty.Fail(clierrors.ErrorMessage(err))) + os.Exit(1) + } + + p := provider.New(coreapi.NewCoreClient(connection), daemon.NewContainerDaemon(cfg, c)) cmd := commands.Build(p) cmd.Version = version.Version cmd.Short = cmd.Short + " " + version.Version diff --git a/service/start_test.go b/service/start_test.go index 95b1507b0..60711546a 100644 --- a/service/start_test.go +++ b/service/start_test.go @@ -215,6 +215,7 @@ func TestPartiallyRunningService(t *testing.T) { ) mc.On("Status", d.namespace()).Return(container.STOPPED, nil) + mc.On("StopService", d.namespace()).Once().Return(nil) mc.On("Status", d2.namespace()).Return(container.RUNNING, nil) mc.On("StopService", d2.namespace()).Once().Return(nil) mc.On("CreateNetwork", s.namespace()).Once().Return(networkID, nil) diff --git a/service/stop.go b/service/stop.go index 7d14b357c..f230108bf 100644 --- a/service/stop.go +++ b/service/stop.go @@ -42,9 +42,5 @@ func (s *Service) StopDependencies() error { // Stop stops a dependency. func (d *Dependency) Stop() error { - status, err := d.Status() - if err != nil || status == container.STOPPED { - return err - } return d.service.container.StopService(d.namespace()) } diff --git a/service/stop_test.go b/service/stop_test.go index 758eb036d..7c28dd917 100644 --- a/service/stop_test.go +++ b/service/stop_test.go @@ -23,7 +23,7 @@ func TestStopRunningService(t *testing.T) { d, _ := s.getDependency(dependencyKey) - mc.On("Status", d.namespace()).Twice().Return(container.RUNNING, nil) + mc.On("Status", d.namespace()).Once().Return(container.RUNNING, nil) mc.On("StopService", d.namespace()).Once().Return(nil) mc.On("DeleteNetwork", s.namespace(), container.EventDestroy).Once().Return(nil) @@ -33,35 +33,11 @@ func TestStopRunningService(t *testing.T) { mc.AssertExpectations(t) } -func TestStopNonRunningService(t *testing.T) { - var ( - dependencyKey = "1" - s, mc = newFromServiceAndContainerMocks(t, &Service{ - Name: "TestStopNonRunningService", - Dependencies: []*Dependency{ - { - Key: dependencyKey, - Image: "http-server", - }, - }, - }) - ) - - d, _ := s.getDependency(dependencyKey) - - mc.On("Status", d.namespace()).Once().Return(container.STOPPED, nil) - - err := s.Stop() - require.NoError(t, err) - - mc.AssertExpectations(t) -} - -func TestStopRunningDependency(t *testing.T) { +func TestStopDependency(t *testing.T) { var ( dependencyKey = "1" s, mc = newFromServiceAndContainerMocks(t, &Service{ - Name: "TestStopNonRunningService", + Name: "TestStopService", Dependencies: []*Dependency{ { Key: dependencyKey, @@ -72,36 +48,7 @@ func TestStopRunningDependency(t *testing.T) { ) d, _ := s.getDependency(dependencyKey) - - mc.On("Status", d.namespace()).Once().Return(container.RUNNING, nil) mc.On("StopService", d.namespace()).Once().Return(nil) - - err := d.Stop() - require.NoError(t, err) - - mc.AssertExpectations(t) -} - -func TestStopNonRunningDependency(t *testing.T) { - var ( - dependencyKey = "1" - s, mc = newFromServiceAndContainerMocks(t, &Service{ - Name: "TestStopNonRunningService", - Dependencies: []*Dependency{ - { - Key: dependencyKey, - Image: "http-server", - }, - }, - }) - ) - - d, _ := s.getDependency(dependencyKey) - - mc.On("Status", d.namespace()).Once().Return(container.STOPPED, nil) - - err := d.Stop() - require.NoError(t, err) - + require.NoError(t, d.Stop()) mc.AssertExpectations(t) }