mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-22 14:46:35 +00:00

In the rare case that the updater downloads the same binary (validated via checksum) we want to make sure that the updater does not attempt to upgrade and restart the cloudflared process. The binaries are equivalent and this would provide no value. However, we are covering this case because there was an errant deployment of cloudflared that reported itself as an older version and was then stuck in an infinite loop attempting to upgrade to the latest version which didn't exist. By making sure that the binary is different ensures that the upgrade will be attempted and cloudflared will be restarted to run the new version. This change only affects cloudflared tunnels running with default settings or `--no-autoupdate=false` which allows cloudflared to auto-update itself in-place. Most distributions that handle package management at the operating system level are not affected by this change.
322 lines
9.0 KiB
Go
322 lines
9.0 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/facebookgo/grace/gracenet"
|
|
"github.com/rs/zerolog"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/term"
|
|
|
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
|
"github.com/cloudflare/cloudflared/config"
|
|
"github.com/cloudflare/cloudflared/logger"
|
|
)
|
|
|
|
const (
|
|
DefaultCheckUpdateFreq = time.Hour * 24
|
|
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/as-a-service/"
|
|
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
|
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
|
isManagedInstallFile = ".installedFromPackageManager"
|
|
UpdateURL = "https://update.argotunnel.com"
|
|
StagingUpdateURL = "https://staging-update.argotunnel.com"
|
|
|
|
LogFieldVersion = "version"
|
|
)
|
|
|
|
var (
|
|
buildInfo *cliutil.BuildInfo
|
|
BuiltForPackageManager = ""
|
|
)
|
|
|
|
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
|
|
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
|
|
type statusSuccess struct {
|
|
newVersion string
|
|
}
|
|
|
|
func (u *statusSuccess) Error() string {
|
|
return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion)
|
|
}
|
|
|
|
func (u *statusSuccess) ExitCode() int {
|
|
return 11
|
|
}
|
|
|
|
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
|
|
type statusErr struct {
|
|
err error
|
|
}
|
|
|
|
func (e *statusErr) Error() string {
|
|
return fmt.Sprintf("failed to update cloudflared: %v", e.err)
|
|
}
|
|
|
|
func (e *statusErr) ExitCode() int {
|
|
return 10
|
|
}
|
|
|
|
type updateOptions struct {
|
|
updateDisabled bool
|
|
isBeta bool
|
|
isStaging bool
|
|
isForced bool
|
|
intendedVersion string
|
|
}
|
|
|
|
type UpdateOutcome struct {
|
|
Updated bool
|
|
Version string
|
|
UserMessage string
|
|
Error error
|
|
}
|
|
|
|
func (uo *UpdateOutcome) noUpdate() bool {
|
|
return uo.Error == nil && uo.Updated == false
|
|
}
|
|
|
|
func Init(info *cliutil.BuildInfo) {
|
|
buildInfo = info
|
|
}
|
|
|
|
func CheckForUpdate(options updateOptions) (CheckResult, error) {
|
|
cfdPath, err := os.Executable()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url := UpdateURL
|
|
if options.isStaging {
|
|
url = StagingUpdateURL
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
cfdPath = encodeWindowsPath(cfdPath)
|
|
}
|
|
|
|
s := NewWorkersService(buildInfo.CloudflaredVersion, url, cfdPath, Options{IsBeta: options.isBeta,
|
|
IsForced: options.isForced, RequestedVersion: options.intendedVersion})
|
|
|
|
return s.Check()
|
|
}
|
|
|
|
func encodeWindowsPath(path string) string {
|
|
// We do this because Windows allows spaces in directories such as
|
|
// Program Files but does not allow these directories to be spaced in batch files.
|
|
targetPath := strings.Replace(path, "Program Files (x86)", "PROGRA~2", -1)
|
|
// This is to do the same in 32 bit systems. We do this second so that the first
|
|
// replace is for x86 dirs.
|
|
targetPath = strings.Replace(targetPath, "Program Files", "PROGRA~1", -1)
|
|
return targetPath
|
|
}
|
|
|
|
func applyUpdate(options updateOptions, update CheckResult) UpdateOutcome {
|
|
if update.Version() == "" || options.updateDisabled {
|
|
return UpdateOutcome{UserMessage: update.UserMessage()}
|
|
}
|
|
|
|
err := update.Apply()
|
|
if err != nil {
|
|
return UpdateOutcome{Error: err}
|
|
}
|
|
|
|
return UpdateOutcome{Updated: true, Version: update.Version(), UserMessage: update.UserMessage()}
|
|
}
|
|
|
|
// Update is the handler for the update command from the command line
|
|
func Update(c *cli.Context) error {
|
|
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
|
|
|
if wasInstalledFromPackageManager() {
|
|
packageManagerName := "a package manager"
|
|
if BuiltForPackageManager != "" {
|
|
packageManagerName = BuiltForPackageManager
|
|
}
|
|
log.Error().Msg(fmt.Sprintf("cloudflared was installed by %s. Please update using the same method.", packageManagerName))
|
|
return nil
|
|
}
|
|
|
|
isBeta := c.Bool("beta")
|
|
if isBeta {
|
|
log.Info().Msg("cloudflared is set to update to the latest beta version")
|
|
}
|
|
|
|
isStaging := c.Bool("staging")
|
|
if isStaging {
|
|
log.Info().Msg("cloudflared is set to update from staging")
|
|
}
|
|
|
|
isForced := c.Bool("force")
|
|
if isForced {
|
|
log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version")
|
|
}
|
|
|
|
updateOutcome := loggedUpdate(log, updateOptions{
|
|
updateDisabled: false,
|
|
isBeta: isBeta,
|
|
isStaging: isStaging,
|
|
isForced: isForced,
|
|
intendedVersion: c.String("version"),
|
|
})
|
|
if updateOutcome.Error != nil {
|
|
return &statusErr{updateOutcome.Error}
|
|
}
|
|
|
|
if updateOutcome.noUpdate() {
|
|
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared is up to date")
|
|
return nil
|
|
}
|
|
|
|
return &statusSuccess{newVersion: updateOutcome.Version}
|
|
}
|
|
|
|
// Checks for an update and applies it if one is available
|
|
func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
|
|
checkResult, err := CheckForUpdate(options)
|
|
if err != nil {
|
|
log.Err(err).Msg("update check failed")
|
|
return UpdateOutcome{Error: err}
|
|
}
|
|
|
|
updateOutcome := applyUpdate(options, checkResult)
|
|
if updateOutcome.Updated {
|
|
log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared has been updated")
|
|
}
|
|
if updateOutcome.Error != nil {
|
|
log.Err(updateOutcome.Error).Msg("update failed to apply")
|
|
}
|
|
|
|
return updateOutcome
|
|
}
|
|
|
|
// AutoUpdater periodically checks for new version of cloudflared.
|
|
type AutoUpdater struct {
|
|
configurable *configurable
|
|
listeners *gracenet.Net
|
|
updateConfigChan chan *configurable
|
|
log *zerolog.Logger
|
|
}
|
|
|
|
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
|
type configurable struct {
|
|
enabled bool
|
|
freq time.Duration
|
|
}
|
|
|
|
func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
|
|
return &AutoUpdater{
|
|
configurable: createUpdateConfig(updateDisabled, freq, log),
|
|
listeners: listeners,
|
|
updateConfigChan: make(chan *configurable),
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Logger) *configurable {
|
|
if isAutoupdateEnabled(log, updateDisabled, freq) {
|
|
log.Info().Dur("autoupdateFreq", freq).Msg("Autoupdate frequency is set")
|
|
return &configurable{
|
|
enabled: true,
|
|
freq: freq,
|
|
}
|
|
} else {
|
|
return &configurable{
|
|
enabled: false,
|
|
freq: DefaultCheckUpdateFreq,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *AutoUpdater) Run(ctx context.Context) error {
|
|
ticker := time.NewTicker(a.configurable.freq)
|
|
for {
|
|
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
|
|
if updateOutcome.Updated {
|
|
buildInfo.CloudflaredVersion = updateOutcome.Version
|
|
if IsSysV() {
|
|
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
|
|
a.log.Info().Msg("Restarting service managed by SysV...")
|
|
pid, err := a.listeners.StartProcess()
|
|
if err != nil {
|
|
a.log.Err(err).Msg("Unable to restart server automatically")
|
|
return &statusErr{err: err}
|
|
}
|
|
// stop old process after autoupdate. Otherwise we create a new process
|
|
// after each update
|
|
a.log.Info().Msgf("PID of the new process is %d", pid)
|
|
}
|
|
return &statusSuccess{newVersion: updateOutcome.Version}
|
|
} else if updateOutcome.UserMessage != "" {
|
|
a.log.Warn().Msg(updateOutcome.UserMessage)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case newConfigurable := <-a.updateConfigChan:
|
|
ticker.Stop()
|
|
a.configurable = newConfigurable
|
|
ticker = time.NewTicker(a.configurable.freq)
|
|
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
|
|
func (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) {
|
|
a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log)
|
|
}
|
|
|
|
func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool {
|
|
if !supportAutoUpdate(log) {
|
|
return false
|
|
}
|
|
return !updateDisabled && updateFreq != 0
|
|
}
|
|
|
|
func supportAutoUpdate(log *zerolog.Logger) bool {
|
|
if runtime.GOOS == "windows" {
|
|
log.Info().Msg(noUpdateOnWindowsMessage)
|
|
return false
|
|
}
|
|
|
|
if wasInstalledFromPackageManager() {
|
|
log.Info().Msg(noUpdateManagedPackageMessage)
|
|
return false
|
|
}
|
|
|
|
if isRunningFromTerminal() {
|
|
log.Info().Msg(noUpdateInShellMessage)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func wasInstalledFromPackageManager() bool {
|
|
ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile))
|
|
return len(BuiltForPackageManager) != 0 || ok
|
|
}
|
|
|
|
func isRunningFromTerminal() bool {
|
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
|
}
|
|
|
|
func IsSysV() bool {
|
|
if runtime.GOOS != "linux" {
|
|
return false
|
|
}
|
|
|
|
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|