make config type safe

This commit is contained in:
Jesse Duffield 2019-06-02 11:37:21 +10:00
parent fdc36903ac
commit 664058d5f0
19 changed files with 225 additions and 770 deletions

View file

@ -2,17 +2,12 @@ package app
import ( import (
"io" "io"
"io/ioutil"
"os"
"path/filepath"
"github.com/heroku/rollrus"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui" "github.com/jesseduffield/lazydocker/pkg/gui"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/updates" "github.com/jesseduffield/lazydocker/pkg/log"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -20,98 +15,32 @@ import (
type App struct { type App struct {
closers []io.Closer closers []io.Closer
Config config.AppConfigurer Config *config.AppConfig
Log *logrus.Entry Log *logrus.Entry
OSCommand *commands.OSCommand OSCommand *commands.OSCommand
DockerCommand *commands.DockerCommand DockerCommand *commands.DockerCommand
Gui *gui.Gui Gui *gui.Gui
Tr *i18n.Localizer 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 // NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) { func NewApp(config *config.AppConfig) (*App, error) {
app := &App{ app := &App{
closers: []io.Closer{}, closers: []io.Closer{},
Config: config, Config: config,
} }
var err error var err error
app.Log = newLogger(config) app.Log = log.NewLogger(config, "23432119147a4367abf7c0de2aa99a2d")
app.Tr = i18n.NewLocalizer(app.Log) app.Tr = i18n.NewLocalizer(app.Log)
app.OSCommand = commands.NewOSCommand(app.Log, config) 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 // 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) app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config)
if err != nil { if err != nil {
return app, err 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 { if err != nil {
return app, err return app, err
} }

View file

@ -292,7 +292,7 @@ func (c *Container) Restart() error {
// RestartService restarts the container // RestartService restarts the container
func (c *Container) RestartService() error { 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) command := utils.ApplyTemplate(templateString, c)
return c.OSCommand.RunCommand(command) return c.OSCommand.RunCommand(command)
} }

View file

@ -21,13 +21,13 @@ type DockerCommand struct {
Log *logrus.Entry Log *logrus.Entry
OSCommand *OSCommand OSCommand *OSCommand
Tr *i18n.Localizer Tr *i18n.Localizer
Config config.AppConfigurer Config *config.AppConfig
Client *client.Client Client *client.Client
InDockerComposeProject bool InDockerComposeProject bool
} }
// NewDockerCommand it runs git commands // 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() cli, err := client.NewEnvClient()
if err != nil { if err != nil {
return nil, err return nil, err
@ -155,7 +155,7 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
return nil, nil 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)) output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand))
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -6,8 +6,6 @@ import (
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/sirupsen/logrus" "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 // This file exports dummy constructors for use by tests in other packages
@ -26,9 +24,7 @@ func NewDummyAppConfig() *config.AppConfig {
BuildDate: "", BuildDate: "",
Debug: false, Debug: false,
BuildSource: "", BuildSource: "",
UserConfig: viper.New(),
} }
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig return appConfig
} }

View file

@ -32,14 +32,14 @@ type Platform struct {
type OSCommand struct { type OSCommand struct {
Log *logrus.Entry Log *logrus.Entry
Platform *Platform Platform *Platform
Config config.AppConfigurer Config *config.AppConfig
command func(string, ...string) *exec.Cmd command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error) getGlobalGitConfig func(string) (string, error)
getenv func(string) string getenv func(string) string
} }
// NewOSCommand os command runner // NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand { func NewOSCommand(log *logrus.Entry, config *config.AppConfig) *OSCommand {
return &OSCommand{ return &OSCommand{
Log: log, Log: log,
Platform: getPlatform(), Platform: getPlatform(),
@ -154,7 +154,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
// OpenFile opens a file with the given // OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error { func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand") commandTemplate := c.Config.UserConfig.OS.OpenCommand
templateValues := map[string]string{ templateValues := map[string]string{
"filename": c.Quote(filename), "filename": c.Quote(filename),
} }
@ -166,7 +166,7 @@ func (c *OSCommand) OpenFile(filename string) error {
// OpenLink opens a file with the given // OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error { func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand") commandTemplate := c.Config.UserConfig.OS.OpenLinkCommand
templateValues := map[string]string{ templateValues := map[string]string{
"link": c.Quote(link), "link": c.Quote(link),
} }

View file

@ -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. // TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) { func TestOSCommandEditFile(t *testing.T) {
type scenario struct { type scenario struct {

View file

@ -37,14 +37,14 @@ func (s *Service) Remove(options types.ContainerRemoveOptions) error {
// Stop stops the service's containers // Stop stops the service's containers
func (s *Service) Stop() error { 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) command := utils.ApplyTemplate(templateString, s)
return s.OSCommand.RunCommand(command) return s.OSCommand.RunCommand(command)
} }
// Restart restarts the service // Restart restarts the service
func (s *Service) Restart() error { 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) command := utils.ApplyTemplate(templateString, s)
return s.OSCommand.RunCommand(command) return s.OSCommand.RunCommand(command)
} }

View file

@ -1,257 +1,143 @@
package config package config
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"github.com/shibukawa/configdir" "github.com/shibukawa/configdir"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2" 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 { type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"` Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"` Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"` Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"` 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:""` BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *viper.Viper UserConfig *UserConfig
AppState *AppState ConfigDir string
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
} }
// NewAppConfig makes a new app config // NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) { 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 { if err != nil {
return nil, err return nil, err
} }
if os.Getenv("DEBUG") == "TRUE" { userConfig, err := loadUserConfigWithDefaults(configDir)
debuggingFlag = true if err != nil {
return nil, err
} }
appConfig := &AppConfig{ appConfig := &AppConfig{
Name: "lazydocker", Name: name,
Version: version, Version: version,
Commit: commit, Commit: commit,
BuildDate: date, BuildDate: date,
Debug: debuggingFlag, Debug: true, // TODO: restore os.Getenv("DEBUG") == "TRUE"
BuildSource: buildSource, BuildSource: buildSource,
UserConfig: userConfig, UserConfig: userConfig,
AppState: &AppState{}, ConfigDir: configDir,
IsNewRepo: false,
}
if err := appConfig.LoadAppState(); err != nil {
return nil, err
} }
return appConfig, nil return appConfig, nil
} }
// GetIsNewRepo returns known repo boolean func findOrCreateConfigDir(projectName string) (string, error) {
func (c *AppConfig) GetIsNewRepo() bool { configDirs := configdir.New("jesseduffield", projectName)
return c.IsNewRepo 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 loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
func (c *AppConfig) SetIsNewRepo(toSet bool) { config := GetDefaultConfig()
c.IsNewRepo = toSet
return loadUserConfig(configDir, &config)
} }
// GetDebug returns debug flag func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
func (c *AppConfig) GetDebug() bool { content, err := ioutil.ReadFile(filepath.Join(configDir, "config.yml"))
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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if withDefaults {
if err = LoadDefaults(v, GetDefaultConfig()); err != nil { if err := yaml.Unmarshal(content, base); 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 nil, err
} }
return v, nil
return base, nil
} }
// LoadDefaults loads in the defaults defined in this file type UserConfig struct {
func LoadDefaults(v *viper.Viper, defaults []byte) error { Gui GuiConfig
return v.MergeConfig(bytes.NewBuffer(defaults)) Reporting string
ConfirmOnQuit bool
CommandTemplates CommandTemplatesConfig
OS OSConfig
Update UpdateConfig
} }
func prepareConfigFile(filename string) (string, error) { type ThemeConfig struct {
// chucking my name there is not for vanity purposes, the xdg spec (and that ActiveBorderColor []string
// function) requires a vendor name. May as well line up with github InactiveBorderColor []string
configDirs := configdir.New("jesseduffield", "lazydocker") OptionsTextColor []string
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
} }
// LoadAndMergeFile Loads the config/state file, creating type GuiConfig struct {
// the file has an empty one if it does not exist ScrollHeight int
func LoadAndMergeFile(v *viper.Viper, filename string) error { ScrollPastBottom bool
configPath, err := prepareConfigFile(filename) MouseEvents bool
if err != nil { Theme ThemeConfig
return err
}
v.AddConfigPath(filepath.Dir(configPath))
return v.MergeInConfig()
} }
// WriteToUserConfig adds a key/value pair to the user's config and saves it type CommandTemplatesConfig struct {
func (c *AppConfig) WriteToUserConfig(key, value string) error { RestartService string
// reloading the user config directly (without defaults) so that we're not DockerCompose string
// writing any defaults back to the user's config StopService string
v, err := LoadConfig("config", false)
if err != nil {
return err
}
v.Set(key, value)
return v.WriteConfig()
} }
// SaveAppState marhsalls the AppState struct and writes it to the disk type OSConfig struct {
func (c *AppConfig) SaveAppState() error { OpenCommand string
marshalledAppState, err := yaml.Marshal(c.AppState) OpenLinkCommand string
if err != nil {
return err
}
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
} }
// LoadAppState loads recorded AppState from file type UpdateConfig struct {
func (c *AppConfig) LoadAppState() error { Method string
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)
} }
// GetDefaultConfig returns the application default configuration // GetDefaultConfig returns the application default configuration
func GetDefaultConfig() []byte { func GetDefaultConfig() UserConfig {
return []byte( return UserConfig{
`gui: Gui: GuiConfig{
## stuff relating to the UI ScrollHeight: 2,
scrollHeight: 2 ScrollPastBottom: false,
scrollPastBottom: false MouseEvents: false,
mouseEvents: false # will default to true when the feature is complete Theme: ThemeConfig{
theme: ActiveBorderColor: []string{"white", "bold"},
activeBorderColor: InactiveBorderColor: []string{"white"},
- white OptionsTextColor: []string{"blue"},
- bold },
inactiveBorderColor: },
- white Reporting: "undetermined",
optionsTextColor: ConfirmOnQuit: false,
- blue CommandTemplates: CommandTemplatesConfig{
update: RestartService: "docker-compose restart {{ .Name }}",
method: prompt # can be: prompt | background | never DockerCompose: "apdev compose",
days: 14 # how often a update is checked for StopService: "apdev stop {{ .Name }}",
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' },
confirmOnQuit: false OS: GetPlatformDefaultConfig(),
commandTemplates: Update: UpdateConfig{
restartService: "docker-compose restart {{ .Name }}" Method: "never",
dockerCompose: "apdev compose" },
`) }
} }
// AppState stores data between runs of the app like when the last update check // AppState stores data between runs of the app like when the last update check
@ -265,3 +151,25 @@ func getDefaultAppState() []byte {
lastUpdateCheck: 0 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")
}

