From bc06781dbfc455d40314f89a07655755728ce844 Mon Sep 17 00:00:00 2001 From: Matty Evans Date: Thu, 19 Dec 2024 15:08:31 +1000 Subject: [PATCH] refactor: Update service dependencies to use interfaces --- cmd/cli/commands/config/display.go | 4 +- cmd/cli/commands/install/display.go | 4 +- cmd/cli/commands/stop/stop.go | 99 ++++++------- cmd/cli/commands/stop/stop_test.go | 204 +++++++++++++++++++++++++++ cmd/cli/commands/update/update.go | 6 +- cmd/cli/main.go | 6 +- go.mod | 2 + go.sum | 2 + internal/service/binary.go | 26 ++-- internal/service/config.go | 27 ++-- internal/service/docker.go | 26 ++-- internal/service/docker_test.go | 28 ++-- internal/service/mock/binary.mock.go | 96 +++++++++++++ internal/service/mock/config.mock.go | 110 +++++++++++++++ internal/service/mock/docker.mock.go | 96 +++++++++++++ internal/service/service.go | 40 ++++++ 16 files changed, 674 insertions(+), 102 deletions(-) create mode 100644 cmd/cli/commands/stop/stop_test.go create mode 100644 internal/service/mock/binary.mock.go create mode 100644 internal/service/mock/config.mock.go create mode 100644 internal/service/mock/docker.mock.go create mode 100644 internal/service/service.go diff --git a/cmd/cli/commands/config/display.go b/cmd/cli/commands/config/display.go index b8d85d4..0cd1c3f 100644 --- a/cmd/cli/commands/config/display.go +++ b/cmd/cli/commands/config/display.go @@ -14,7 +14,7 @@ type ConfigDisplay struct { pages *tview.Pages frame *tview.Frame log *logrus.Logger - configService *service.ConfigService + configService service.ConfigManager homePage *tui.Page categoryList *tview.List content tview.Primitive @@ -26,7 +26,7 @@ type ConfigDisplay struct { } // NewConfigDisplay creates a new Configtui. -func NewConfigDisplay(log *logrus.Logger, app *tview.Application, configService *service.ConfigService) *ConfigDisplay { +func NewConfigDisplay(log *logrus.Logger, app *tview.Application, configService service.ConfigManager) *ConfigDisplay { display := &ConfigDisplay{ app: app, pages: tview.NewPages(), diff --git a/cmd/cli/commands/install/display.go b/cmd/cli/commands/install/display.go index fe077ff..0f7c440 100644 --- a/cmd/cli/commands/install/display.go +++ b/cmd/cli/commands/install/display.go @@ -13,7 +13,7 @@ type InstallDisplay struct { pages *tview.Pages frame *tview.Frame log *logrus.Logger - configService *service.ConfigService + configService service.ConfigManager installPages []tui.PageInterface welcomePage *WelcomePage networkConfigPage *NetworkConfigPage @@ -24,7 +24,7 @@ type InstallDisplay struct { } // NewInstallDisplay creates a new InstallDisplay. -func NewInstallDisplay(log *logrus.Logger, app *tview.Application, configService *service.ConfigService) *InstallDisplay { +func NewInstallDisplay(log *logrus.Logger, app *tview.Application, configService service.ConfigManager) *InstallDisplay { display := &InstallDisplay{ app: app, pages: tview.NewPages(), diff --git a/cmd/cli/commands/stop/stop.go b/cmd/cli/commands/stop/stop.go index 98f6042..8000e9e 100644 --- a/cmd/cli/commands/stop/stop.go +++ b/cmd/cli/commands/stop/stop.go @@ -6,76 +6,77 @@ import ( "github.com/ethpandaops/contributoor-installer/cmd/cli/options" "github.com/ethpandaops/contributoor-installer/internal/service" "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) -func RegisterCommands(app *cli.App, opts *options.CommandOpts) { +func RegisterCommands(app *cli.App, opts *options.CommandOpts) error { app.Commands = append(app.Commands, cli.Command{ Name: opts.Name(), Aliases: opts.Aliases(), Usage: "Stop Contributoor", UsageText: "contributoor stop [options]", Action: func(c *cli.Context) error { - return stopContributoor(c, opts) + log := opts.Logger() + + configService, err := service.NewConfigService(log, c.GlobalString("config-path")) + if err != nil { + return fmt.Errorf("error loading config: %w", err) + } + + dockerService, err := service.NewDockerService(log, configService) + if err != nil { + return fmt.Errorf("error creating docker service: %w", err) + } + + binaryService := service.NewBinaryService(log, configService) + + return stopContributoor(c, log, configService, dockerService, binaryService) }, }) + + return nil } -func stopContributoor(c *cli.Context, opts *options.CommandOpts) error { - log := opts.Logger() +func stopContributoor( + c *cli.Context, + log *logrus.Logger, + config service.ConfigManager, + docker service.DockerService, + binary service.BinaryService, +) error { + var ( + runner service.ServiceRunner + cfg = config.Get() + ) - configService, err := service.NewConfigService(log, c.GlobalString("config-path")) - if err != nil { - return fmt.Errorf("%sError loading config: %v%s", tui.TerminalColorRed, err, tui.TerminalColorReset) - } + log.WithField("version", cfg.Version).Info("Stopping Contributoor") // Stop the service via whatever method the user has configured (docker or binary). - switch configService.Get().RunMethod { + switch cfg.RunMethod { case service.RunMethodDocker: - log.WithField("version", configService.Get().Version).Info("Stopping Contributoor") - - dockerService, err := service.NewDockerService(log, configService) - if err != nil { - log.Errorf("could not create docker service: %v", err) - - return err - } - - // Check if running before attempting to stop. - running, err := dockerService.IsRunning() - if err != nil { - log.Errorf("could not check service status: %v", err) + runner = docker + case service.RunMethodBinary: + runner = binary + default: + return fmt.Errorf("invalid run method: %s", cfg.RunMethod) + } - return err - } + // Check if running before attempting to stop. + running, err := runner.IsRunning() + if err != nil { + log.Errorf("could not check service status: %v", err) - // If the service is not running, we can just return. - if !running { - return fmt.Errorf("%sContributoor is not running. Use 'contributoor start' to start it%s", tui.TerminalColorRed, tui.TerminalColorReset) - } + return err + } - if err := dockerService.Stop(); err != nil { - log.Errorf("could not stop service: %v", err) + // If the service is not running, we can just return. + if !running { + return fmt.Errorf("%sContributoor is not running. Use 'contributoor start' to start it%s", tui.TerminalColorRed, tui.TerminalColorReset) + } - return err - } - case service.RunMethodBinary: - binaryService := service.NewBinaryService(log, configService) - - // Check if the service is currently running. - running, err := binaryService.IsRunning() - if err != nil { - return fmt.Errorf("failed to check service status: %v", err) - } - - // If the service is not running, we can just return. - if !running { - return fmt.Errorf("%sContributoor is not running%s", tui.TerminalColorRed, tui.TerminalColorReset) - } - - if err := binaryService.Stop(); err != nil { - return err - } + if err := runner.Stop(); err != nil { + return err } return nil diff --git a/cmd/cli/commands/stop/stop_test.go b/cmd/cli/commands/stop/stop_test.go new file mode 100644 index 0000000..5ef6b49 --- /dev/null +++ b/cmd/cli/commands/stop/stop_test.go @@ -0,0 +1,204 @@ +package stop + +import ( + "errors" + "flag" + "testing" + + "github.com/ethpandaops/contributoor-installer/cmd/cli/options" + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" + "go.uber.org/mock/gomock" +) + +func TestStopContributoor(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + runMethod string + setupMocks func(*mock.MockConfigManager, *mock.MockDockerService, *mock.MockBinaryService) + expectedError string + }{ + { + name: "docker - stops running service successfully", + runMethod: service.RunMethodDocker, + setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerService, b *mock.MockBinaryService) { + cfg.EXPECT().Get().Return(&service.ContributoorConfig{ + RunMethod: service.RunMethodDocker, + Version: "latest", + }).Times(1) + d.EXPECT().IsRunning().Return(true, nil) + d.EXPECT().Stop().Return(nil) + }, + }, + { + name: "docker - service not running", + runMethod: service.RunMethodDocker, + setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerService, b *mock.MockBinaryService) { + cfg.EXPECT().Get().Return(&service.ContributoorConfig{ + RunMethod: service.RunMethodDocker, + }).Times(1) + d.EXPECT().IsRunning().Return(false, nil) + }, + expectedError: "Contributoor is not running", + }, + { + name: "docker - stop fails", + runMethod: service.RunMethodDocker, + setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerService, b *mock.MockBinaryService) { + cfg.EXPECT().Get().Return(&service.ContributoorConfig{ + RunMethod: service.RunMethodDocker, + }).Times(1) + d.EXPECT().IsRunning().Return(true, nil) + d.EXPECT().Stop().Return(errors.New("stop failed")) + }, + expectedError: "stop failed", + }, + { + name: "binary - stops running service successfully", + runMethod: service.RunMethodBinary, + setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerService, b *mock.MockBinaryService) { + cfg.EXPECT().Get().Return(&service.ContributoorConfig{ + RunMethod: service.RunMethodBinary, + }).Times(1) + b.EXPECT().IsRunning().Return(true, nil) + b.EXPECT().Stop().Return(nil) + }, + }, + { + name: "binary - service not running", + runMethod: service.RunMethodBinary, + setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerService, b *mock.MockBinaryService) { + cfg.EXPECT().Get().Return(&service.ContributoorConfig{ + RunMethod: service.RunMethodBinary, + }).Times(1) + b.EXPECT().IsRunning().Return(false, nil) + }, + expectedError: "Contributoor is not running", + }, + { + name: "invalid run method", + runMethod: "invalid", + setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerService, b *mock.MockBinaryService) { + cfg.EXPECT().Get().Return(&service.ContributoorConfig{ + RunMethod: "invalid", + }).Times(1) + }, + expectedError: "invalid run method", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockConfig := mock.NewMockConfigManager(ctrl) + mockDocker := mock.NewMockDockerService(ctrl) + mockBinary := mock.NewMockBinaryService(ctrl) + + tt.setupMocks(mockConfig, mockDocker, mockBinary) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + + err := stopContributoor(ctx, logrus.New(), mockConfig, mockDocker, mockBinary) + + if tt.expectedError != "" { + assert.ErrorContains(t, err, tt.expectedError) + + return + } + + assert.NoError(t, err) + }) + } +} + +func TestRegisterCommands(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + configPath string + expectedError string + }{ + { + name: "successfully registers command", + configPath: "testdata/valid", // "testdata" is an ancillary dir provided by go-test. + }, + { + name: "fails when config service fails", + configPath: "/invalid/path/that/doesnt/exist", + expectedError: "error loading config", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create CLI app, with the config flag. + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "config-path", + }, + } + + // Ensure we set the config path flag. + globalSet := flag.NewFlagSet("test", flag.ContinueOnError) + globalSet.String("config-path", "", "") + err := globalSet.Set("config-path", tt.configPath) + require.NoError(t, err) + + // Create the cmd context. + globalCtx := cli.NewContext(app, globalSet, nil) + app.Metadata = map[string]interface{}{ + "flagContext": globalCtx, + } + + // Now test! + err = RegisterCommands( + app, + options.NewCommandOpts( + options.WithName("stop"), + options.WithLogger(logrus.New()), + options.WithAliases([]string{"s"}), + ), + ) + + if tt.expectedError != "" { + // Ensure the command registration succeeded. + assert.NoError(t, err) + + // Assert that the action execution fails as expected. + cmd := app.Commands[0] + ctx := cli.NewContext(app, nil, globalCtx) + + // Assert that the action is the func we expect, mainly because the linter is having a fit otherwise. + action, ok := cmd.Action.(func(*cli.Context) error) + require.True(t, ok, "expected action to be func(*cli.Context) error") + + // Execute the action and assert the error. + actionErr := action(ctx) + assert.Error(t, actionErr) + assert.ErrorContains(t, actionErr, tt.expectedError) + } else { + // Ensure the command registration succeeded. + assert.NoError(t, err) + assert.Len(t, app.Commands, 1) + + // Ensure the command is registered as expected by dumping the command. + cmd := app.Commands[0] + assert.Equal(t, "stop", cmd.Name) + assert.Equal(t, []string{"s"}, cmd.Aliases) + assert.Equal(t, "Stop Contributoor", cmd.Usage) + assert.Equal(t, "contributoor stop [options]", cmd.UsageText) + assert.NotNil(t, cmd.Action) + } + }) + } +} diff --git a/cmd/cli/commands/update/update.go b/cmd/cli/commands/update/update.go index a355591..6251bd8 100644 --- a/cmd/cli/commands/update/update.go +++ b/cmd/cli/commands/update/update.go @@ -131,7 +131,7 @@ func updateContributoor(c *cli.Context, opts *options.CommandOpts) error { return err } - var updateFn func(log *logrus.Logger, configService *service.ConfigService) (bool, error) + var updateFn func(log *logrus.Logger, configService service.ConfigManager) (bool, error) // Update the service via whatever method the user has configured (docker or binary). switch configService.Get().RunMethod { @@ -156,7 +156,7 @@ func updateContributoor(c *cli.Context, opts *options.CommandOpts) error { return nil } -func updateBinary(log *logrus.Logger, configService *service.ConfigService) (bool, error) { +func updateBinary(log *logrus.Logger, configService service.ConfigManager) (bool, error) { binaryService := service.NewBinaryService(log, configService) log.WithField("version", configService.Get().Version).Info("Updating Contributoor") @@ -204,7 +204,7 @@ func updateBinary(log *logrus.Logger, configService *service.ConfigService) (boo return true, nil } -func updateDocker(log *logrus.Logger, configService *service.ConfigService) (bool, error) { +func updateDocker(log *logrus.Logger, configService service.ConfigManager) (bool, error) { dockerService, err := service.NewDockerService(log, configService) if err != nil { log.Errorf("could not create docker service: %v", err) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 80fc626..0b0e2fc 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -68,10 +68,12 @@ func main() { options.WithLogger(log), )) - stop.RegisterCommands(app, options.NewCommandOpts( + if err := stop.RegisterCommands(app, options.NewCommandOpts( options.WithName("stop"), options.WithLogger(log), - )) + )); err != nil { + log.Errorf("failed to register stop command: %v", err) + } update.RegisterCommands(app, options.NewCommandOpts( options.WithName("update"), diff --git a/go.mod b/go.mod index d6f6a6d..dd2fd9e 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.34.0 github.com/urfave/cli v1.22.16 + go.uber.org/mock v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -56,6 +57,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect diff --git a/go.sum b/go.sum index 1514b59..eb1b88d 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/internal/service/binary.go b/internal/service/binary.go index dc6ecc0..a859873 100644 --- a/internal/service/binary.go +++ b/internal/service/binary.go @@ -15,8 +15,14 @@ import ( "github.com/sirupsen/logrus" ) +//go:generate mockgen -package mock -destination mock/binary.mock.go github.com/ethpandaops/contributoor-installer/internal/service BinaryService + +type BinaryService interface { + ServiceRunner +} + // BinaryService is a basic service for interacting with the contributoor binary. -type BinaryService struct { +type binaryService struct { logger *logrus.Logger config *ContributoorConfig stdout *os.File @@ -24,12 +30,12 @@ type BinaryService struct { } // NewBinaryService creates a new BinaryService. -func NewBinaryService(logger *logrus.Logger, configService *ConfigService) *BinaryService { +func NewBinaryService(logger *logrus.Logger, configService ConfigManager) BinaryService { expandedDir, err := homedir.Expand(configService.Get().ContributoorDirectory) if err != nil { logger.Errorf("Failed to expand config path: %v", err) - return &BinaryService{ + return &binaryService{ logger: logger, config: configService.Get(), } @@ -42,7 +48,7 @@ func NewBinaryService(logger *logrus.Logger, configService *ConfigService) *Bina if err != nil { logger.Errorf("Failed to open stdout log file: %v", err) - return &BinaryService{ + return &binaryService{ logger: logger, config: configService.Get(), } @@ -54,13 +60,13 @@ func NewBinaryService(logger *logrus.Logger, configService *ConfigService) *Bina logger.Errorf("Failed to open stderr log file: %v", err) - return &BinaryService{ + return &binaryService{ logger: logger, config: configService.Get(), } } - return &BinaryService{ + return &binaryService{ logger: logger, config: configService.Get(), stdout: stdout, @@ -69,7 +75,7 @@ func NewBinaryService(logger *logrus.Logger, configService *ConfigService) *Bina } // Start starts the binary service. -func (s *BinaryService) Start() error { +func (s *binaryService) Start() error { binaryPath := filepath.Join(s.config.ContributoorDirectory, "bin", "sentry") if _, err := os.Stat(binaryPath); err != nil { return fmt.Errorf("binary not found at %s - please reinstall", binaryPath) @@ -115,7 +121,7 @@ func (s *BinaryService) Start() error { } // Stop stops the binary service. -func (s *BinaryService) Stop() error { +func (s *binaryService) Stop() error { pidFile := filepath.Join(s.config.ContributoorDirectory, "contributoor.pid") pidBytes, err := os.ReadFile(pidFile) @@ -153,7 +159,7 @@ func (s *BinaryService) Stop() error { } // IsRunning checks if the binary service is running. -func (s *BinaryService) IsRunning() (bool, error) { +func (s *binaryService) IsRunning() (bool, error) { pidFile := filepath.Join(s.config.ContributoorDirectory, "contributoor.pid") if _, err := os.Stat(pidFile); os.IsNotExist(err) { return false, nil @@ -183,7 +189,7 @@ func (s *BinaryService) IsRunning() (bool, error) { } // Update updates the binary service. -func (s *BinaryService) Update() error { +func (s *binaryService) Update() error { expandedDir, err := homedir.Expand(s.config.ContributoorDirectory) if err != nil { return fmt.Errorf("failed to expand config path: %w", err) diff --git a/internal/service/config.go b/internal/service/config.go index 7f93b1d..3c986ba 100644 --- a/internal/service/config.go +++ b/internal/service/config.go @@ -21,11 +21,7 @@ import ( "gopkg.in/yaml.v3" ) -// RunMethods defines the possible ways to run the contributoor service. -const ( - RunMethodDocker = "docker" - RunMethodBinary = "binary" -) +//go:generate mockgen -package mock -destination mock/config.mock.go github.com/ethpandaops/contributoor-installer/internal/service ConfigManager // ContributoorConfig is the configuration for the contributoor service. type ContributoorConfig struct { @@ -45,7 +41,7 @@ type OutputServerConfig struct { } // ConfigService is a basic service for interacting with file configuration. -type ConfigService struct { +type configService struct { logger *logrus.Logger configPath string configDir string @@ -53,7 +49,7 @@ type ConfigService struct { } // NewConfigService creates a new ConfigService. -func NewConfigService(logger *logrus.Logger, configPath string) (*ConfigService, error) { +func NewConfigService(logger *logrus.Logger, configPath string) (ConfigManager, error) { // Expand home directory path, err := homedir.Expand(configPath) if err != nil { @@ -110,7 +106,7 @@ func NewConfigService(logger *logrus.Logger, configPath string) (*ConfigService, } } - return &ConfigService{ + return &configService{ logger: logger, configPath: fullConfigPath, configDir: filepath.Dir(fullConfigPath), @@ -119,7 +115,7 @@ func NewConfigService(logger *logrus.Logger, configPath string) (*ConfigService, } // Update updates the file config with the given updates. -func (s *ConfigService) Update(updates func(*ContributoorConfig)) error { +func (s *configService) Update(updates func(*ContributoorConfig)) error { // Apply updates to a copy updatedConfig := *s.config updates(&updatedConfig) @@ -151,15 +147,20 @@ func (s *ConfigService) Update(updates func(*ContributoorConfig)) error { } // Get returns the current file config. -func (s *ConfigService) Get() *ContributoorConfig { +func (s *configService) Get() *ContributoorConfig { return s.config } // GetConfigDir returns the directory of the file config. -func (s *ConfigService) GetConfigDir() string { +func (s *configService) GetConfigDir() string { return s.configDir } +// GetConfigPath returns the path of the file config. +func (s *configService) GetConfigPath() string { + return s.configPath +} + // WriteConfig writes the file config to the given path. func WriteConfig(path string, cfg *ContributoorConfig) error { if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { @@ -188,7 +189,7 @@ func newDefaultConfig() *ContributoorConfig { } } -func (s *ConfigService) validate(cfg *ContributoorConfig) error { +func (s *configService) validate(cfg *ContributoorConfig) error { if cfg.Version == "" { return fmt.Errorf("version is required") } @@ -246,6 +247,6 @@ func migrateConfig(target, source *ContributoorConfig) error { return nil } -func (s *ConfigService) Save() error { +func (s *configService) Save() error { return WriteConfig(s.configPath, s.config) } diff --git a/internal/service/docker.go b/internal/service/docker.go index c239297..514a5ba 100644 --- a/internal/service/docker.go +++ b/internal/service/docker.go @@ -11,17 +11,23 @@ import ( "github.com/sirupsen/logrus" ) +//go:generate mockgen -package mock -destination mock/docker.mock.go github.com/ethpandaops/contributoor-installer/internal/service DockerService + +type DockerService interface { + ServiceRunner +} + // DockerService is a basic service for interacting with the docker container. -type DockerService struct { +type dockerService struct { logger *logrus.Logger config *ContributoorConfig composePath string configPath string - configService *ConfigService + configService ConfigManager } // NewDockerService creates a new DockerService. -func NewDockerService(logger *logrus.Logger, configService *ConfigService) (*DockerService, error) { +func NewDockerService(logger *logrus.Logger, configService ConfigManager) (DockerService, error) { composePath, err := findComposeFile() if err != nil { return nil, fmt.Errorf("failed to find docker-compose.yml: %w", err) @@ -31,17 +37,17 @@ func NewDockerService(logger *logrus.Logger, configService *ConfigService) (*Doc return nil, fmt.Errorf("invalid docker-compose file: %w", err) } - return &DockerService{ + return &dockerService{ logger: logger, config: configService.Get(), composePath: filepath.Clean(composePath), - configPath: configService.configPath, + configPath: configService.GetConfigPath(), configService: configService, }, nil } // Start starts the docker container using docker-compose. -func (s *DockerService) Start() error { +func (s *dockerService) Start() error { cmd := exec.Command("docker", "compose", "-f", s.composePath, "up", "-d", "--pull", "always") //nolint:gosec // validateComposePath() and filepath.Clean() in-use. cmd.Env = s.getComposeEnv() @@ -55,7 +61,7 @@ func (s *DockerService) Start() error { } // Stop stops and removes the docker container using docker-compose. -func (s *DockerService) Stop() error { +func (s *dockerService) Stop() error { // Stop and remove containers, volumes, and networks //nolint:gosec // validateComposePath() and filepath.Clean() in-use. cmd := exec.Command("docker", "compose", "-f", s.composePath, "down", @@ -75,7 +81,7 @@ func (s *DockerService) Stop() error { } // IsRunning checks if the docker container is running. -func (s *DockerService) IsRunning() (bool, error) { +func (s *dockerService) IsRunning() (bool, error) { cmd := exec.Command("docker", "compose", "-f", s.composePath, "ps", "--format", "{{.State}}") //nolint:gosec // validateComposePath() and filepath.Clean() in-use. cmd.Env = s.getComposeEnv() @@ -95,7 +101,7 @@ func (s *DockerService) IsRunning() (bool, error) { } // Update pulls the latest image and restarts the container. -func (s *DockerService) Update() error { +func (s *dockerService) Update() error { //nolint:gosec // validateComposePath() and filepath.Clean() in-use. cmd := exec.Command("docker", "pull", fmt.Sprintf("ethpandaops/contributoor:%s", s.config.Version)) if output, err := cmd.CombinedOutput(); err != nil { @@ -111,7 +117,7 @@ func (s *DockerService) Update() error { return nil } -func (s *DockerService) getComposeEnv() []string { +func (s *dockerService) getComposeEnv() []string { return append(os.Environ(), fmt.Sprintf("CONTRIBUTOOR_CONFIG_PATH=%s", s.configService.GetConfigDir()), fmt.Sprintf("CONTRIBUTOOR_VERSION=%s", s.configService.Get().Version), diff --git a/internal/service/docker_test.go b/internal/service/docker_test.go index 094cb61..82aaafd 100644 --- a/internal/service/docker_test.go +++ b/internal/service/docker_test.go @@ -1,4 +1,4 @@ -package service +package service_test import ( "context" @@ -9,10 +9,13 @@ import ( "time" "github.com/docker/go-connections/nat" + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" + "go.uber.org/mock/gomock" ) const composeFile = ` @@ -43,19 +46,22 @@ func TestDockerService_Integration(t *testing.T) { port = 2375 tmpDir = t.TempDir() logger = logrus.New() - cfg = &ContributoorConfig{ + cfg = &service.ContributoorConfig{ Version: "latest", ContributoorDirectory: tmpDir, - RunMethod: RunMethodDocker, - } - cfgSvc = &ConfigService{ - logger: logger, - configPath: filepath.Join(tmpDir, "config.yaml"), - configDir: tmpDir, - config: cfg, + RunMethod: service.RunMethodDocker, } ) + // Create mock config manager + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(cfg).AnyTimes() + mockConfig.EXPECT().GetConfigDir().Return(tmpDir).AnyTimes() + mockConfig.EXPECT().GetConfigPath().Return(filepath.Join(tmpDir, "config.yaml")).AnyTimes() + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "docker:dind", @@ -88,8 +94,8 @@ func TestDockerService_Integration(t *testing.T) { containerPort, err := container.MappedPort(ctx, nat.Port(fmt.Sprintf("%d/tcp", port))) require.NoError(t, err) - // Create docker service with test docker host. - ds, err := NewDockerService(logger, cfgSvc) + // Create docker service with mock config + ds, err := service.NewDockerService(logger, mockConfig) require.NoError(t, err) // Set docker host to test container. diff --git a/internal/service/mock/binary.mock.go b/internal/service/mock/binary.mock.go new file mode 100644 index 0000000..06af7ab --- /dev/null +++ b/internal/service/mock/binary.mock.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ethpandaops/contributoor-installer/internal/service (interfaces: BinaryService) +// +// Generated by this command: +// +// mockgen -package mock -destination mock/binary.mock.go github.com/ethpandaops/contributoor-installer/internal/service BinaryService +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockBinaryService is a mock of BinaryService interface. +type MockBinaryService struct { + ctrl *gomock.Controller + recorder *MockBinaryServiceMockRecorder +} + +// MockBinaryServiceMockRecorder is the mock recorder for MockBinaryService. +type MockBinaryServiceMockRecorder struct { + mock *MockBinaryService +} + +// NewMockBinaryService creates a new mock instance. +func NewMockBinaryService(ctrl *gomock.Controller) *MockBinaryService { + mock := &MockBinaryService{ctrl: ctrl} + mock.recorder = &MockBinaryServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBinaryService) EXPECT() *MockBinaryServiceMockRecorder { + return m.recorder +} + +// IsRunning mocks base method. +func (m *MockBinaryService) IsRunning() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsRunning") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsRunning indicates an expected call of IsRunning. +func (mr *MockBinaryServiceMockRecorder) IsRunning() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRunning", reflect.TypeOf((*MockBinaryService)(nil).IsRunning)) +} + +// Start mocks base method. +func (m *MockBinaryService) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start. +func (mr *MockBinaryServiceMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockBinaryService)(nil).Start)) +} + +// Stop mocks base method. +func (m *MockBinaryService) Stop() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stop") + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop. +func (mr *MockBinaryServiceMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockBinaryService)(nil).Stop)) +} + +// Update mocks base method. +func (m *MockBinaryService) Update() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update") + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockBinaryServiceMockRecorder) Update() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockBinaryService)(nil).Update)) +} diff --git a/internal/service/mock/config.mock.go b/internal/service/mock/config.mock.go new file mode 100644 index 0000000..1425667 --- /dev/null +++ b/internal/service/mock/config.mock.go @@ -0,0 +1,110 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ethpandaops/contributoor-installer/internal/service (interfaces: ConfigManager) +// +// Generated by this command: +// +// mockgen -package mock -destination mock/config.mock.go github.com/ethpandaops/contributoor-installer/internal/service ConfigManager +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + service "github.com/ethpandaops/contributoor-installer/internal/service" + gomock "go.uber.org/mock/gomock" +) + +// MockConfigManager is a mock of ConfigManager interface. +type MockConfigManager struct { + ctrl *gomock.Controller + recorder *MockConfigManagerMockRecorder +} + +// MockConfigManagerMockRecorder is the mock recorder for MockConfigManager. +type MockConfigManagerMockRecorder struct { + mock *MockConfigManager +} + +// NewMockConfigManager creates a new mock instance. +func NewMockConfigManager(ctrl *gomock.Controller) *MockConfigManager { + mock := &MockConfigManager{ctrl: ctrl} + mock.recorder = &MockConfigManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConfigManager) EXPECT() *MockConfigManagerMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockConfigManager) Get() *service.ContributoorConfig { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get") + ret0, _ := ret[0].(*service.ContributoorConfig) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockConfigManagerMockRecorder) Get() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockConfigManager)(nil).Get)) +} + +// GetConfigDir mocks base method. +func (m *MockConfigManager) GetConfigDir() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfigDir") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetConfigDir indicates an expected call of GetConfigDir. +func (mr *MockConfigManagerMockRecorder) GetConfigDir() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigDir", reflect.TypeOf((*MockConfigManager)(nil).GetConfigDir)) +} + +// GetConfigPath mocks base method. +func (m *MockConfigManager) GetConfigPath() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfigPath") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetConfigPath indicates an expected call of GetConfigPath. +func (mr *MockConfigManagerMockRecorder) GetConfigPath() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigPath", reflect.TypeOf((*MockConfigManager)(nil).GetConfigPath)) +} + +// Save mocks base method. +func (m *MockConfigManager) Save() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Save") + ret0, _ := ret[0].(error) + return ret0 +} + +// Save indicates an expected call of Save. +func (mr *MockConfigManagerMockRecorder) Save() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockConfigManager)(nil).Save)) +} + +// Update mocks base method. +func (m *MockConfigManager) Update(arg0 func(*service.ContributoorConfig)) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockConfigManagerMockRecorder) Update(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockConfigManager)(nil).Update), arg0) +} diff --git a/internal/service/mock/docker.mock.go b/internal/service/mock/docker.mock.go new file mode 100644 index 0000000..3c32dea --- /dev/null +++ b/internal/service/mock/docker.mock.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ethpandaops/contributoor-installer/internal/service (interfaces: DockerService) +// +// Generated by this command: +// +// mockgen -package mock -destination mock/docker.mock.go github.com/ethpandaops/contributoor-installer/internal/service DockerService +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockDockerService is a mock of DockerService interface. +type MockDockerService struct { + ctrl *gomock.Controller + recorder *MockDockerServiceMockRecorder +} + +// MockDockerServiceMockRecorder is the mock recorder for MockDockerService. +type MockDockerServiceMockRecorder struct { + mock *MockDockerService +} + +// NewMockDockerService creates a new mock instance. +func NewMockDockerService(ctrl *gomock.Controller) *MockDockerService { + mock := &MockDockerService{ctrl: ctrl} + mock.recorder = &MockDockerServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDockerService) EXPECT() *MockDockerServiceMockRecorder { + return m.recorder +} + +// IsRunning mocks base method. +func (m *MockDockerService) IsRunning() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsRunning") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsRunning indicates an expected call of IsRunning. +func (mr *MockDockerServiceMockRecorder) IsRunning() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRunning", reflect.TypeOf((*MockDockerService)(nil).IsRunning)) +} + +// Start mocks base method. +func (m *MockDockerService) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start. +func (mr *MockDockerServiceMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDockerService)(nil).Start)) +} + +// Stop mocks base method. +func (m *MockDockerService) Stop() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stop") + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop. +func (mr *MockDockerServiceMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockDockerService)(nil).Stop)) +} + +// Update mocks base method. +func (m *MockDockerService) Update() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update") + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockDockerServiceMockRecorder) Update() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockDockerService)(nil).Update)) +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..f224e39 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,40 @@ +package service + +// RunMethods defines the possible ways to run the contributoor service. +const ( + RunMethodDocker = "docker" + RunMethodBinary = "binary" +) + +// ServiceRunner handles operations for the various run methods. +type ServiceRunner interface { + // Start starts the service. + Start() error + + // Stop stops the service. + Stop() error + + // Update updates the service. + Update() error + + // IsRunning checks if the service is running. + IsRunning() (bool, error) +} + +// ConfigManager defines the interface for configuration management. +type ConfigManager interface { + // Update modifies the configuration using the provided update function. + Update(updates func(*ContributoorConfig)) error + + // Get returns the current configuration. + Get() *ContributoorConfig + + // GetConfigDir returns the directory containing the config file. + GetConfigDir() string + + // GetConfigPath returns the path of the file config. + GetConfigPath() string + + // Save persists the current configuration to disk. + Save() error +}