cmd/cli: add upgrade command

This commit implements upgrade command which will:

 - Download latest version for current running arch.
 - Replacing the binary on disk.
 - Self-restart ctrld service.

If the service does not start with new binary, old binary will be
restored and self-restart again.
This commit is contained in:
Cuong Manh Le
2024-03-11 23:33:07 +07:00
committed by Cuong Manh Le
parent 87513cba6d
commit ebcbf85373
4 changed files with 131 additions and 0 deletions

View File

@@ -23,11 +23,13 @@ import (
"sync"
"time"
"github.com/Masterminds/semver"
"github.com/cuonglm/osinfo"
"github.com/fsnotify/fsnotify"
"github.com/go-playground/validator/v10"
"github.com/kardianos/service"
"github.com/miekg/dns"
"github.com/minio/selfupdate"
"github.com/olekukonko/tablewriter"
"github.com/pelletier/go-toml/v2"
"github.com/rs/zerolog"
@@ -845,6 +847,89 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
}
clientsCmd.AddCommand(listClientsCmd)
rootCmd.AddCommand(clientsCmd)
upgradeCmd := &cobra.Command{
Use: "upgrade",
Short: "Upgrading ctrld to latest version",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege()
},
Run: func(cmd *cobra.Command, args []string) {
s, err := newService(&prog{}, svcConfig)
if err != nil {
mainLog.Load().Error().Msg(err.Error())
return
}
if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
mainLog.Load().Warn().Msg("service not installed")
return
}
bin, err := os.Executable()
if err != nil {
mainLog.Load().Fatal().Err(err).Msg("failed to get current ctrld binary path")
}
oldBin := bin + "_previous"
urlString := "https://dl.controld.com"
if !isStableVersion(curVersion()) {
urlString = "https://dl.controld.dev"
}
dlUrl := fmt.Sprintf("%s/%s-%s/ctrld", urlString, runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
dlUrl += ".exe"
}
mainLog.Load().Debug().Msgf("Downloading binary: %s", dlUrl)
resp, err := http.Get(dlUrl)
if err != nil {
mainLog.Load().Fatal().Err(err).Msg("failed to download binary")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
mainLog.Load().Fatal().Msgf("could not download binary: %s", http.StatusText(resp.StatusCode))
}
mainLog.Load().Debug().Msg("Updating current binary")
if err := selfupdate.Apply(resp.Body, selfupdate.Options{OldSavePath: oldBin}); err != nil {
if rerr := selfupdate.RollbackError(err); rerr != nil {
mainLog.Load().Error().Err(rerr).Msg("could not rollback old binary")
}
mainLog.Load().Fatal().Err(err).Msg("failed to update current binary")
}
doRestart := func() bool {
tasks := []task{
{s.Stop, false},
{s.Start, false},
}
if doTasks(tasks) {
if dir, err := socketDir(); err == nil {
return newSocketControlClient(s, dir) != nil
}
}
return false
}
mainLog.Load().Debug().Msg("Restarting ctrld service using new binary")
if doRestart() {
_ = os.Remove(oldBin)
_ = os.Chmod(bin, 0755)
mainLog.Load().Notice().Msg("Upgrade successful")
return
}
mainLog.Load().Warn().Msgf("Upgrade failed, restoring previous binary: %s", oldBin)
if err := os.Remove(bin); err != nil {
mainLog.Load().Fatal().Err(err).Msg("failed to remove new binary")
}
if err := os.Rename(oldBin, bin); err != nil {
mainLog.Load().Fatal().Err(err).Msg("failed to restore old binary")
}
if doRestart() {
mainLog.Load().Notice().Msg("Restored previous binary successfully")
return
}
},
}
rootCmd.AddCommand(upgradeCmd)
}
// isMobile reports whether the current OS is a mobile platform.
@@ -857,6 +942,15 @@ func isAndroid() bool {
return runtime.GOOS == "android"
}
// isStableVersion reports whether vs is a stable semantic version.
func isStableVersion(vs string) bool {
v, err := semver.NewVersion(vs)
if err != nil {
return false
}
return v.Prerelease() == ""
}
// RunCobraCommand runs ctrld cli.
func RunCobraCommand(cmd *cobra.Command) {
noConfigStart = isNoConfigStart(cmd)

View File

@@ -21,3 +21,26 @@ func Test_writeConfigFile(t *testing.T) {
_, err = os.Stat(configPath)
require.NoError(t, err)
}
func Test_isStableVersion(t *testing.T) {
tests := []struct {
name string
ver string
isStable bool
}{
{"stable", "v1.3.5", true},
{"pre", "v1.3.5-next", false},
{"pre with commit hash", "v1.3.5-next-asdf", false},
{"dev", "dev", false},
{"empty", "dev", false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := isStableVersion(tc.ver); got != tc.isStable {
t.Errorf("unexpected result for %s, want: %v, got: %v", tc.ver, tc.isStable, got)
}
})
}
}