View file

@ -3,9 +3,9 @@
package config package config
// GetPlatformDefaultConfig gets the defaults for the platform // GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte { func GetPlatformDefaultConfig() OSConfig {
return []byte( return OSConfig{
`os: OpenCommand: "open {{filename}}",
openCommand: 'open {{filename}}' OpenLinkCommand: "open {{link}}",
openLinkCommand: 'open {{link}}'`) }
} }

View file

@ -1,9 +1,9 @@
package config package config
// GetPlatformDefaultConfig gets the defaults for the platform // GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte { func GetPlatformDefaultConfig() OSConfig {
return []byte( return OSConfig{
`os: OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"' OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`) }
} }

View file

@ -1,9 +1,9 @@
package config package config
// GetPlatformDefaultConfig gets the defaults for the platform // GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte { func GetPlatformDefaultConfig() OSConfig {
return []byte( return OSConfig{
`os: OpenCommand: `cmd /c "start "" {{filename}}"`,
openCommand: 'cmd /c "start "" {{filename}}"' OpenLinkCommand: `cmd /c "start "" {{link}}"`,
openLinkCommand: 'cmd /c "start "" {{link}}"'`) }
} }

View file

@ -24,7 +24,6 @@ import (
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/updates"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -69,10 +68,9 @@ type Gui struct {
OSCommand *commands.OSCommand OSCommand *commands.OSCommand
SubProcess *exec.Cmd SubProcess *exec.Cmd
State guiState State guiState
Config config.AppConfigurer Config *config.AppConfig
Tr *i18n.Localizer Tr *i18n.Localizer
Errors SentinelErrors Errors SentinelErrors
Updater *updates.Updater
statusManager *statusManager statusManager *statusManager
waitForIntro sync.WaitGroup waitForIntro sync.WaitGroup
T *tasks.TaskManager T *tasks.TaskManager
@ -116,7 +114,6 @@ type guiState struct {
MenuItemCount int // can't store the actual list because it's of interface{} type MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string PreviousView string
Platform commands.Platform Platform commands.Platform
Updating bool
Panels *panelStates Panels *panelStates
SubProcessOutput string SubProcessOutput string
MainProcessMutex sync.Mutex MainProcessMutex sync.Mutex
@ -124,7 +121,7 @@ type guiState struct {
} }
// NewGui builds a new gui handler // 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{ initialState := guiState{
Containers: make([]*commands.Container, 0), Containers: make([]*commands.Container, 0),
@ -157,7 +154,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
State: initialState, State: initialState,
Config: config, Config: config,
Tr: tr, Tr: tr,
Updater: updater,
statusManager: &statusManager{}, statusManager: &statusManager{},
T: tasks.NewTaskManager(), T: tasks.NewTaskManager(),
} }
@ -171,7 +167,7 @@ func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main") mainView, _ := g.View("main")
mainView.Autoscroll = false mainView.Autoscroll = false
ox, oy := mainView.Origin() 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) return mainView.SetOrigin(ox, newOy)
} }
@ -179,13 +175,13 @@ func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main") mainView, _ := g.View("main")
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
y := oy y := oy
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") { if !gui.Config.UserConfig.Gui.ScrollPastBottom {
_, sy := mainView.Size() _, sy := mainView.Size()
y += sy y += sy
} }
// for some reason we can't work out whether we've hit the bottomq // 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 // 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 { 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 g.Highlight = true
width, height := g.Size() width, height := g.Size()
information := gui.Config.GetVersion() information := gui.Config.Version
minimumHeight := 9 minimumHeight := 9
minimumWidth := 10 minimumWidth := 10
@ -453,15 +449,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
} }
func (gui *Gui) loadNewDirectory() error { func (gui *Gui) loadNewDirectory() error {
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
gui.waitForIntro.Done() gui.waitForIntro.Done()
if err := gui.refreshSidePanels(gui.g); err != nil { if err := gui.refreshSidePanels(gui.g); err != nil {
return err return err
} }
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" { if gui.Config.UserConfig.Reporting == "undetermined" {
if err := gui.promptAnonymousReporting(); err != nil { if err := gui.promptAnonymousReporting(); err != nil {
return err return err
} }
@ -472,10 +466,16 @@ func (gui *Gui) loadNewDirectory() error {
func (gui *Gui) promptAnonymousReporting() 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 { 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() 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 { }, func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done() 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() defer g.Close()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") { if gui.Config.UserConfig.Gui.MouseEvents {
g.Mouse = true g.Mouse = true
} }
@ -522,7 +522,7 @@ func (gui *Gui) Run() error {
return err return err
} }
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" { if gui.Config.UserConfig.Reporting == "undetermined" {
gui.waitForIntro.Add(2) gui.waitForIntro.Add(2)
} else { } else {
gui.waitForIntro.Add(1) gui.waitForIntro.Add(1)
@ -600,10 +600,7 @@ func (gui *Gui) runCommand() error {
} }
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.State.Updating { if gui.Config.UserConfig.ConfirmOnQuit {
return gui.createUpdateQuitConfirmation(g, v)
}
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit return gocui.ErrQuit
}, nil) }, nil)

