mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
all: implement self-upgrade flag from API
So upgrading don't have to be initiated manually, helping large deployments to upgrade to latest ctrld version easily.
This commit is contained in:
committed by
Cuong Manh Le
parent
f27cbe3525
commit
c60cf33af3
@@ -25,7 +25,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/cuonglm/osinfo"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/kardianos/service"
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/kardianos/service"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
@@ -304,6 +306,16 @@ func (p *prog) apiConfigReload() {
|
||||
logger := mainLog.Load().With().Str("mode", "api-reload").Logger()
|
||||
logger.Debug().Msg("starting custom config reload timer")
|
||||
lastUpdated := time.Now().Unix()
|
||||
curVerStr := curVersion()
|
||||
curVer, err := semver.NewVersion(curVerStr)
|
||||
isStable := curVer != nil && curVer.Prerelease() == ""
|
||||
if err != nil || !isStable {
|
||||
l := mainLog.Load().Warn()
|
||||
if err != nil {
|
||||
l = l.Err(err)
|
||||
}
|
||||
l.Msgf("current version is not stable, skipping self-upgrade: %s", curVerStr)
|
||||
}
|
||||
|
||||
doReloadApiConfig := func(forced bool, logger zerolog.Logger) {
|
||||
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
|
||||
@@ -313,6 +325,11 @@ func (p *prog) apiConfigReload() {
|
||||
return
|
||||
}
|
||||
|
||||
// Performing self-upgrade check for production version.
|
||||
if isStable {
|
||||
selfUpgradeCheck(resolverConfig.Ctrld.VersionTarget, curVer, &logger)
|
||||
}
|
||||
|
||||
if resolverConfig.DeactivationPin != nil {
|
||||
newDeactivationPin := *resolverConfig.DeactivationPin
|
||||
curDeactivationPin := cdDeactivationPin.Load()
|
||||
@@ -1422,6 +1439,46 @@ func selfUninstallCheck(uninstallErr error, p *prog, logger zerolog.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
// selfUpgradeCheck checks if the version target vt is greater
|
||||
// than the current one cv, perform self-upgrade then.
|
||||
//
|
||||
// The callers must ensure curVer and logger are non-nil.
|
||||
func selfUpgradeCheck(vt string, cv *semver.Version, logger *zerolog.Logger) {
|
||||
if vt == "" {
|
||||
logger.Debug().Msg("no version target set, skipped checking self-upgrade")
|
||||
return
|
||||
}
|
||||
vts := vt
|
||||
if !strings.HasPrefix(vts, "v") {
|
||||
vts = "v" + vts
|
||||
}
|
||||
targetVer, err := semver.NewVersion(vts)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msgf("invalid target version, skipped self-upgrade: %s", vt)
|
||||
return
|
||||
}
|
||||
if !targetVer.GreaterThan(cv) {
|
||||
logger.Debug().
|
||||
Str("target", vt).
|
||||
Str("current", cv.String()).
|
||||
Msgf("target version is not greater than current one, skipped self-upgrade")
|
||||
return
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to get executable path, skipped self-upgrade")
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(exe, "upgrade", "prod", "-vv")
|
||||
cmd.SysProcAttr = sysProcAttrForSelfUpgrade()
|
||||
if err := cmd.Start(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to start self-upgrade")
|
||||
return
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("self-upgrade triggered, version target: %s", vts)
|
||||
}
|
||||
|
||||
// leakOnUpstreamFailure reports whether ctrld should initiate a recovery flow
|
||||
// when upstream failures occur.
|
||||
func (p *prog) leakOnUpstreamFailure() bool {
|
||||
|
||||
12
cmd/cli/self_upgrade_others.go
Normal file
12
cmd/cli/self_upgrade_others.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build !windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// sysProcAttrForSelfUpgrade returns *syscall.SysProcAttr instance for running self-upgrade command.
|
||||
func sysProcAttrForSelfUpgrade() *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{Setsid: true}
|
||||
}
|
||||
18
cmd/cli/self_upgrade_windows.go
Normal file
18
cmd/cli/self_upgrade_windows.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// From: https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags?redirectedfrom=MSDN
|
||||
|
||||
// SYSCALL_CREATE_NO_WINDOW set flag to run process without a console window.
|
||||
const SYSCALL_CREATE_NO_WINDOW = 0x08000000
|
||||
|
||||
// sysProcAttrForSelfUpgrade returns *syscall.SysProcAttr instance for running self-upgrade command.
|
||||
func sysProcAttrForSelfUpgrade() *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP | SYSCALL_CREATE_NO_WINDOW,
|
||||
HideWindow: true,
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/unit"
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
@@ -132,6 +134,59 @@ func (s *systemd) Status() (service.Status, error) {
|
||||
return s.Service.Status()
|
||||
}
|
||||
|
||||
func (s *systemd) Start() error {
|
||||
const systemdUnitFile = "/etc/systemd/system/ctrld.service"
|
||||
f, err := os.Open(systemdUnitFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if opts, change := ensureSystemdKillMode(f); change {
|
||||
mode := os.FileMode(0644)
|
||||
buf, err := io.ReadAll(unit.Serialize(opts))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(systemdUnitFile, buf, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl daemon-reload failed: %w\n%s", err, string(out))
|
||||
}
|
||||
mainLog.Load().Debug().Msg("set KillMode=process successfully")
|
||||
}
|
||||
return s.Service.Start()
|
||||
}
|
||||
|
||||
// ensureSystemdKillMode ensure systemd unit file is configured with KillMode=process.
|
||||
// This is necessary for running self-upgrade flow.
|
||||
func ensureSystemdKillMode(r io.Reader) (opts []*unit.UnitOption, change bool) {
|
||||
opts, err := unit.DeserializeOptions(r)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to deserialize options")
|
||||
return
|
||||
}
|
||||
change = true
|
||||
needKillModeOpt := true
|
||||
killModeOpt := unit.NewUnitOption("Service", "KillMode", "process")
|
||||
for _, opt := range opts {
|
||||
if opt.Match(killModeOpt) {
|
||||
needKillModeOpt = false
|
||||
change = false
|
||||
break
|
||||
}
|
||||
if opt.Section == killModeOpt.Section && opt.Name == killModeOpt.Name {
|
||||
opt.Value = killModeOpt.Value
|
||||
needKillModeOpt = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if needKillModeOpt {
|
||||
opts = append(opts, killModeOpt)
|
||||
}
|
||||
return opts, change
|
||||
}
|
||||
|
||||
func newLaunchd(s service.Service) *launchd {
|
||||
return &launchd{
|
||||
Service: s,
|
||||
|
||||
28
cmd/cli/service_test.go
Normal file
28
cmd/cli/service_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ensureSystemdKillMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
unitFile string
|
||||
wantChange bool
|
||||
}{
|
||||
{"no KillMode", "[Service]\nExecStart=/bin/sleep 1", true},
|
||||
{"not KillMode=process", "[Service]\nExecStart=/bin/sleep 1\nKillMode=mixed", true},
|
||||
{"KillMode=process", "[Service]\nExecStart=/bin/sleep 1\nKillMode=process", false},
|
||||
{"invalid unit file", "[Service\nExecStart=/bin/sleep 1\nKillMode=process", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, change := ensureSystemdKillMode(strings.NewReader(tc.unitFile)); tc.wantChange != change {
|
||||
t.Errorf("ensureSystemdKillMode(%q) = %v, want %v", tc.unitFile, change, tc.wantChange)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user