mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-29 13:51:51 +02: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:
1 parent
f27cbe3525
commit
c60cf33af3
9 files changed
+175
-4
No files matched your search
+1
-1
@@ -25,7 +25,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/cuonglm/osinfo"
|
"github.com/cuonglm/osinfo"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -304,6 +306,16 @@ func (p *prog) apiConfigReload() {
|
|||||||
logger := mainLog.Load().With().Str("mode", "api-reload").Logger()
|
logger := mainLog.Load().With().Str("mode", "api-reload").Logger()
|
||||||
logger.Debug().Msg("starting custom config reload timer")
|
logger.Debug().Msg("starting custom config reload timer")
|
||||||
lastUpdated := time.Now().Unix()
|
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) {
|
doReloadApiConfig := func(forced bool, logger zerolog.Logger) {
|
||||||
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
|
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
|
||||||
@@ -313,6 +325,11 @@ func (p *prog) apiConfigReload() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performing self-upgrade check for production version.
|
||||||
|
if isStable {
|
||||||
|
selfUpgradeCheck(resolverConfig.Ctrld.VersionTarget, curVer, &logger)
|
||||||
|
}
|
||||||
|
|
||||||
if resolverConfig.DeactivationPin != nil {
|
if resolverConfig.DeactivationPin != nil {
|
||||||
newDeactivationPin := *resolverConfig.DeactivationPin
|
newDeactivationPin := *resolverConfig.DeactivationPin
|
||||||
curDeactivationPin := cdDeactivationPin.Load()
|
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
|
// leakOnUpstreamFailure reports whether ctrld should initiate a recovery flow
|
||||||
// when upstream failures occur.
|
// when upstream failures occur.
|
||||||
func (p *prog) leakOnUpstreamFailure() bool {
|
func (p *prog) leakOnUpstreamFailure() bool {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/coreos/go-systemd/v22/unit"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
@@ -132,6 +134,59 @@ func (s *systemd) Status() (service.Status, error) {
|
|||||||
return s.Service.Status()
|
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 {
|
func newLaunchd(s service.Service) *launchd {
|
||||||
return &launchd{
|
return &launchd{
|
||||||
Service: s,
|
Service: s,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ go 1.23.0
|
|||||||
toolchain go1.23.7
|
toolchain go1.23.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver/v3 v3.2.1
|
||||||
github.com/ameshkov/dnsstamps v1.0.3
|
github.com/ameshkov/dnsstamps v1.0.3
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0
|
github.com/coreos/go-systemd/v22 v22.5.0
|
||||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c h1:UqFsxmwiCh/DBvwJB0m7KQ2QFDd6DdUkosznfMppdhE=
|
github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c h1:UqFsxmwiCh/DBvwJB0m7KQ2QFDd6DdUkosznfMppdhE=
|
||||||
github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type ResolverConfig struct {
|
|||||||
Ctrld struct {
|
Ctrld struct {
|
||||||
CustomConfig string `json:"custom_config"`
|
CustomConfig string `json:"custom_config"`
|
||||||
CustomLastUpdate int64 `json:"custom_last_update"`
|
CustomLastUpdate int64 `json:"custom_last_update"`
|
||||||
|
VersionTarget string `json:"version_target"`
|
||||||
} `json:"ctrld"`
|
} `json:"ctrld"`
|
||||||
Exclude []string `json:"exclude"`
|
Exclude []string `json:"exclude"`
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
|
|||||||
Reference in new issue
Block a user