From 664058d5f0d630991e603ae2b3739ec8c8e7b1aa Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 2 Jun 2019 11:37:21 +1000 Subject: [PATCH] make config type safe --- pkg/app/app.go | 81 +------ pkg/commands/container.go | 2 +- pkg/commands/docker.go | 6 +- pkg/commands/dummies.go | 4 - pkg/commands/os.go | 8 +- pkg/commands/os_test.go | 51 ----- pkg/commands/service.go | 4 +- pkg/config/app_config.go | 294 +++++++++--------------- pkg/config/config_default_platform.go | 10 +- pkg/config/config_linux.go | 10 +- pkg/config/config_windows.go | 10 +- pkg/gui/gui.go | 39 ++-- pkg/gui/keybindings.go | 6 - pkg/gui/services_panel.go | 2 +- pkg/gui/status_panel.go | 11 +- pkg/gui/theme.go | 11 +- pkg/gui/updates.go | 65 ------ pkg/log/log.go | 68 ++++++ pkg/updates/updates.go | 313 -------------------------- 19 files changed, 225 insertions(+), 770 deletions(-) delete mode 100644 pkg/gui/updates.go create mode 100644 pkg/log/log.go delete mode 100644 pkg/updates/updates.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 39bec4f7..c164211f 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,17 +2,12 @@ package app import ( "io" - "io/ioutil" - "os" - "path/filepath" - "github.com/heroku/rollrus" "github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/gui" "github.com/jesseduffield/lazydocker/pkg/i18n" - "github.com/jesseduffield/lazydocker/pkg/updates" - "github.com/shibukawa/configdir" + "github.com/jesseduffield/lazydocker/pkg/log" "github.com/sirupsen/logrus" ) @@ -20,98 +15,32 @@ import ( type App struct { closers []io.Closer - Config config.AppConfigurer + Config *config.AppConfig Log *logrus.Entry OSCommand *commands.OSCommand DockerCommand *commands.DockerCommand Gui *gui.Gui Tr *i18n.Localizer - Updater *updates.Updater // may only need this on the Gui -} - -func newProductionLogger(config config.AppConfigurer) *logrus.Logger { - log := logrus.New() - log.Out = ioutil.Discard - log.SetLevel(logrus.ErrorLevel) - return log -} - -func globalConfigDir() string { - configDirs := configdir.New("jesseduffield", "lazydocker") - configDir := configDirs.QueryFolders(configdir.Global)[0] - return configDir.Path -} - -func getLogLevel() logrus.Level { - strLevel := os.Getenv("LOG_LEVEL") - level, err := logrus.ParseLevel(strLevel) - if err != nil { - return logrus.DebugLevel - } - return level -} - -func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger { - log := logrus.New() - log.SetLevel(getLogLevel()) - file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err != nil { - panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function) - } - log.SetOutput(file) - return log -} - -func newLogger(config config.AppConfigurer) *logrus.Entry { - var log *logrus.Logger - environment := "production" - if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" { - environment = "development" - log = newDevelopmentLogger(config) - } else { - log = newProductionLogger(config) - } - - // highly recommended: tail -f development.log | humanlog - // https://github.com/aybabtme/humanlog - log.Formatter = &logrus.JSONFormatter{} - - if config.GetUserConfig().GetString("reporting") == "on" { - // this isn't really a secret token: it only has permission to push new rollbar items - hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment) // using the same key that lazygit uses for now - log.Hooks.Add(hook) - } - return log.WithFields(logrus.Fields{ - "debug": config.GetDebug(), - "version": config.GetVersion(), - "commit": config.GetCommit(), - "buildDate": config.GetBuildDate(), - }) } // NewApp bootstrap a new application -func NewApp(config config.AppConfigurer) (*App, error) { +func NewApp(config *config.AppConfig) (*App, error) { app := &App{ closers: []io.Closer{}, Config: config, } var err error - app.Log = newLogger(config) + app.Log = log.NewLogger(config, "23432119147a4367abf7c0de2aa99a2d") app.Tr = i18n.NewLocalizer(app.Log) app.OSCommand = commands.NewOSCommand(app.Log, config) - app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr) - if err != nil { - return app, err - } - // here is the place to make use of the docker-compose.yml file in the current directory app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config) if err != nil { return app, err } - app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.Updater) + app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config) if err != nil { return app, err } diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 6c9f5a8c..f178c0cd 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -292,7 +292,7 @@ func (c *Container) Restart() error { // RestartService restarts the container func (c *Container) RestartService() error { - templateString := c.OSCommand.Config.GetUserConfig().GetString("commandTemplates.restartService") + templateString := c.OSCommand.Config.UserConfig.CommandTemplates.RestartService command := utils.ApplyTemplate(templateString, c) return c.OSCommand.RunCommand(command) } diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 3af999bf..d7d68417 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -21,13 +21,13 @@ type DockerCommand struct { Log *logrus.Entry OSCommand *OSCommand Tr *i18n.Localizer - Config config.AppConfigurer + Config *config.AppConfig Client *client.Client InDockerComposeProject bool } // NewDockerCommand it runs git commands -func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*DockerCommand, error) { +func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, config *config.AppConfig) (*DockerCommand, error) { cli, err := client.NewEnvClient() if err != nil { return nil, err @@ -155,7 +155,7 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { return nil, nil } - composeCommand := c.Config.GetUserConfig().GetString("commandTemplates.dockerCompose") + composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand)) if err != nil { return nil, err diff --git a/pkg/commands/dummies.go b/pkg/commands/dummies.go index 7fd0c366..a2f4869a 100644 --- a/pkg/commands/dummies.go +++ b/pkg/commands/dummies.go @@ -6,8 +6,6 @@ import ( "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/sirupsen/logrus" - "github.com/spf13/viper" - yaml "gopkg.in/yaml.v2" ) // This file exports dummy constructors for use by tests in other packages @@ -26,9 +24,7 @@ func NewDummyAppConfig() *config.AppConfig { BuildDate: "", Debug: false, BuildSource: "", - UserConfig: viper.New(), } - _ = yaml.Unmarshal([]byte{}, appConfig.AppState) return appConfig } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 9d6724b5..9c8d4dbc 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -32,14 +32,14 @@ type Platform struct { type OSCommand struct { Log *logrus.Entry Platform *Platform - Config config.AppConfigurer + Config *config.AppConfig command func(string, ...string) *exec.Cmd getGlobalGitConfig func(string) (string, error) getenv func(string) string } // NewOSCommand os command runner -func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand { +func NewOSCommand(log *logrus.Entry, config *config.AppConfig) *OSCommand { return &OSCommand{ Log: log, Platform: getPlatform(), @@ -154,7 +154,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { // OpenFile opens a file with the given func (c *OSCommand) OpenFile(filename string) error { - commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand") + commandTemplate := c.Config.UserConfig.OS.OpenCommand templateValues := map[string]string{ "filename": c.Quote(filename), } @@ -166,7 +166,7 @@ func (c *OSCommand) OpenFile(filename string) error { // OpenLink opens a file with the given func (c *OSCommand) OpenLink(link string) error { - commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand") + commandTemplate := c.Config.UserConfig.OS.OpenLinkCommand templateValues := map[string]string{ "link": c.Quote(link), } diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go index 2141b0e1..4980560c 100644 --- a/pkg/commands/os_test.go +++ b/pkg/commands/os_test.go @@ -58,57 +58,6 @@ func TestOSCommandRunCommand(t *testing.T) { } } -// TestOSCommandOpenFile is a function. -func TestOSCommandOpenFile(t *testing.T) { - type scenario struct { - filename string - command func(string, ...string) *exec.Cmd - test func(error) - } - - scenarios := []scenario{ - { - "test", - func(name string, arg ...string) *exec.Cmd { - return exec.Command("exit", "1") - }, - func(err error) { - assert.Error(t, err) - }, - }, - { - "test", - func(name string, arg ...string) *exec.Cmd { - assert.Equal(t, "open", name) - assert.Equal(t, []string{"test"}, arg) - return exec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - }, - { - "filename with spaces", - func(name string, arg ...string) *exec.Cmd { - assert.Equal(t, "open", name) - assert.Equal(t, []string{"filename with spaces"}, arg) - return exec.Command("echo") - }, - func(err error) { - assert.NoError(t, err) - }, - }, - } - - for _, s := range scenarios { - OSCmd := NewDummyOSCommand() - OSCmd.command = s.command - OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}") - - s.test(OSCmd.OpenFile(s.filename)) - } -} - // TestOSCommandEditFile is a function. func TestOSCommandEditFile(t *testing.T) { type scenario struct { diff --git a/pkg/commands/service.go b/pkg/commands/service.go index 84a1ee92..d1e22847 100644 --- a/pkg/commands/service.go +++ b/pkg/commands/service.go @@ -37,14 +37,14 @@ func (s *Service) Remove(options types.ContainerRemoveOptions) error { // Stop stops the service's containers func (s *Service) Stop() error { - templateString := s.OSCommand.Config.GetUserConfig().GetString("commandTemplates.stopService") + templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StopService command := utils.ApplyTemplate(templateString, s) return s.OSCommand.RunCommand(command) } // Restart restarts the service func (s *Service) Restart() error { - templateString := s.OSCommand.Config.GetUserConfig().GetString("commandTemplates.restartService") + templateString := s.OSCommand.Config.UserConfig.CommandTemplates.RestartService command := utils.ApplyTemplate(templateString, s) return s.OSCommand.RunCommand(command) } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 646f9d4a..f46b4594 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -1,257 +1,143 @@ package config import ( - "bytes" "io/ioutil" - "os" "path/filepath" "github.com/shibukawa/configdir" - "github.com/spf13/viper" yaml "gopkg.in/yaml.v2" ) -// AppConfig contains the base configuration fields required for lazydocker. +// AppConfig contains the base configuration fields required for lazygit. type AppConfig struct { Debug bool `long:"debug" env:"DEBUG" default:"false"` Version string `long:"version" env:"VERSION" default:"unversioned"` Commit string `long:"commit" env:"COMMIT"` BuildDate string `long:"build-date" env:"BUILD_DATE"` - Name string `long:"name" env:"NAME" default:"lazydocker"` + Name string `long:"name" env:"NAME" default:"lazygit"` BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""` - UserConfig *viper.Viper - AppState *AppState - IsNewRepo bool -} - -// AppConfigurer interface allows individual app config structs to inherit Fields -// from AppConfig and still be used by lazydocker. -type AppConfigurer interface { - GetDebug() bool - GetVersion() string - GetCommit() string - GetBuildDate() string - GetName() string - GetBuildSource() string - GetUserConfig() *viper.Viper - GetAppState() *AppState - WriteToUserConfig(string, string) error - SaveAppState() error - LoadAppState() error - SetIsNewRepo(bool) - GetIsNewRepo() bool + UserConfig *UserConfig + ConfigDir string } // NewAppConfig makes a new app config func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) { - userConfig, err := LoadConfig("config", true) + configDir, err := findOrCreateConfigDir(name) if err != nil { return nil, err } - if os.Getenv("DEBUG") == "TRUE" { - debuggingFlag = true + userConfig, err := loadUserConfigWithDefaults(configDir) + if err != nil { + return nil, err } appConfig := &AppConfig{ - Name: "lazydocker", + Name: name, Version: version, Commit: commit, BuildDate: date, - Debug: debuggingFlag, + Debug: true, // TODO: restore os.Getenv("DEBUG") == "TRUE" BuildSource: buildSource, UserConfig: userConfig, - AppState: &AppState{}, - IsNewRepo: false, - } - - if err := appConfig.LoadAppState(); err != nil { - return nil, err + ConfigDir: configDir, } return appConfig, nil } -// GetIsNewRepo returns known repo boolean -func (c *AppConfig) GetIsNewRepo() bool { - return c.IsNewRepo +func findOrCreateConfigDir(projectName string) (string, error) { + configDirs := configdir.New("jesseduffield", projectName) + folders := configDirs.QueryFolders(configdir.Global) + + if err := folders[0].CreateParentDir("foo"); err != nil { + return "", err + } + + return folders[0].Path, nil } -// SetIsNewRepo set if the current repo is known -func (c *AppConfig) SetIsNewRepo(toSet bool) { - c.IsNewRepo = toSet +func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) { + config := GetDefaultConfig() + + return loadUserConfig(configDir, &config) } -// GetDebug returns debug flag -func (c *AppConfig) GetDebug() bool { - return c.Debug -} - -// GetVersion returns debug flag -func (c *AppConfig) GetVersion() string { - return c.Version -} - -// GetCommit returns debug flag -func (c *AppConfig) GetCommit() string { - return c.Commit -} - -// GetBuildDate returns debug flag -func (c *AppConfig) GetBuildDate() string { - return c.BuildDate -} - -// GetName returns debug flag -func (c *AppConfig) GetName() string { - return c.Name -} - -// GetBuildSource returns the source of the build. For builds from goreleaser -// this will be binaryBuild -func (c *AppConfig) GetBuildSource() string { - return c.BuildSource -} - -// GetUserConfig returns the user config -func (c *AppConfig) GetUserConfig() *viper.Viper { - return c.UserConfig -} - -// GetAppState returns the app state -func (c *AppConfig) GetAppState() *AppState { - return c.AppState -} - -func newViper(filename string) (*viper.Viper, error) { - v := viper.New() - v.SetConfigType("yaml") - v.SetConfigName(filename) - return v, nil -} - -// LoadConfig gets the user's config -func LoadConfig(filename string, withDefaults bool) (*viper.Viper, error) { - v, err := newViper(filename) +func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) { + content, err := ioutil.ReadFile(filepath.Join(configDir, "config.yml")) if err != nil { return nil, err } - if withDefaults { - if err = LoadDefaults(v, GetDefaultConfig()); err != nil { - return nil, err - } - if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil { - return nil, err - } - } - if err = LoadAndMergeFile(v, filename+".yml"); err != nil { + + if err := yaml.Unmarshal(content, base); err != nil { return nil, err } - return v, nil + + return base, nil } -// LoadDefaults loads in the defaults defined in this file -func LoadDefaults(v *viper.Viper, defaults []byte) error { - return v.MergeConfig(bytes.NewBuffer(defaults)) +type UserConfig struct { + Gui GuiConfig + Reporting string + ConfirmOnQuit bool + CommandTemplates CommandTemplatesConfig + OS OSConfig + Update UpdateConfig } -func prepareConfigFile(filename string) (string, error) { - // chucking my name there is not for vanity purposes, the xdg spec (and that - // function) requires a vendor name. May as well line up with github - configDirs := configdir.New("jesseduffield", "lazydocker") - folder := configDirs.QueryFolderContainsFile(filename) - if folder == nil { - // create the file as empty - folders := configDirs.QueryFolders(configdir.Global) - if err := folders[0].WriteFile(filename, []byte{}); err != nil { - return "", err - } - folder = configDirs.QueryFolderContainsFile(filename) - } - return filepath.Join(folder.Path, filename), nil +type ThemeConfig struct { + ActiveBorderColor []string + InactiveBorderColor []string + OptionsTextColor []string } -// LoadAndMergeFile Loads the config/state file, creating -// the file has an empty one if it does not exist -func LoadAndMergeFile(v *viper.Viper, filename string) error { - configPath, err := prepareConfigFile(filename) - if err != nil { - return err - } - - v.AddConfigPath(filepath.Dir(configPath)) - return v.MergeInConfig() +type GuiConfig struct { + ScrollHeight int + ScrollPastBottom bool + MouseEvents bool + Theme ThemeConfig } -// WriteToUserConfig adds a key/value pair to the user's config and saves it -func (c *AppConfig) WriteToUserConfig(key, value string) error { - // reloading the user config directly (without defaults) so that we're not - // writing any defaults back to the user's config - v, err := LoadConfig("config", false) - if err != nil { - return err - } - - v.Set(key, value) - return v.WriteConfig() +type CommandTemplatesConfig struct { + RestartService string + DockerCompose string + StopService string } -// SaveAppState marhsalls the AppState struct and writes it to the disk -func (c *AppConfig) SaveAppState() error { - marshalledAppState, err := yaml.Marshal(c.AppState) - if err != nil { - return err - } - - filepath, err := prepareConfigFile("state.yml") - if err != nil { - return err - } - - return ioutil.WriteFile(filepath, marshalledAppState, 0644) +type OSConfig struct { + OpenCommand string + OpenLinkCommand string } -// LoadAppState loads recorded AppState from file -func (c *AppConfig) LoadAppState() error { - filepath, err := prepareConfigFile("state.yml") - if err != nil { - return err - } - appStateBytes, err := ioutil.ReadFile(filepath) - if err != nil { - return err - } - if len(appStateBytes) == 0 { - return yaml.Unmarshal(getDefaultAppState(), c.AppState) - } - return yaml.Unmarshal(appStateBytes, c.AppState) +type UpdateConfig struct { + Method string } // GetDefaultConfig returns the application default configuration -func GetDefaultConfig() []byte { - return []byte( - `gui: - ## stuff relating to the UI - scrollHeight: 2 - scrollPastBottom: false - mouseEvents: false # will default to true when the feature is complete - theme: - activeBorderColor: - - white - - bold - inactiveBorderColor: - - white - optionsTextColor: - - blue -update: - method: prompt # can be: prompt | background | never - days: 14 # how often a update is checked for -reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' -confirmOnQuit: false -commandTemplates: - restartService: "docker-compose restart {{ .Name }}" - dockerCompose: "apdev compose" -`) +func GetDefaultConfig() UserConfig { + return UserConfig{ + Gui: GuiConfig{ + ScrollHeight: 2, + ScrollPastBottom: false, + MouseEvents: false, + Theme: ThemeConfig{ + ActiveBorderColor: []string{"white", "bold"}, + InactiveBorderColor: []string{"white"}, + OptionsTextColor: []string{"blue"}, + }, + }, + Reporting: "undetermined", + ConfirmOnQuit: false, + CommandTemplates: CommandTemplatesConfig{ + RestartService: "docker-compose restart {{ .Name }}", + DockerCompose: "apdev compose", + StopService: "apdev stop {{ .Name }}", + }, + OS: GetPlatformDefaultConfig(), + Update: UpdateConfig{ + Method: "never", + }, + } } // AppState stores data between runs of the app like when the last update check @@ -265,3 +151,25 @@ func getDefaultAppState() []byte { lastUpdateCheck: 0 `) } + +func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) error { + userConfig, err := loadUserConfig(c.ConfigDir, &UserConfig{}) + if err != nil { + return err + } + + if err := updateConfig(userConfig); err != nil { + return err + } + + out, err := yaml.Marshal(userConfig) + if err != nil { + return err + } + + return ioutil.WriteFile(c.ConfigFilename(), out, 0666) +} + +func (c *AppConfig) ConfigFilename() string { + return filepath.Join(c.ConfigDir, "config.yml") +} diff --git a/pkg/config/config_default_platform.go b/pkg/config/config_default_platform.go index df205c0d..e9e2f63b 100644 --- a/pkg/config/config_default_platform.go +++ b/pkg/config/config_default_platform.go @@ -3,9 +3,9 @@ package config // GetPlatformDefaultConfig gets the defaults for the platform -func GetPlatformDefaultConfig() []byte { - return []byte( - `os: - openCommand: 'open {{filename}}' - openLinkCommand: 'open {{link}}'`) +func GetPlatformDefaultConfig() OSConfig { + return OSConfig{ + OpenCommand: "open {{filename}}", + OpenLinkCommand: "open {{link}}", + } } diff --git a/pkg/config/config_linux.go b/pkg/config/config_linux.go index 2dfbdb1c..b3d7f42b 100644 --- a/pkg/config/config_linux.go +++ b/pkg/config/config_linux.go @@ -1,9 +1,9 @@ package config // GetPlatformDefaultConfig gets the defaults for the platform -func GetPlatformDefaultConfig() []byte { - return []byte( - `os: - openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"' - openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`) +func GetPlatformDefaultConfig() OSConfig { + return OSConfig{ + OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`, + OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`, + } } diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 6f656031..cea72242 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -1,9 +1,9 @@ package config // GetPlatformDefaultConfig gets the defaults for the platform -func GetPlatformDefaultConfig() []byte { - return []byte( - `os: - openCommand: 'cmd /c "start "" {{filename}}"' - openLinkCommand: 'cmd /c "start "" {{link}}"'`) +func GetPlatformDefaultConfig() OSConfig { + return OSConfig{ + OpenCommand: `cmd /c "start "" {{filename}}"`, + OpenLinkCommand: `cmd /c "start "" {{link}}"`, + } } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 580d3f4b..2d6203f0 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -24,7 +24,6 @@ import ( "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/tasks" - "github.com/jesseduffield/lazydocker/pkg/updates" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/sirupsen/logrus" ) @@ -69,10 +68,9 @@ type Gui struct { OSCommand *commands.OSCommand SubProcess *exec.Cmd State guiState - Config config.AppConfigurer + Config *config.AppConfig Tr *i18n.Localizer Errors SentinelErrors - Updater *updates.Updater statusManager *statusManager waitForIntro sync.WaitGroup T *tasks.TaskManager @@ -116,7 +114,6 @@ type guiState struct { MenuItemCount int // can't store the actual list because it's of interface{} type PreviousView string Platform commands.Platform - Updating bool Panels *panelStates SubProcessOutput string MainProcessMutex sync.Mutex @@ -124,7 +121,7 @@ type guiState struct { } // NewGui builds a new gui handler -func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) { +func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config *config.AppConfig) (*Gui, error) { initialState := guiState{ Containers: make([]*commands.Container, 0), @@ -157,7 +154,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand State: initialState, Config: config, Tr: tr, - Updater: updater, statusManager: &statusManager{}, T: tasks.NewTaskManager(), } @@ -171,7 +167,7 @@ func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error { mainView, _ := g.View("main") mainView.Autoscroll = false ox, oy := mainView.Origin() - newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight")))) + newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight))) return mainView.SetOrigin(ox, newOy) } @@ -179,13 +175,13 @@ func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error { mainView, _ := g.View("main") ox, oy := mainView.Origin() y := oy - if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") { + if !gui.Config.UserConfig.Gui.ScrollPastBottom { _, sy := mainView.Size() y += sy } // for some reason we can't work out whether we've hit the bottomq // there is a large discrepancy in the origin's y value and the length of BufferLines - return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight")) + return mainView.SetOrigin(ox, oy+gui.Config.UserConfig.Gui.ScrollHeight) } func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error { @@ -255,7 +251,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { g.Highlight = true width, height := g.Size() - information := gui.Config.GetVersion() + information := gui.Config.Version minimumHeight := 9 minimumWidth := 10 @@ -453,15 +449,13 @@ func (gui *Gui) layout(g *gocui.Gui) error { } func (gui *Gui) loadNewDirectory() error { - gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false) - gui.waitForIntro.Done() if err := gui.refreshSidePanels(gui.g); err != nil { return err } - if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" { + if gui.Config.UserConfig.Reporting == "undetermined" { if err := gui.promptAnonymousReporting(); err != nil { return err } @@ -472,10 +466,16 @@ func (gui *Gui) loadNewDirectory() error { func (gui *Gui) promptAnonymousReporting() error { return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error { gui.waitForIntro.Done() - return gui.Config.WriteToUserConfig("reporting", "on") + return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error { + userConfig.Reporting = "on" + return nil + }) }, func(g *gocui.Gui, v *gocui.View) error { gui.waitForIntro.Done() - return gui.Config.WriteToUserConfig("reporting", "off") + return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error { + userConfig.Reporting = "off" + return nil + }) }) } @@ -512,7 +512,7 @@ func (gui *Gui) Run() error { } defer g.Close() - if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") { + if gui.Config.UserConfig.Gui.MouseEvents { g.Mouse = true } @@ -522,7 +522,7 @@ func (gui *Gui) Run() error { return err } - if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" { + if gui.Config.UserConfig.Reporting == "undetermined" { gui.waitForIntro.Add(2) } else { gui.waitForIntro.Add(1) @@ -600,10 +600,7 @@ func (gui *Gui) runCommand() error { } func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { - if gui.State.Updating { - return gui.createUpdateQuitConfirmation(g, v) - } - if gui.Config.GetUserConfig().GetBool("confirmOnQuit") { + if gui.Config.UserConfig.ConfirmOnQuit { return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit }, nil) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index ce666353..6e36d6b6 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -116,12 +116,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleOpenConfig, Description: gui.Tr.SLocalize("OpenConfig"), - }, { - ViewName: "status", - Key: 'u', - Modifier: gocui.ModNone, - Handler: gui.handleCheckForUpdate, - Description: gui.Tr.SLocalize("checkForUpdate"), }, { ViewName: "menu", diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 14e49ec1..c05d164f 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -186,7 +186,7 @@ func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error { return nil } - composeCommand := gui.Config.GetUserConfig().GetString("commandTemplates.dockerCompose") + composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose options := []*removeServiceOption{ { diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 3f77fad0..be09aa01 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "path/filepath" "strings" "github.com/jesseduffield/gocui" @@ -24,11 +25,6 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error { return nil } -func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { - gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) - return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates")) -} - func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil @@ -51,11 +47,12 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error { - return gui.openFile(gui.Config.GetUserConfig().ConfigFileUsed()) + filename := filepath.Join(gui.Config.ConfigDir, "config.yml") + return gui.openFile(filename) } func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error { - filename := gui.Config.GetUserConfig().ConfigFileUsed() + filename := filepath.Join(gui.Config.ConfigDir, "config.yml") return gui.editFile(filename) } diff --git a/pkg/gui/theme.go b/pkg/gui/theme.go index dbc8b904..62c2c5a9 100644 --- a/pkg/gui/theme.go +++ b/pkg/gui/theme.go @@ -38,17 +38,12 @@ func (gui *Gui) GetColor(keys []string) gocui.Attribute { // GetOptionsPanelTextColor gets the color of the options panel text func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) { - userConfig := gui.Config.GetUserConfig() - optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor") - return gui.GetColor(optionsColor), nil + return gui.GetColor(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil } // SetColorScheme sets the color scheme for the app based on the user config func (gui *Gui) SetColorScheme() error { - userConfig := gui.Config.GetUserConfig() - activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor") - inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor") - gui.g.FgColor = gui.GetColor(inactiveBorderColor) - gui.g.SelFgColor = gui.GetColor(activeBorderColor) + gui.g.FgColor = gui.GetColor(gui.Config.UserConfig.Gui.Theme.InactiveBorderColor) + gui.g.SelFgColor = gui.GetColor(gui.Config.UserConfig.Gui.Theme.ActiveBorderColor) return nil } diff --git a/pkg/gui/updates.go b/pkg/gui/updates.go deleted file mode 100644 index ff5a0afb..00000000 --- a/pkg/gui/updates.go +++ /dev/null @@ -1,65 +0,0 @@ -package gui - -import "github.com/jesseduffield/gocui" - -func (gui *Gui) showUpdatePrompt(newVersion string) error { - title := "New version available!" - message := "Download latest version? (enter/esc)" - currentView := gui.g.CurrentView() - return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error { - gui.startUpdating(newVersion) - return nil - }, nil) -} - -func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error { - if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) - } - if newVersion == "" { - return gui.createErrorPanel(gui.g, "New version not found") - } - return gui.showUpdatePrompt(newVersion) -} - -func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error { - if err != nil { - // ignoring the error for now so that I'm not annoying users - gui.Log.Error(err.Error()) - return nil - } - if newVersion == "" { - return nil - } - if gui.Config.GetUserConfig().Get("update.method") == "background" { - gui.startUpdating(newVersion) - return nil - } - return gui.showUpdatePrompt(newVersion) -} - -func (gui *Gui) startUpdating(newVersion string) { - gui.State.Updating = true - gui.statusManager.addWaitingStatus("updating") - gui.Updater.Update(newVersion, gui.onUpdateFinish) -} - -func (gui *Gui) onUpdateFinish(err error) error { - gui.State.Updating = false - gui.statusManager.removeStatus("updating") - if err := gui.renderString(gui.g, "appStatus", ""); err != nil { - return err - } - if err != nil { - return gui.createErrorPanel(gui.g, "Update failed: "+err.Error()) - } - return nil -} - -func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error { - title := "Currently Updating" - message := "An update is in progress. Are you sure you want to quit?" - return gui.createConfirmationPanel(gui.g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit - }, nil) -} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 00000000..e33f93ed --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,68 @@ +package log + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/heroku/rollrus" + "github.com/jesseduffield/lazydocker/pkg/config" + "github.com/sirupsen/logrus" +) + +// NewLogger returns a new logger +func NewLogger(config *config.AppConfig, rollrusHook string) *logrus.Entry { + var log *logrus.Logger + environment := "production" + if true || config.Debug || os.Getenv("DEBUG") == "TRUE" { // TODO: remove true here + environment = "development" + log = newDevelopmentLogger(config) + } else { + log = newProductionLogger() + } + + // highly recommended: tail -f development.log | humanlog + // https://github.com/aybabtme/humanlog + log.Formatter = &logrus.JSONFormatter{} + + if config.UserConfig.Reporting == "on" && rollrusHook != "" { + // this isn't really a secret token: it only has permission to push new rollbar items + hook := rollrus.NewHook(rollrusHook, environment) + log.Hooks.Add(hook) + } + return log.WithFields(logrus.Fields{ + "debug": config.Debug, + "version": config.Version, + "commit": config.Commit, + "buildDate": config.BuildDate, + }) +} + +func getLogLevel() logrus.Level { + strLevel := os.Getenv("LOG_LEVEL") + level, err := logrus.ParseLevel(strLevel) + if err != nil { + return logrus.DebugLevel + } + return level +} + +func newDevelopmentLogger(config *config.AppConfig) *logrus.Logger { + log := logrus.New() + log.SetLevel(getLogLevel()) + file, err := os.OpenFile(filepath.Join(config.ConfigDir, "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + fmt.Println("unable to log to file") + os.Exit(1) + } + log.SetOutput(file) + return log +} + +func newProductionLogger() *logrus.Logger { + log := logrus.New() + log.Out = ioutil.Discard + log.SetLevel(logrus.ErrorLevel) + return log +} diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go deleted file mode 100644 index bb7cc329..00000000 --- a/pkg/updates/updates.go +++ /dev/null @@ -1,313 +0,0 @@ -package updates - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/go-errors/errors" - - "github.com/kardianos/osext" - - getter "github.com/jesseduffield/go-getter" - "github.com/jesseduffield/lazydocker/pkg/commands" - "github.com/jesseduffield/lazydocker/pkg/config" - "github.com/jesseduffield/lazydocker/pkg/i18n" - "github.com/sirupsen/logrus" -) - -// Updater checks for updates and does updates -type Updater struct { - Log *logrus.Entry - Config config.AppConfigurer - OSCommand *commands.OSCommand - Tr *i18n.Localizer -} - -// Updaterer implements the check and update methods -type Updaterer interface { - CheckForNewUpdate() - Update() -} - -const ( - PROJECT_URL = "https://github.com/jesseduffield/lazydocker" -) - -// NewUpdater creates a new updater -func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *commands.OSCommand, tr *i18n.Localizer) (*Updater, error) { - contextLogger := log.WithField("context", "updates") - - return &Updater{ - Log: contextLogger, - Config: config, - OSCommand: osCommand, - Tr: tr, - }, nil -} - -func (u *Updater) getLatestVersionNumber() (string, error) { - req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil) - if err != nil { - return "", err - } - req.Header.Set("Accept", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - dec := json.NewDecoder(resp.Body) - data := struct { - TagName string `json:"tag_name"` - }{} - if err := dec.Decode(&data); err != nil { - return "", err - } - - return data.TagName, nil -} - -// RecordLastUpdateCheck records last time an update check was performed -func (u *Updater) RecordLastUpdateCheck() error { - u.Config.GetAppState().LastUpdateCheck = time.Now().Unix() - return u.Config.SaveAppState() -} - -// expecting version to be of the form `v12.34.56` -func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool { - if oldVersion == "unversioned" { - return false - } - oldVersion = strings.TrimPrefix(oldVersion, "v") - newVersion = strings.TrimPrefix(newVersion, "v") - return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0] -} - -func (u *Updater) checkForNewUpdate() (string, error) { - u.Log.Info("Checking for an updated version") - currentVersion := u.Config.GetVersion() - if err := u.RecordLastUpdateCheck(); err != nil { - return "", err - } - - newVersion, err := u.getLatestVersionNumber() - if err != nil { - return "", err - } - u.Log.Info("Current version is " + currentVersion) - u.Log.Info("New version is " + newVersion) - - if newVersion == currentVersion { - return "", errors.New(u.Tr.SLocalize("OnLatestVersionErr")) - } - - if u.majorVersionDiffers(currentVersion, newVersion) { - errMessage := u.Tr.TemplateLocalize( - "MajorVersionErr", - i18n.Teml{ - "newVersion": newVersion, - "currentVersion": currentVersion, - }, - ) - return "", errors.New(errMessage) - } - - rawUrl, err := u.getBinaryUrl(newVersion) - if err != nil { - return "", err - } - u.Log.Info("Checking for resource at url " + rawUrl) - if !u.verifyResourceFound(rawUrl) { - errMessage := u.Tr.TemplateLocalize( - "CouldNotFindBinaryErr", - i18n.Teml{ - "url": rawUrl, - }, - ) - return "", errors.New(errMessage) - } - u.Log.Info("Verified resource is available, ready to update") - - return newVersion, nil -} - -// CheckForNewUpdate checks if there is an available update -func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequested bool) { - if !userRequested && u.skipUpdateCheck() { - return - } - - go func() { - newVersion, err := u.checkForNewUpdate() - if err = onFinish(newVersion, err); err != nil { - u.Log.Error(err) - } - }() -} - -func (u *Updater) skipUpdateCheck() bool { - // will remove the check for windows after adding a manifest file asking for - // the required permissions - if runtime.GOOS == "windows" { - u.Log.Info("Updating is currently not supported for windows until we can fix permission issues") - return true - } - - if u.Config.GetVersion() == "unversioned" { - u.Log.Info("Current version is not built from an official release so we won't check for an update") - return true - } - - if u.Config.GetBuildSource() != "buildBinary" { - u.Log.Info("Binary is not built with the buildBinary flag so we won't check for an update") - return true - } - - userConfig := u.Config.GetUserConfig() - if userConfig.Get("update.method") == "never" { - u.Log.Info("Update method is set to never so we won't check for an update") - return true - } - - currentTimestamp := time.Now().Unix() - lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck - days := userConfig.GetInt64("update.days") - - if (currentTimestamp-lastUpdateCheck)/(60*60*24) < days { - u.Log.Info("Last update was too recent so we won't check for an update") - return true - } - - return false -} - -func (u *Updater) mappedOs(os string) string { - osMap := map[string]string{ - "darwin": "Darwin", - "linux": "Linux", - "windows": "Windows", - } - result, found := osMap[os] - if found { - return result - } - return os -} - -func (u *Updater) mappedArch(arch string) string { - archMap := map[string]string{ - "386": "32-bit", - "amd64": "x86_64", - } - result, found := archMap[arch] - if found { - return result - } - return arch -} - -// example: https://github.com/jesseduffield/lazydocker/releases/download/v0.1.73/lazydocker_0.1.73_Darwin_x86_64.tar.gz -func (u *Updater) getBinaryUrl(newVersion string) (string, error) { - extension := "tar.gz" - if runtime.GOOS == "windows" { - extension = "zip" - } - url := fmt.Sprintf( - "%s/releases/download/%s/lazydocker_%s_%s_%s.%s", - PROJECT_URL, - newVersion, - newVersion[1:], - u.mappedOs(runtime.GOOS), - u.mappedArch(runtime.GOARCH), - extension, - ) - u.Log.Info("Url for latest release is " + url) - return url, nil -} - -// Update downloads the latest binary and replaces the current binary with it -func (u *Updater) Update(newVersion string, onFinish func(error) error) { - go func() { - err := u.update(newVersion) - if err = onFinish(err); err != nil { - u.Log.Error(err) - } - }() -} - -func (u *Updater) update(newVersion string) error { - rawUrl, err := u.getBinaryUrl(newVersion) - if err != nil { - return err - } - u.Log.Info("Updating with url " + rawUrl) - return u.downloadAndInstall(rawUrl) -} - -func (u *Updater) downloadAndInstall(rawUrl string) error { - url, err := url.Parse(rawUrl) - if err != nil { - return err - } - - g := new(getter.HttpGetter) - tempDir, err := ioutil.TempDir("", "lazydocker") - if err != nil { - return err - } - defer os.RemoveAll(tempDir) - u.Log.Info("Temp directory is " + tempDir) - - // Get it! - if err := g.Get(tempDir, url); err != nil { - return err - } - - // get the path of the current binary - binaryPath, err := osext.Executable() - if err != nil { - return err - } - u.Log.Info("Binary path is " + binaryPath) - - binaryName := filepath.Base(binaryPath) - u.Log.Info("Binary name is " + binaryName) - - // Verify the main file exists - tempPath := filepath.Join(tempDir, binaryName) - u.Log.Info("Temp path to binary is " + tempPath) - if _, err := os.Stat(tempPath); err != nil { - return err - } - - // swap out the old binary for the new one - err = os.Rename(tempPath, binaryPath) - if err != nil { - return err - } - u.Log.Info("Update complete!") - - return nil -} - -func (u *Updater) verifyResourceFound(rawUrl string) bool { - resp, err := http.Head(rawUrl) - if err != nil { - return false - } - defer resp.Body.Close() - u.Log.Info("Received status code ", resp.StatusCode) - // 403 means the resource is there (not going to bother adding extra request headers) - // 404 means its not - return resp.StatusCode == 403 -}