make config type safe

pull/1/head
Jesse Duffield 5 years ago
parent fdc36903ac
commit 664058d5f0

@ -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
}

@ -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)
}

@ -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

@ -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
}

@ -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),
}

@ -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 {

@ -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)
}

@ -1,267 +1,175 @@
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)
// SetIsNewRepo set if the current repo is known
func (c *AppConfig) SetIsNewRepo(toSet bool) {
c.IsNewRepo = toSet
}
if err := folders[0].CreateParentDir("foo"); err != nil {
return "", err
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
return folders[0].Path, nil
}
// GetVersion returns debug flag
func (c *AppConfig) GetVersion() string {
return c.Version
}
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
config := GetDefaultConfig()
// GetCommit returns debug flag
func (c *AppConfig) GetCommit() string {
return c.Commit
return loadUserConfig(configDir, &config)
}
// GetBuildDate returns debug flag
func (c *AppConfig) GetBuildDate() string {
return c.BuildDate
}
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
content, err := ioutil.ReadFile(filepath.Join(configDir, "config.yml"))
if err != nil {
return nil, err
}
// GetName returns debug flag
func (c *AppConfig) GetName() string {
return c.Name
}
if err := yaml.Unmarshal(content, base); err != nil {
return nil, err
}
// GetBuildSource returns the source of the build. For builds from goreleaser
// this will be binaryBuild
func (c *AppConfig) GetBuildSource() string {
return c.BuildSource
return base, nil
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig
type UserConfig struct {
Gui GuiConfig
Reporting string
ConfirmOnQuit bool
CommandTemplates CommandTemplatesConfig
OS OSConfig
Update UpdateConfig
}
// GetAppState returns the app state
func (c *AppConfig) GetAppState() *AppState {
return c.AppState
type ThemeConfig struct {
ActiveBorderColor []string
InactiveBorderColor []string
OptionsTextColor []string
}
func newViper(filename string) (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigName(filename)
return v, nil
type GuiConfig struct {
ScrollHeight int
ScrollPastBottom bool
MouseEvents bool
Theme ThemeConfig
}
// LoadConfig gets the user's config
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, error) {
v, err := newViper(filename)
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 {
return nil, err
}
return v, nil
type CommandTemplatesConfig struct {
RestartService string
DockerCompose string
StopService string
}
// LoadDefaults loads in the defaults defined in this file
func LoadDefaults(v *viper.Viper, defaults []byte) error {
return v.MergeConfig(bytes.NewBuffer(defaults))
type OSConfig struct {
OpenCommand string
OpenLinkCommand string
}
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 UpdateConfig struct {
Method 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
// GetDefaultConfig returns the application default configuration
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",
},
}
v.AddConfigPath(filepath.Dir(configPath))
return v.MergeInConfig()
}
// 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
}
// AppState stores data between runs of the app like when the last update check
// was performed and which other repos have been checked out
type AppState struct {
LastUpdateCheck int64
}
v.Set(key, value)
return v.WriteConfig()
func getDefaultAppState() []byte {
return []byte(`
lastUpdateCheck: 0
`)
}
// SaveAppState marhsalls the AppState struct and writes it to the disk
func (c *AppConfig) SaveAppState() error {
marshalledAppState, err := yaml.Marshal(c.AppState)
func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) error {
userConfig, err := loadUserConfig(c.ConfigDir, &UserConfig{})
if err != nil {
return err
}
filepath, err := prepareConfigFile("state.yml")
if err != nil {
if err := updateConfig(userConfig); err != nil {
return err
}
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
}
// 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)
out, err := yaml.Marshal(userConfig)
if err != nil {
return err
}
if len(appStateBytes) == 0 {
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
}
return yaml.Unmarshal(appStateBytes, c.AppState)
}
// 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"
`)
return ioutil.WriteFile(c.ConfigFilename(), out, 0666)
}
// AppState stores data between runs of the app like when the last update check
// was performed and which other repos have been checked out
type AppState struct {
LastUpdateCheck int64
}
func getDefaultAppState() []byte {
return []byte(`
lastUpdateCheck: 0
`)
func (c *AppConfig) ConfigFilename() string {
return filepath.Join(c.ConfigDir, "config.yml")
}

@ -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}}",
}
}

@ -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"`,
}
}

@ -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}}"`,
}
}

@ -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)

@ -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",

@ -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{
{

@ -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)
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}
Loading…
Cancel
Save