View file

@ -116,12 +116,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleOpenConfig, Handler: gui.handleOpenConfig,
Description: gui.Tr.SLocalize("OpenConfig"), Description: gui.Tr.SLocalize("OpenConfig"),
}, {
ViewName: "status",
Key: 'u',
Modifier: gocui.ModNone,
Handler: gui.handleCheckForUpdate,
Description: gui.Tr.SLocalize("checkForUpdate"),
}, },
{ {
ViewName: "menu", ViewName: "menu",

View file

@ -186,7 +186,7 @@ func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
composeCommand := gui.Config.GetUserConfig().GetString("commandTemplates.dockerCompose") composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose
options := []*removeServiceOption{ options := []*removeServiceOption{
{ {

View file

@ -2,6 +2,7 @@ package gui
import ( import (
"fmt" "fmt"
"path/filepath"
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
@ -24,11 +25,6 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
return nil 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 { func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() { if gui.popupPanelFocused() {
return nil 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 { 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 { 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) return gui.editFile(filename)
} }

View file

@ -38,17 +38,12 @@ func (gui *Gui) GetColor(keys []string) gocui.Attribute {
// GetOptionsPanelTextColor gets the color of the options panel text // GetOptionsPanelTextColor gets the color of the options panel text
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) { func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
userConfig := gui.Config.GetUserConfig() return gui.GetColor(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil
optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor")
return gui.GetColor(optionsColor), nil
} }
// SetColorScheme sets the color scheme for the app based on the user config // SetColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) SetColorScheme() error { func (gui *Gui) SetColorScheme() error {
userConfig := gui.Config.GetUserConfig() gui.g.FgColor = gui.GetColor(gui.Config.UserConfig.Gui.Theme.InactiveBorderColor)
activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor") gui.g.SelFgColor = gui.GetColor(gui.Config.UserConfig.Gui.Theme.ActiveBorderColor)
inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor")
gui.g.FgColor = gui.GetColor(inactiveBorderColor)
gui.g.SelFgColor = gui.GetColor(activeBorderColor)
return nil return nil
} }

View file

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

View file

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