mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
make config type safe
This commit is contained in:
parent
fdc36903ac
commit
664058d5f0
19 changed files with 225 additions and 770 deletions
|
|
@ -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,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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
68
pkg/log/log.go
Normal file
68
pkg/log/log.go
Normal file
|
|
@ -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…
Add table
Reference in a new issue