1
0
mirror of https://github.com/fumiama/terasu-cloudflared.git synced 2026-06-05 00:50:24 +08:00
Files
terasu-cloudflared/cmd/cloudflared/updater/update.go
João "Pisco" Fernandes 7336a1a4d6 TUN-8914: Create a flags module to group all cloudflared cli flags
## Summary

This commit refactors some of the flags of cloudflared to their own module, so that they can be used across the code without requiring to literal strings which are much more error prone.

 Closes TUN-8914
2025-02-06 03:30:27 -08:00

314 lines
8.8 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"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"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
// nolint: errname
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
}
// statusError implements ExitCoder interface, the app will exit with status code 10
type statusError struct {
err error
}
func (e *statusError) Error() string {
return fmt.Sprintf("failed to update cloudflared: %v", e.err)
}
func (e *statusError) 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
}
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(cfdflags.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 &statusError{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
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,
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,
}
}
}
// Run will perodically check for cloudflared updates, download them, and then restart the current cloudflared process
// to use the new version. It delays the first update check by the configured frequency as to not attempt a
// download immediately and restart after starting (in the case that there is an upgrade available).
func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
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 &statusError{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)
}
}
}
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
}